Skip to content
54 changes: 51 additions & 3 deletions administration/src/cards/card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
initializeCardFromCSV,
isValid,
isValueValid,
updateCard,
} from './card'
import AddressExtensions from './extensions/AddressFieldExtensions'
import BavariaCardTypeExtension, {
Expand Down Expand Up @@ -51,6 +52,29 @@ describe('Card', () => {
expect(card.extensions[REGION_EXTENSION_NAME]).toEqual(region.id)
})

describe('updateCard', () => {
it('should clear the expirationDate when the card becomes an infinite-lifetime Goldkarte', () => {
const card = initializeCard(cardConfig, region, { fullName: 'Thea Test' })
expect(card.expirationDate).not.toBeNull()

const goldCard = updateCard(card, {
extensions: { [BAVARIA_CARD_TYPE_EXTENSION_NAME]: 'Goldkarte' },
})

expect(goldCard.expirationDate).toBeNull()
expect(isValid(goldCard, cardConfig)).toBe(true)
})

it('should keep the expirationDate for a card with finite lifetime', () => {
const card = initializeCard(cardConfig, region, { fullName: 'Thea Test' })
const newExpirationDate = Temporal.PlainDate.from('2022-06-15')

const updated = updateCard(card, { expirationDate: newExpirationDate })

expect(updated.expirationDate).toEqual(newExpirationDate)
})
})

it('should generate CardInfo even with invalid expiration date', () => {
const card = initializeCard(cardConfig, region, {
fullName: '',
Expand Down Expand Up @@ -103,13 +127,13 @@ describe('Card', () => {
it('should correctly set and get value', () => {
const dateString = '03.04.2022'
const date = parseGermanPlainDateString(dateString)
const line = ['Thea Test', dateString, 'Goldkarte']
const line = ['Thea Test', dateString, 'Standard']
const headers = ['Name', 'Ablaufdatum', 'Kartentyp']
const card = initializeCardFromCSV(cardConfig, line, headers, region)

expect(card.fullName).toBe('Thea Test')
expect(card.expirationDate).toEqual(date)
expect(card.extensions[BAVARIA_CARD_TYPE_EXTENSION_NAME]).toBe('Goldkarte')
expect(card.extensions[BAVARIA_CARD_TYPE_EXTENSION_NAME]).toBe('Standard')

expect(isValueValid(card, cardConfig, 'Name')).toBeTruthy()
expect(isValueValid(card, cardConfig, 'Ablaufdatum')).toBeTruthy()
Expand All @@ -120,7 +144,31 @@ describe('Card', () => {
expect(getValueByCSVHeader(card, cardConfig, 'Ablaufdatum')).toBe(
formatDateDefaultGerman(date),
)
expect(getValueByCSVHeader(card, cardConfig, 'Kartentyp')).toBe('Goldkarte')
expect(getValueByCSVHeader(card, cardConfig, 'Kartentyp')).toBe('Standard')
})

it('should treat a gold card with an expiration date as invalid', () => {
const line = ['Thea Test', '03.04.2022', 'Goldkarte']
const headers = ['Name', 'Ablaufdatum', 'Kartentyp']
const card = initializeCardFromCSV(cardConfig, line, headers, region)

expect(card.expirationDate).not.toBeNull()
expect(card.extensions[BAVARIA_CARD_TYPE_EXTENSION_NAME]).toBe('Goldkarte')

expect(isValueValid(card, cardConfig, 'Ablaufdatum')).toBeFalsy()
expect(isValid(card, cardConfig)).toBeFalsy()
})

it('should treat a gold card without an expiration date as valid', () => {
const line = ['Thea Test', '', 'Goldkarte']
const headers = ['Name', 'Ablaufdatum', 'Kartentyp']
const card = initializeCardFromCSV(cardConfig, line, headers, region)

expect(card.expirationDate).toBeNull()
expect(card.extensions[BAVARIA_CARD_TYPE_EXTENSION_NAME]).toBe('Goldkarte')

expect(isValueValid(card, cardConfig, 'Ablaufdatum')).toBeTruthy()
expect(isValid(card, cardConfig)).toBeTruthy()
})

it('should not modify value for invalid header', () => {
Expand Down
27 changes: 15 additions & 12 deletions administration/src/cards/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ export const isExpirationDateValid = (card: Card, { nullable } = { nullable: fal
const startDay = card.extensions.startDay

if (card.expirationDate === null) {
return nullable
return nullable || hasInfiniteLifetime(card)
}

if (hasInfiniteLifetime(card)) {
return false
}

return (
Expand All @@ -167,8 +171,7 @@ export const isValid = (
): boolean =>
isFullNameValid(card) &&
getExtensions(card).every(({ extension, state }) => extension.isValid(state)) &&
(isExpirationDateValid(card, { nullable: expirationDateNullable }) ||
hasInfiniteLifetime(card)) &&
isExpirationDateValid(card, { nullable: expirationDateNullable }) &&
cardHasAllMandatoryExtensions(card, cardConfig)

export const generateCardInfo = (card: Card): CardInfo => {
Expand Down Expand Up @@ -203,7 +206,7 @@ export const isValueValid = (card: Card, cardConfig: CardConfig, columnHeader: s
case cardConfig.nameColumnName:
return isFullNameValid(card)
case cardConfig.expiryColumnName:
return isExpirationDateValid(card) || hasInfiniteLifetime(card)
return isExpirationDateValid(card)
default: {
const extensionName = getExtensionNameByCSVHeader(cardConfig, columnHeader)
const extension = cardConfig.extensions.find(extension => extension.name === extensionName)
Expand Down Expand Up @@ -269,14 +272,14 @@ export const initializeCardFromCSV = (
}
}

export const updateCard = (oldCard: Card, updatedCard: Partial<Card>): Card => ({
...oldCard,
...updatedCard,
extensions: {
...oldCard.extensions,
...(updatedCard.extensions ?? {}),
},
})
export const updateCard = (oldCard: Card, updatedCard: Partial<Card>): Card => {
const mergedCard = {
...oldCard,
...updatedCard,
extensions: { ...oldCard.extensions, ...updatedCard.extensions },
}
return hasInfiniteLifetime(mergedCard) ? { ...mergedCard, expirationDate: null } : mergedCard
}

export const getFullNameValidationErrorMessage = (name: string): string => {
const normalizedName = normalizeWhitespace(name)
Expand Down
7 changes: 6 additions & 1 deletion administration/src/components/FormAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@
severity = 'error',
}: FormAlertProps): ReactElement | null =>
errorMessage != null ? (
<Container $severity={severity} $isToast={isToast} data-testid='form-alert'>
<Container
$severity={severity}
$isToast={isToast}
role={isToast ? undefined : 'alert'}

Check warning on line 37 in administration/src/components/FormAlert.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
data-testid='form-alert'
>
<InfoOutlined />
<Typography component='span'>{errorMessage}</Typography>
</Container>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Check, Close } from '@mui/icons-material'
import { Alert, Button, Card, Divider, Typography, styled } from '@mui/material'
import { Button, Card, Divider, Typography, styled } from '@mui/material'
import React, { ReactElement, useContext } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Temporal } from 'temporal-polyfill'

import AlertBox from '../../components/AlertBox'
import JsonFieldView from '../../components/JsonFieldView'
import PageLayout from '../../components/PageLayout'
import { GetApplicationByApplicantQuery } from '../../graphql'
Expand All @@ -17,11 +18,6 @@ const ApplicationViewCard = styled(Card)`
margin: 16px auto 16px auto;
`

const StyledAlert = styled(Alert)`
margin: 20px 0;
background-color: transparent;
`

const ButtonContainer = styled('div')`
display: flex;
width: inherit;
Expand Down Expand Up @@ -78,9 +74,11 @@ const ApplicationVerifierView = ({
values={{ organizationName: verification.organizationName }}
/>
</Typography>
<StyledAlert severity='warning'>
<Trans i18nKey='applicationVerification:confirmationNote' />
</StyledAlert>
<AlertBox
severity='warning'
description={<Trans i18nKey='applicationVerification:confirmationNote' />}
sx={{ my: 3 }}
/>

<ButtonContainer>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,18 @@
RejectApplicationStatusDocument,
)

const deleteApplication = async () => {
setDeleteDialogCalled(true)
const result = await deleteApplicationMutation({ applicationId: application.id })

if (result.error) {
const { title } = messageFromGraphQlError(result.error)
enqueueSnackbar(title, { variant: 'error' })
setDeleteDialogCalled(false)
} else if (result.data) {
if (result.data.deleted) {
onDelete()
enqueueSnackbar(t('deleteApplicationSuccessMessage'), { variant: 'success' })

Check warning on line 278 in administration/src/routes/applications/components/ApplicationCard.tsx

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
console.error('Delete operation returned false.')
enqueueSnackbar(t('errors:unknown'), { variant: 'error' })
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Alert, Typography, styled } from '@mui/material'
import { Typography } from '@mui/material'
import { Trans, useTranslation } from 'react-i18next'

import AlertBox from '../../../../components/AlertBox'
import { OrganizationInput } from '../../../../graphql'
import i18next from '../../../../translations/i18n'
import { normalizeName } from '../../../../util/normalizeString'
Expand All @@ -18,10 +19,6 @@ import SelectForm from '../primitive-inputs/SelectForm'
import ShortTextForm from '../primitive-inputs/ShortTextForm'
import AddressForm from './AddressForm'

const WarningContactPersonSamePerson = styled(Alert)`
margin: 8px 0;
`

const organizationCategoryOptions = {
items: [
'social',
Expand Down Expand Up @@ -110,9 +107,12 @@ const OrganizationForm: Form<State, OrganizationInput, AdditionalProps> = {
</Typography>
<Typography>{t('applicationForms:organizationContactPersonDescription')}</Typography>
{normalizeName(applicantName) === normalizeName(state.contactName.shortText) && (
<WarningContactPersonSamePerson severity='warning'>
<Trans i18nKey='applicationForms:organizationContactPersonAlert' />
</WarningContactPersonSamePerson>
<AlertBox
fullWidth
severity='warning'
sx={{ margin: '8px 0' }}
description={<Trans i18nKey='applicationForms:organizationContactPersonAlert' />}
/>
)}
<ShortTextForm.Component
state={state.contactName}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable react/jsx-pascal-case -- we cannot change the keys of application namespace, see translation file comment */
import { Alert, CircularProgress, Link, Typography } from '@mui/material'
import { styled } from '@mui/system'
import { CircularProgress, Link, Typography } from '@mui/material'
import { TFunction } from 'i18next'
import { useContext, useEffect } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { UseQueryState, useQuery } from 'urql'

import AlertBox from '../../../../components/AlertBox'
import {
GetRegionsByPostalCodeDocument,
GetRegionsByPostalCodeQuery,
Expand All @@ -22,12 +22,7 @@ import {
import { Form, FormComponentProps } from '../../util/formType'
import SelectForm, { SelectItem } from '../primitive-inputs/SelectForm'

const StyledAlert = styled(Alert)`
margin: 16px 0;
transition:
background-color 0.2s,
color 0.2s;
`
const alertSx = { margin: '16px 0', transition: 'background-color 0.2s, color 0.2s' }

const SubForms = {
region: SelectForm,
Expand Down Expand Up @@ -57,45 +52,69 @@ const renderAlert = (
return null
}
if (postalCode.length !== 5) {
return <StyledAlert severity='error'>{t('regionAlertPostalCode')}</StyledAlert>
return (
<AlertBox fullWidth severity='error' sx={alertSx} description={t('regionAlertPostalCode')} />
)
}
if (queryState.fetching || !isCurrentResult) {
return <StyledAlert severity='info' icon={<CircularProgress size='1em' />} />
return (
<AlertBox
fullWidth
severity='info'
sx={alertSx}
customIcon={<CircularProgress size='1em' />}
/>
)
}
if (queryState.error) {
return (
<StyledAlert severity='warning'>
<Trans i18nKey='applicationForms:regionNotDetermined' />
</StyledAlert>
<AlertBox
fullWidth
severity='warning'
sx={alertSx}
description={<Trans i18nKey='applicationForms:regionNotDetermined' />}
/>
)
}
if (queryState.data && queryState.data.regions.length === 0) {
return (
<StyledAlert severity='warning'>
<Trans i18nKey='applicationForms:regionNotDetermined' />
</StyledAlert>
<AlertBox
fullWidth
severity='warning'
sx={alertSx}
description={<Trans i18nKey='applicationForms:regionNotDetermined' />}
/>
)
}
if (queryState.data && queryState.data.regions.length > 1) {
const regions = queryState.data.regions
return (
<StyledAlert severity='warning'>
<Trans i18nKey='applicationForms:regionNotUnique' />
<Typography component='ul' sx={{ marginX: 0.5 }}>
{regions.map(region => {
const displayName = `${region.name} (${region.prefix})`
return (
<Typography component='li' key={displayName}>
{displayName}
</Typography>
)
})}
</Typography>
</StyledAlert>
<AlertBox
fullWidth
severity='warning'
sx={alertSx}
description={
<>
<Trans i18nKey='applicationForms:regionNotUnique' />
<Typography component='ul' sx={{ marginX: 0.5 }}>
{regions.map(region => {
const displayName = `${region.name} (${region.prefix})`
return (
<Typography component='li' key={displayName}>
{displayName}
</Typography>
)
})}
</Typography>
</>
}
/>
)
}
if (queryState.data) {
return <StyledAlert severity='success'>{t('regionDetermined')}</StyledAlert>
return (
<AlertBox fullWidth severity='success' sx={alertSx} description={t('regionDetermined')} />
)
}
return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const StoresImport = ({ fields }: StoreImportProps): ReactElement => {
/>,
{
persist: true,
variant: 'success',
},
)
setAcceptingStores([])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const StoresImportAlert = ({
)}
</>
)}
<br />
<BaseCheckbox
checked={dryRun}
onChange={checked => setDryRun(checked)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import React from 'react'

import type { StoresFieldConfig } from '../../../../project-configs'
import { storesManagementConfig } from '../../../../project-configs/storesManagementConfig'
import {
FIELD_LATITUDE,
FIELD_LONGITUDE,
storesManagementConfig,
} from '../../../../project-configs/storesManagementConfig'
import { renderWithOptions } from '../../../../testing/render'
import StoresRequirementsText from './StoresRequirementsText'

describe('StoresRequirementsText', () => {
const fields = (storesManagementConfig as { enabled: boolean; fields: StoresFieldConfig[] })
.fields
const expectedHeaders = fields
// Long/Lat is only required for the import not for the csv file, since it will be resolved by location
.filter(field => ![FIELD_LATITUDE, FIELD_LONGITUDE].includes(field.name))
.map(field => (field.isMandatory ? `${field.name}*` : field.name))
.join(', ')

Expand Down
Loading