diff --git a/frontend/src/routes/guest/register/index.test.tsx b/frontend/src/routes/guest/register/index.test.tsx index 18c271998077ca40e7c3fcd0c9e9ad8410b09ed4..2863233310172efed13a54444400a6a3fc804f0e 100644 --- a/frontend/src/routes/guest/register/index.test.tsx +++ b/frontend/src/routes/guest/register/index.test.tsx @@ -13,9 +13,10 @@ const testData = { last_name: 'Tester', private_mobile: '+4797543910', private_email: 'test@example.org', - fnr: '04062141242', + fnr: '08015214555', passport: 'DK-123456', - date_of_birth: '1995-02-25', + date_of_birth: '1952-01-08', + gender: '', }, role: { ou_name_en: 'English organizational unit name', @@ -31,13 +32,10 @@ const testData = { }, } -beforeEach(() => { +test('Field showing values correctly', async () => { fetchMock.mockIf('/api/ui/v1/invited/', () => Promise.resolve<any>(JSON.stringify(testData)) ) -}) - -test('Field showing values correctly', async () => { render( <LocalizationProvider dateAdapter={AdapterDateFns}> <GuestRegister /> @@ -49,6 +47,10 @@ test('Field showing values correctly', async () => { await screen.findByDisplayValue(testData.person.private_email) await screen.findByDisplayValue(testData.person.fnr) + // Check that suggestions for date of birth and gender are showing + await screen.findByDisplayValue(testData.person.date_of_birth) + await screen.findByDisplayValue('male') + // Passport nationality. The i18n-mock sets up en as the i18n.language property, so look for the English name await screen.findByText('DK') await screen.findByDisplayValue('123456') @@ -69,3 +71,52 @@ test('Field showing values correctly', async () => { // For the default setup the contact person at unit field should be showing await screen.findByDisplayValue(testData.role.contact_person_unit) }) + +test('Gender and birth date suggestions not if no national ID given', async () => { + const existingDateOfBirth = testData.person.date_of_birth + testData.person.fnr = '' + testData.person.date_of_birth = '' + + fetchMock.mockIf('/api/ui/v1/invited/', () => + Promise.resolve<any>(JSON.stringify(testData)) + ) + render( + <LocalizationProvider dateAdapter={AdapterDateFns}> + <GuestRegister /> + </LocalizationProvider> + ) + + // Wait a bit so that all the values are showing + await screen.findByDisplayValue(testData.person.first_name) + await screen.findByDisplayValue(testData.person.last_name) + + // No national is given in the input data so there should be no + // suggestion for the birthdate or gender + const dateOfBirth = screen.queryByDisplayValue(existingDateOfBirth) + expect(dateOfBirth).toBeNull() + + const gender = screen.queryByDisplayValue('male') + expect(gender).toBeNull() +}) + +test('Gender and birth date suggestions not if no national ID given', async () => { + // Make the date of birth and national ID not match + testData.person.fnr = '08015214555' + testData.person.date_of_birth = '1960-01-08' + // Also set the gender to female to check that it is not overridden by a suggestion + testData.person.gender = 'female' + + fetchMock.mockIf('/api/ui/v1/invited/', () => + Promise.resolve<any>(JSON.stringify(testData)) + ) + render( + <LocalizationProvider dateAdapter={AdapterDateFns}> + <GuestRegister /> + </LocalizationProvider> + ) + + // In this a date of birth was already set, and it should not have been overridden by a suggestion + await screen.findByDisplayValue(testData.person.date_of_birth) + // Check that the gender has not been overridden + await screen.findByDisplayValue('female') +}) diff --git a/frontend/src/routes/guest/register/steps/register.test.tsx b/frontend/src/routes/guest/register/steps/register.test.tsx index 8f8c1d83915c53faa1bb97aa5ea7607a06ec1c3b..d88a1312c7d4058017cabcf0916d9df093b5eb29 100644 --- a/frontend/src/routes/guest/register/steps/register.test.tsx +++ b/frontend/src/routes/guest/register/steps/register.test.tsx @@ -17,7 +17,6 @@ function getEmptyGuestData(): GuestInviteInformation { role_name_nb: '', role_start: '', role_end: '', - comment: '', email: '', mobile_phone: '', date_of_birth: null, @@ -25,6 +24,7 @@ function getEmptyGuestData(): GuestInviteInformation { passport: '', countryForCallingCode: '', authentication_method: AuthenticationMethod.Invite, + gender: '', } } diff --git a/frontend/src/routes/guest/register/steps/register.tsx b/frontend/src/routes/guest/register/steps/register.tsx index e958dc913ff7f11a1776ec2063c9d0b7db184eba..53b058e47722d502ada87b574fee2326a24b8809 100644 --- a/frontend/src/routes/guest/register/steps/register.tsx +++ b/frontend/src/routes/guest/register/steps/register.tsx @@ -27,7 +27,12 @@ import { import { getAlpha2Codes, getName } from 'i18n-iso-countries' import { DatePicker } from '@mui/lab' import { subYears } from 'date-fns/fp' -import { isValidFnr, isValidMobilePhoneNumber } from 'utils' +import { + isValidFnr, + isValidMobilePhoneNumber, + extractGenderOrBlank, + extractBirthdateFromNationalId, +} from 'utils' import { GuestInviteInformation } from '../guestDataForm' import { GuestRegisterData } from '../enteredGuestData' import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods' @@ -62,7 +67,11 @@ const GuestRegisterStep = forwardRef( const [passportNationality, setPassportNationality] = useState< string | undefined >(undefined) - const [gender, setGender] = useState<string>('') + + // Set suggestion for gender field is a gender is not already given in the input + const [gender, setGender] = useState<string>( + initialGuestData.gender ?? extractGenderOrBlank(initialGuestData.fnr) + ) const [idErrorState, setIdErrorState] = useState<string>('') const [phoneErrorState, setPhoneErrorState] = useState<string>('') const { displayContactAtUnitGuestInput } = useContext(FeatureContext) @@ -82,6 +91,19 @@ const GuestRegisterStep = forwardRef( defaultValues: registerData ?? {}, }) + // If there is no already a date of birth set, add a suggestion for + // this value based on the national ID, if it is set + if ( + (!registerData || !registerData.dateOfBirth) && + !initialGuestData.date_of_birth && + initialGuestData.fnr + ) { + const dateOfBirth = extractBirthdateFromNationalId(initialGuestData.fnr) + if (dateOfBirth) { + setValue('dateOfBirth', dateOfBirth) + } + } + const submit: SubmitHandler<GuestRegisterData> = async (data) => { console.log('submit data is', data) const result = await trigger() diff --git a/frontend/src/utils/index.test.ts b/frontend/src/utils/index.test.ts index 24f9d152577bd844e60994a58ca6400588d99cea..0c6a66c68bc18aecfa8544b4c0090b2e05405a70 100644 --- a/frontend/src/utils/index.test.ts +++ b/frontend/src/utils/index.test.ts @@ -1,3 +1,4 @@ +import parse from 'date-fns/parse' import { getCookie, deleteCookie, @@ -7,6 +8,8 @@ import { isValidMobilePhoneNumber, maybeCsrfToken, submitJsonOpts, + isFemaleBasedOnNationalId, + extractBirthdateFromNationalId, } from './index' // Mock i18next module to return a translation that just returns the key @@ -117,3 +120,31 @@ test('Null fnr', async () => { test('Invalid fnr', async () => { expect(isValidFnr('')).toEqual('common:validation.invalidIdNumber') }) + +test('Female extracted from fnr', async () => { + expect(isFemaleBasedOnNationalId('12103626631')).toEqual(true) + expect(isFemaleBasedOnNationalId('08015214474')).toEqual(true) + expect(isFemaleBasedOnNationalId('26052088029')).toEqual(true) + expect(isFemaleBasedOnNationalId('11082335449')).toEqual(true) + expect(isFemaleBasedOnNationalId('11081670619')).toEqual(true) +}) + +test('Male extracted from fnr', async () => { + expect(isFemaleBasedOnNationalId('12103626712')).toEqual(false) + expect(isFemaleBasedOnNationalId('08015214555')).toEqual(false) + expect(isFemaleBasedOnNationalId('01088538788')).toEqual(false) + expect(isFemaleBasedOnNationalId('15101739551')).toEqual(false) + expect(isFemaleBasedOnNationalId('05127648192')).toEqual(false) +}) + +test('Date of birth extract from D-number', async () => { + expect(extractBirthdateFromNationalId('53097248016')).toEqual( + parse('1972-09-13', 'yyyy-MM-dd', new Date()) + ) +}) + +test('Date of birth extracted from fødselsnummer', async () => { + expect(extractBirthdateFromNationalId('04062141242')).toEqual( + parse('1921-06-04', 'yyyy-MM-dd', new Date()) + ) +}) diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index f045820a16de62626651f877b5828f0730b800ac..b8fedc4536dc2bb79a26abfab7e188b8f60d0df2 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -1,5 +1,5 @@ import validator from '@navikt/fnrvalidator' -import { parseISO } from 'date-fns' +import { getYear, parseISO } from 'date-fns' import { OuData } from 'hooks/useOus' import i18n from 'i18next' import { @@ -11,6 +11,8 @@ import { FetchedConsent, } from 'interfaces' import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js' +import parse from 'date-fns/parse' +import { parseInt } from 'lodash' const validEmailRegex = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/ @@ -221,3 +223,90 @@ export function getOuName(ou: OuData) { } return ou.nb ? ou.nb : ou.en } + +/** + * Note the the input is assumed to be either a D-number or a "fødselsnummer". Other types such as H-numbers are not supported. + * + * @param nationalId D-number or "fødselsnummer" + */ +function isDnr(nationalId: string): boolean { + return parseInt(nationalId.substring(0, 1), 10) >= 4 +} + +export function isFemaleBasedOnNationalId(nationalId: string): boolean { + if (isDnr(nationalId)) { + return parseInt(nationalId.charAt(10), 10) % 2 === 0 + } + return parseInt(nationalId.charAt(8), 10) % 2 === 0 +} + +export function extractGenderOrBlank(nationalId?: string): string { + if ( + nationalId == null || + nationalId === '' || + isValidFnr(nationalId) !== true + ) { + return '' + } + + if (isFemaleBasedOnNationalId(nationalId)) { + return 'female' + } + return 'male' +} + +/** + * Gives a guess of the birthdate with century included. + * + * @param dateOfBirth a date on the form ddMMyy + */ +function suggestBirthDate(dateOfBirth: string): Date { + const currentYear = getYear(new Date()) + const year = dateOfBirth.substring(4, 6) + const yearAsInt = parseInt(year) + let century = '20' + + // Check that the year the person is born is not a year in the future, + // given he is born in the 21th century. Also assuming he is born in + // the 21th century, check that he is older than 15 years, if not + // then assume he is born in the 20th century + if ( + yearAsInt + 2000 > currentYear || + !(currentYear - 2000 - yearAsInt > 15) + ) { + century = '19' + } + return parse( + dateOfBirth.substring(0, 4) + century + dateOfBirth.substring(4, 6), + 'ddMMyyyy', + new Date() + ) +} + +function extractBirthdateFromFnr(nationalId: string): Date { + return suggestBirthDate(nationalId.substring(0, 6)) +} + +function extractBirthdateFromDnumber(nationalId: string): Date { + return suggestBirthDate( + (parseInt(nationalId.charAt(0), 10) - 4).toString(10) + + nationalId.substring(1, 6) + ) +} + +export function extractBirthdateFromNationalId( + nationalId?: string +): Date | null { + if ( + nationalId == null || + nationalId === '' || + isValidFnr(nationalId) !== true + ) { + return null + } + + if (isDnr(nationalId)) { + return extractBirthdateFromDnumber(nationalId) + } + return extractBirthdateFromFnr(nationalId) +}