diff --git a/administration/src/bp-modules/cards/AddCardForm.tsx b/administration/src/bp-modules/cards/AddCardForm.tsx index fbd005edf..4ae634c4f 100644 --- a/administration/src/bp-modules/cards/AddCardForm.tsx +++ b/administration/src/bp-modules/cards/AddCardForm.tsx @@ -28,6 +28,8 @@ const AddCardForm = ({ card, onRemove, updateCard }: CreateCardsFormProps): Reac const today = PlainDate.fromLocalDate(new Date()) const { t } = useTranslation('cards') + console.log(card) + return ( diff --git a/administration/src/bp-modules/cards/CreateCardsButtonBar.tsx b/administration/src/bp-modules/cards/CreateCardsButtonBar.tsx index 2a5537f67..f7e9be613 100644 --- a/administration/src/bp-modules/cards/CreateCardsButtonBar.tsx +++ b/administration/src/bp-modules/cards/CreateCardsButtonBar.tsx @@ -19,8 +19,8 @@ const CreateCardsButtonBar = ({ generateCardsCsv, goBack, }: CreateCardsButtonBarProps): ReactElement => { - const allCardsValid = cards.every(card => isValid(card)) - const { csvExport } = useContext(ProjectConfigContext) + const { csvExport, card: cardConfig } = useContext(ProjectConfigContext) + const allCardsValid = cards.every(card => isValid(card, cardConfig)) const { t } = useTranslation('cards') return ( diff --git a/administration/src/bp-modules/cards/ImportCardsInput.test.tsx b/administration/src/bp-modules/cards/ImportCardsInput.test.tsx index 9593103a0..cfa3b2c2c 100644 --- a/administration/src/bp-modules/cards/ImportCardsInput.test.tsx +++ b/administration/src/bp-modules/cards/ImportCardsInput.test.tsx @@ -2,6 +2,7 @@ import { OverlayToaster } from '@blueprintjs/core' import { fireEvent, waitFor } from '@testing-library/react' import React from 'react' +import { BAVARIA_CARD_TYPE_GOLD, BAVARIA_CARD_TYPE_STANDARD } from '../../cards/extensions/BavariaCardTypeExtension' import { Region } from '../../generated/graphql' import { ProjectConfigProvider } from '../../project-configs/ProjectConfigContext' import bayernConfig from '../../project-configs/bayern/config' @@ -61,7 +62,7 @@ describe('ImportCardsInput', () => { const csv = ` Name,Ablaufdatum,Kartentyp Thea Test,03.04.2024,Standard -Tilo Traber,,Gold +Tilo Traber,,gold ` await renderAndSubmitCardsInput(projectConfig, csv, setCards) @@ -70,20 +71,54 @@ Tilo Traber,,Gold expect(setCards).toHaveBeenCalledWith([ { expirationDate: PlainDate.fromCustomFormat('03.04.2024'), - extensions: { bavariaCardType: 'Standard', regionId: 0 }, + extensions: { bavariaCardType: BAVARIA_CARD_TYPE_STANDARD, regionId: 0 }, fullName: 'Thea Test', id: expect.any(Number), }, - { expirationDate: null, extensions: { regionId: 0 }, fullName: 'Tilo Traber', id: expect.any(Number) }, + { + expirationDate: null, + extensions: { regionId: 0, bavariaCardType: BAVARIA_CARD_TYPE_GOLD }, + fullName: 'Tilo Traber', + id: expect.any(Number), + }, + ]) + }) + + it('should correctly import CSV Card for bayern freinet', async () => { + jest.spyOn(URLSearchParams.prototype, 'get').mockReturnValue('true') + + const projectConfig = bayernConfig + const csv = ` +inhaber_ehrenamtskarte;eak_datum;eak_karten_status;co_name;anrede;titel;vorname;nachname;strasse;plz;ort +�Blau�;01.12.2029;Karte abgelaufen;;Herr;;Maxim;Musterin;Kirchgasse 30;97346;Iphofen +�Gold�;;Karte an EA verschickt;;Herr;;Max;Muster;Kleinlangheimer Stra�e 12;97355;Kleinlangheim +` + await renderAndSubmitCardsInput(projectConfig, csv, setCards) + + expect(toaster).not.toHaveBeenCalled() + expect(setCards).toHaveBeenCalledTimes(1) + expect(setCards).toHaveBeenCalledWith([ + { + expirationDate: PlainDate.fromCustomFormat('01.12.2029'), + extensions: { bavariaCardType: BAVARIA_CARD_TYPE_STANDARD, regionId: 0 }, + fullName: 'Maxim Musterin', + id: expect.any(Number), + }, + { + expirationDate: null, + extensions: { regionId: 0, bavariaCardType: BAVARIA_CARD_TYPE_GOLD }, + fullName: 'Max Muster', + id: expect.any(Number), + }, ]) }) it('should correctly import CSV Card for nuernberg', async () => { const projectConfig = nuernbergConfig const csv = ` -Name,Ablaufdatum,Geburtsdatum,Pass-ID -Thea Test,03.04.2024,10.10.2000,12345678 -Tilo Traber,03.04.2025,12.01.1984,98765432 +Name,Ablaufdatum,Startdatum,Geburtsdatum,Pass-ID +Thea Test,03.04.2024,01.01.2026,10.10.2000,12345678 +Tilo Traber,03.04.2025,01.01.2026,12.01.1984,98765432 ` await renderAndSubmitCardsInput(projectConfig, csv, setCards) @@ -93,13 +128,23 @@ Tilo Traber,03.04.2025,12.01.1984,98765432 expect(setCards).toHaveBeenCalledWith([ { expirationDate: PlainDate.fromCustomFormat('03.04.2024'), - extensions: { birthday: PlainDate.fromCustomFormat('10.10.2000'), regionId: 0, nuernbergPassId: 12345678 }, + extensions: { + birthday: PlainDate.fromCustomFormat('10.10.2000'), + regionId: 0, + nuernbergPassId: 12345678, + startDay: PlainDate.fromCustomFormat('01.01.2026'), + }, fullName: 'Thea Test', id: expect.any(Number), }, { expirationDate: PlainDate.fromCustomFormat('03.04.2025'), - extensions: { birthday: PlainDate.fromCustomFormat('12.01.1984'), regionId: 0, nuernbergPassId: 98765432 }, + extensions: { + birthday: PlainDate.fromCustomFormat('12.01.1984'), + regionId: 0, + nuernbergPassId: 98765432, + startDay: PlainDate.fromCustomFormat('01.01.2026'), + }, fullName: 'Tilo Traber', id: expect.any(Number), }, diff --git a/administration/src/bp-modules/self-service/CardSelfServiceForm.tsx b/administration/src/bp-modules/self-service/CardSelfServiceForm.tsx index b8c596b7c..53be0a9ad 100644 --- a/administration/src/bp-modules/self-service/CardSelfServiceForm.tsx +++ b/administration/src/bp-modules/self-service/CardSelfServiceForm.tsx @@ -52,7 +52,7 @@ const CardSelfServiceForm = ({ const [openDataPrivacy, setOpenDataPrivacy] = useState(false) const [openReferenceInformation, setOpenReferenceInformation] = useState(false) const [_, setSearchParams] = useSearchParams() - const cardValid = isValid(card, { expirationDateNullable: true }) + const cardValid = isValid(card, projectConfig.card, { expirationDateNullable: true }) const appToaster = useAppToaster() const showErrorMessage = touchedFullName || formSendAttempt diff --git a/administration/src/cards/Card.test.ts b/administration/src/cards/Card.test.ts index 7a5f55abf..96bc553d7 100644 --- a/administration/src/cards/Card.test.ts +++ b/administration/src/cards/Card.test.ts @@ -1,5 +1,8 @@ import { BavariaCardType } from '../generated/card_pb' import { Region } from '../generated/graphql' +import bayernConfig from '../project-configs/bayern/config' +import koblenzConfig from '../project-configs/koblenz/config' +import nuernbergConfig from '../project-configs/nuernberg/config' import PlainDate from '../util/PlainDate' import { MAX_NAME_LENGTH, @@ -104,7 +107,7 @@ describe('Card', () => { expect(isValueValid(card, cardConfig, 'Name')).toBeTruthy() expect(isValueValid(card, cardConfig, 'Ablaufdatum')).toBeTruthy() expect(isValueValid(card, cardConfig, 'Kartentyp')).toBeTruthy() - expect(isValid(card)).toBeTruthy() + expect(isValid(card, cardConfig)).toBeTruthy() expect(getValueByCSVHeader(card, cardConfig, 'Name')).toBe('Thea Test') expect(getValueByCSVHeader(card, cardConfig, 'Ablaufdatum')).toBe(date.format()) @@ -136,7 +139,7 @@ describe('Card', () => { expect(isValueValid(card, cardConfig, 'Name')).toBeFalsy() expect(isValueValid(card, cardConfig, 'Ablaufdatum')).toBeFalsy() expect(isValueValid(card, cardConfig, 'Kartentyp')).toBeFalsy() - expect(isValid(card)).toBeFalsy() + expect(isValid(card, cardConfig)).toBeFalsy() }) }) @@ -146,7 +149,7 @@ describe('Card', () => { const card = initializeCard(cardConfig, region, { fullName }) expect(card.fullName).toBe(fullName) expect(isValueValid(card, cardConfig, 'Name')).toBeFalsy() - expect(isValid(card)).toBeFalsy() + expect(isValid(card, cardConfig)).toBeFalsy() } ) @@ -156,7 +159,7 @@ describe('Card', () => { const card = initializeCard(cardConfig, region, { fullName }) expect(card.fullName).toBe(fullName) expect(isValueValid(card, cardConfig, 'Name')).toBeTruthy() - expect(isValid(card)).toBeTruthy() + expect(isValid(card, cardConfig)).toBeTruthy() } ) @@ -166,22 +169,68 @@ describe('Card', () => { const card = initializeCard(cardConfig, region, { fullName }) expect(card.fullName).toBe(fullName) expect(isValueValid(card, cardConfig, 'Name')).toBeTruthy() - expect(isValid(card)).toBeTruthy() + expect(isValid(card, cardConfig)).toBeTruthy() } ) it.each(['Karla', 'Peter'])('should correctly identify invalid fullname that is incomplete', fullName => { const card = initializeCard(cardConfig, region, { fullName }) expect(isValueValid(card, cardConfig, 'Name')).toBeFalsy() - expect(isValid(card)).toBeFalsy() + expect(isValid(card, cardConfig)).toBeFalsy() }) it(`should correctly identify invalid fullname that exceeds max length (${MAX_NAME_LENGTH} characters)`, () => { const card = initializeCard(cardConfig, region, { fullName: 'Karl LauterLauterLauterLauterLauterLauterLauterbach' }) expect(isValueValid(card, cardConfig, 'Name')).toBeFalsy() - expect(isValid(card)).toBeFalsy() + expect(isValid(card, cardConfig)).toBeFalsy() }) + it.each([ + { + projectConfig: bayernConfig, + line: ['Thea Test', '03.03.2026'], + headers: ['Name', 'Ablaufdatum'], + missingExtension: 'Kartentyp', + }, + { + projectConfig: nuernbergConfig, + line: ['Thea Test', '03.04.2024', '10.10.2000', '12345678'], + headers: ['Name', 'Ablaufdatum', 'Geburtsdatum', 'Pass-ID'], + missingExtension: 'Startdatum', + }, + { + projectConfig: nuernbergConfig, + line: ['Thea Test', '03.04.2024', '10.10.2000', '10.12.2024'], + headers: ['Name', 'Ablaufdatum', 'Geburtsdatum', 'Startdatum'], + missingExtension: 'Pass-ID', + }, + { + projectConfig: nuernbergConfig, + line: ['Thea Test', '03.04.2027', '12345678', '10.12.2024'], + headers: ['Name', 'Ablaufdatum', 'Pass-ID', 'Startdatum'], + missingExtension: 'Geburtsdatum', + }, + { + projectConfig: koblenzConfig, + line: ['Thea Test', '03.03.2026', '12.12.2000'], + headers: ['Name', 'Ablaufdatum', 'Geburtsdatum'], + missingExtension: 'Referenznummer', + }, + { + projectConfig: koblenzConfig, + line: ['Thea Test', '03.03.2026', '123K'], + headers: ['Name', 'Ablaufdatum', 'Referenznummer'], + missingExtension: 'Geburtsdatum', + }, + ])( + `should invalidate card if '$missingExtension' is not provided for '$projectConfig.name'`, + ({ projectConfig, line, headers, missingExtension }) => { + const card = initializeCardFromCSV(projectConfig.card, line, headers, region) + expect(isValueValid(card, projectConfig.card, missingExtension)).toBeFalsy() + expect(isValid(card, cardConfig)).toBeFalsy() + } + ) + describe('self service', () => { const cardConfig = { defaultValidity: { years: 3 }, diff --git a/administration/src/cards/Card.ts b/administration/src/cards/Card.ts index 2c1224728..e4f3dc0d3 100644 --- a/administration/src/cards/Card.ts +++ b/administration/src/cards/Card.ts @@ -131,10 +131,28 @@ export const isExpirationDateValid = (card: Card, { nullable } = { nullable: fal ) } -export const isValid = (card: Card, { expirationDateNullable } = { expirationDateNullable: false }): boolean => - isFullNameValid(card) && - getExtensions(card).every(({ extension, state }) => extension.isValid(state)) && - (isExpirationDateValid(card, { nullable: expirationDateNullable }) || hasInfiniteLifetime(card)) +export const cardHasAllMandatoryExtensions = (card: Card, cardConfig: CardConfig): boolean => { + const mandatoryExtensions = cardConfig.extensions.filter(extension => extension.isMandatory) + return mandatoryExtensions.every(extension => Object.keys(card.extensions).includes(extension.name)) +} + +export const isValid = ( + card: Card, + cardConfig: CardConfig, + { expirationDateNullable } = { expirationDateNullable: false } +): boolean => { + console.log('fullname:', isFullNameValid(card)) + console.log( + 'expirationDate:', + isExpirationDateValid(card, { nullable: expirationDateNullable }) || hasInfiniteLifetime(card) + ) + return ( + isFullNameValid(card) && + getExtensions(card).every(({ extension, state }) => extension.isValid(state)) && + (isExpirationDateValid(card, { nullable: expirationDateNullable }) || hasInfiniteLifetime(card)) && + cardHasAllMandatoryExtensions(card, cardConfig) + ) +} export const generateCardInfo = (card: Card): CardInfo => { const extensionsMessage: PartialMessage = getExtensions(card).reduce( diff --git a/administration/src/cards/extensions/AddressFieldExtensions.ts b/administration/src/cards/extensions/AddressFieldExtensions.ts index 47d746bcf..492fc05f2 100644 --- a/administration/src/cards/extensions/AddressFieldExtensions.ts +++ b/administration/src/cards/extensions/AddressFieldExtensions.ts @@ -26,6 +26,7 @@ const getAddressFieldExtension = ( toString: (state): string => state[name], fromSerialized: (value: string) => ({ [name]: value } as AddressFieldExtensionState), serialize: (state): string => state[name], + isMandatory: false, }) export const AddressLine1Extension = getAddressFieldExtension(ADDRESS_LINE_1_EXTENSION) diff --git a/administration/src/cards/extensions/BavariaCardTypeExtension.tsx b/administration/src/cards/extensions/BavariaCardTypeExtension.tsx index 6628fa23c..8f74ec33f 100644 --- a/administration/src/cards/extensions/BavariaCardTypeExtension.tsx +++ b/administration/src/cards/extensions/BavariaCardTypeExtension.tsx @@ -75,6 +75,7 @@ const BavariaCardTypeExtension: Extension = { toString, fromSerialized: fromString, serialize: toString, + isMandatory: true, } export default BavariaCardTypeExtension diff --git a/administration/src/cards/extensions/BirthdayExtension.tsx b/administration/src/cards/extensions/BirthdayExtension.tsx index cf3bddbb6..1cc6b2aca 100644 --- a/administration/src/cards/extensions/BirthdayExtension.tsx +++ b/administration/src/cards/extensions/BirthdayExtension.tsx @@ -78,6 +78,7 @@ const BirthdayExtension: Extension = { return birthday === null ? null : { birthday } }, serialize: ({ birthday }: BirthdayExtensionState) => birthday?.formatISO() ?? '', + isMandatory: true, } export default BirthdayExtension diff --git a/administration/src/cards/extensions/EMailNotificationExtension.ts b/administration/src/cards/extensions/EMailNotificationExtension.ts index ef0b03ac3..80581c3b4 100644 --- a/administration/src/cards/extensions/EMailNotificationExtension.ts +++ b/administration/src/cards/extensions/EMailNotificationExtension.ts @@ -17,6 +17,7 @@ const EMailNotificationExtension: Extension = { toString, fromSerialized: fromString, serialize: toString, + isMandatory: false, } export default EMailNotificationExtension diff --git a/administration/src/cards/extensions/KoblenzReferenceNumberExtension.tsx b/administration/src/cards/extensions/KoblenzReferenceNumberExtension.tsx index 567ccdb64..71b921896 100644 --- a/administration/src/cards/extensions/KoblenzReferenceNumberExtension.tsx +++ b/administration/src/cards/extensions/KoblenzReferenceNumberExtension.tsx @@ -87,6 +87,7 @@ const KoblenzReferenceNumberExtension: Extension = { toString, fromSerialized: fromString, serialize: toString, + isMandatory: true, } export default NuernbergPassIdExtension diff --git a/administration/src/cards/extensions/RegionExtension.ts b/administration/src/cards/extensions/RegionExtension.ts index 1c8070914..13b02f784 100644 --- a/administration/src/cards/extensions/RegionExtension.ts +++ b/administration/src/cards/extensions/RegionExtension.ts @@ -21,6 +21,7 @@ const RegionExtension: Extension = { fromString, fromSerialized: fromString, serialize: toString, + isMandatory: true, } export default RegionExtension diff --git a/administration/src/cards/extensions/StartDayExtension.tsx b/administration/src/cards/extensions/StartDayExtension.tsx index cfc5a241f..23cd72ef8 100644 --- a/administration/src/cards/extensions/StartDayExtension.tsx +++ b/administration/src/cards/extensions/StartDayExtension.tsx @@ -6,7 +6,7 @@ import PlainDate from '../../util/PlainDate' import { Extension, ExtensionComponentProps } from './extensions' export const START_DAY_EXTENSION_NAME = 'startDay' -export type StartDayExtensionState = { [START_DAY_EXTENSION_NAME]: PlainDate } +export type StartDayExtensionState = { [START_DAY_EXTENSION_NAME]: PlainDate | null } // Some minimum start day after 1970 is necessary, as we use an uint32 in the protobuf. const minStartDay = new PlainDate(2020, 1, 1) @@ -19,7 +19,7 @@ const StartDayForm = ({ value, setValue, isValid }: ExtensionComponentProps ) -const isStartDayValid = ({ startDay }: StartDayExtensionState): boolean => startDay.isAfter(minStartDay) +const isStartDayValid = ({ startDay }: StartDayExtensionState): boolean => + startDay ? startDay.isAfter(minStartDay) : false const StartDayExtension: Extension = { name: START_DAY_EXTENSION_NAME, @@ -44,7 +45,7 @@ const StartDayExtension: Extension = { causesInfiniteLifetime: () => false, getProtobufData: (state: StartDayExtensionState) => ({ extensionStartDay: { - startDay: isStartDayValid(state) ? state.startDay.toDaysSinceEpoch() : minStartDay.toDaysSinceEpoch(), + startDay: isStartDayValid(state) ? state.startDay?.toDaysSinceEpoch() : minStartDay.toDaysSinceEpoch(), }, }), isValid: isStartDayValid, @@ -52,12 +53,13 @@ const StartDayExtension: Extension = { const startDay = PlainDate.safeFromCustomFormat(value) return startDay === null ? null : { startDay } }, - toString: ({ startDay }: StartDayExtensionState) => startDay.format(), + toString: ({ startDay }: StartDayExtensionState) => startDay?.format() ?? '', fromSerialized: (value: string) => { const startDay = PlainDate.safeFrom(value) return startDay === null ? null : { startDay } }, - serialize: ({ startDay }: StartDayExtensionState) => startDay.formatISO(), + serialize: ({ startDay }: StartDayExtensionState) => startDay?.formatISO() ?? '', + isMandatory: true, } export default StartDayExtension diff --git a/administration/src/cards/extensions/extensions.tsx b/administration/src/cards/extensions/extensions.tsx index 2f85404c2..6bd244c09 100644 --- a/administration/src/cards/extensions/extensions.tsx +++ b/administration/src/cards/extensions/extensions.tsx @@ -41,6 +41,7 @@ export type Extension> = { toString(state: T): string fromSerialized(value: string): T | null serialize(state: T): string + isMandatory: boolean } const Extensions = [