Skip to content
Snippets Groups Projects
Commit 3c0d3227 authored by Tore.Brede's avatar Tore.Brede
Browse files

GREG-171: Populating gender and birthdate with suggestions based on national ID

parent 9b1c7689
No related branches found
No related tags found
1 merge request!274GREG-171: Populating gender and birthdate with suggestions based on national ID
Pipeline #114483 failed
......@@ -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')
})
......@@ -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: '',
}
}
......
......@@ -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()
......
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())
)
})
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)
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment