diff --git a/locales/en/audits.json b/locales/en/audits.json index 303d9aa8a..90bf2dddf 100644 --- a/locales/en/audits.json +++ b/locales/en/audits.json @@ -31,6 +31,8 @@ "P_M_Age": "Person age is missing", "P_L_InvalidJourneySequences": "At least one journey sequence is invalid or duplicated", "P_W_JourneySequenceGaps": "Journey sequences are not contiguous", + "P_I_AgeTooHigh": "Person age is too high", + "P_W_VeryOldAge": "Person is very old, please validate", "J_M_StartDate": "Journey start date is missing", "J_L_InvalidVisitedPlaceSequences": "At least one visited place sequence is invalid or duplicated", diff --git a/locales/fr/audits.json b/locales/fr/audits.json index 3934ecac9..4877ff7a2 100644 --- a/locales/fr/audits.json +++ b/locales/fr/audits.json @@ -31,6 +31,8 @@ "P_M_Age": "L'âge de la personne est manquant", "P_L_InvalidJourneySequences": "Le numéro de séquence d'au moins un séjour est invalide ou en double", "P_W_JourneySequenceGaps": "Les numéros de séquence des séjours ne sont pas consécutifs", + "P_I_AgeTooHigh": "L'âge de la personne est trop élevé", + "P_W_VeryOldAge": "L'âge de la personne est très élevé, veuillez valider", "J_M_StartDate": "La date de début du séjour est manquante", "J_L_InvalidVisitedPlaceSequences": "Le numéro de séquence d'au moins un lieu visité est invalide ou en double", diff --git a/packages/evolution-backend/src/services/audits/auditChecks/checks/PersonAuditChecks.ts b/packages/evolution-backend/src/services/audits/auditChecks/checks/PersonAuditChecks.ts index c789abea5..9e9c24924 100644 --- a/packages/evolution-backend/src/services/audits/auditChecks/checks/PersonAuditChecks.ts +++ b/packages/evolution-backend/src/services/audits/auditChecks/checks/PersonAuditChecks.ts @@ -1,10 +1,11 @@ /* - * Copyright 2025, Polytechnique Montreal and contributors + * Copyright Polytechnique Montreal and contributors * * This file is licensed under the MIT License. * License text available at https://opensource.org/licenses/MIT */ +import projectConfig from 'evolution-common/lib/config/project.config'; import type { AuditForObject } from 'evolution-common/lib/services/audits/types'; import type { PersonAuditCheckContext, PersonAuditCheckFunction } from '../AuditCheckContexts'; import { @@ -61,6 +62,30 @@ export const personAuditChecks: { [errorCode: string]: PersonAuditCheckFunction return undefined; // No audit needed }, + /** + * Check if person age exceeds the configured maximum + * @param context - PersonAuditCheckContext + * @returns AuditForObject + */ + P_I_AgeTooHigh: (context: PersonAuditCheckContext): AuditForObject | undefined => { + const { person } = context; + const age = person.age; + + if (typeof age === 'number' && age > projectConfig.maxPersonAge) { + return { + objectType: 'person', + objectUuid: person._uuid!, + errorCode: 'P_I_AgeTooHigh', + version: 1, + level: 'error', + message: 'Person age is too high', + ignore: false + }; + } + + return undefined; // No audit needed + }, + /** * Check for holes in the journey sequences (e.g. 1,2,3,5,6). The questionnaire keeps * them contiguous when a journey is added or deleted, so a hole points at a survey bug @@ -83,6 +108,37 @@ export const personAuditChecks: { [errorCode: string]: PersonAuditCheckFunction }; } + return undefined; // No audit needed + }, + + /** + * Warning when person age is at or above `addAuditWarningVeryOldAge` but still within + * `maxPersonAge` (see project config). Intended for reviewer verification. + * @param context - PersonAuditCheckContext + * @returns AuditForObject + */ + P_W_VeryOldAge: (context: PersonAuditCheckContext): AuditForObject | undefined => { + const { person } = context; + const age = person.age; + const warningAge = projectConfig.addAuditWarningVeryOldAge; + + if ( + typeof age === 'number' && + warningAge !== undefined && + age >= warningAge && + age <= projectConfig.maxPersonAge + ) { + return { + objectType: 'person', + objectUuid: person._uuid!, + errorCode: 'P_W_VeryOldAge', + version: 1, + level: 'warning', + message: 'Person is very old, please validate', + ignore: false + }; + } + return undefined; // No audit needed } }; diff --git a/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_I_AgeTooHigh.test.ts b/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_I_AgeTooHigh.test.ts new file mode 100644 index 000000000..6698aeee2 --- /dev/null +++ b/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_I_AgeTooHigh.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright Polytechnique Montreal and contributors + * + * This file is licensed under the MIT License. + * License text available at https://opensource.org/licenses/MIT + */ + +import { v4 as uuidV4 } from 'uuid'; +import projectConfig from 'evolution-common/lib/config/project.config'; +import { personAuditChecks } from '../../PersonAuditChecks'; +import { createContextWithPerson } from './testHelper'; + +describe('P_I_AgeTooHigh audit check', () => { + const validUuid = uuidV4(); + + // [title, age, shouldError] + const cases: [string, number | undefined | null, boolean][] = [ + ['age at the maximum does not error', projectConfig.maxPersonAge, false], + ['age above the maximum errors', projectConfig.maxPersonAge + 1, true], + ['typical age does not error', 30, false], + ['undefined age does not error', undefined, false], + ['null age does not error', null, false] + ]; + + it.each(cases)('%s', (_title, age, shouldError) => { + const context = createContextWithPerson({ age: age as number | undefined }, validUuid); + + const result = personAuditChecks.P_I_AgeTooHigh(context); + + if (shouldError) { + expect(result).toMatchObject({ + objectType: 'person', + objectUuid: validUuid, + errorCode: 'P_I_AgeTooHigh', + version: 1, + level: 'error', + message: 'Person age is too high', + ignore: false + }); + } else { + expect(result).toBeUndefined(); + } + }); +}); diff --git a/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_W_VeryOldAge.test.ts b/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_W_VeryOldAge.test.ts new file mode 100644 index 000000000..16ee45a4f --- /dev/null +++ b/packages/evolution-backend/src/services/audits/auditChecks/checks/__tests__/person/P_W_VeryOldAge.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright Polytechnique Montreal and contributors + * + * This file is licensed under the MIT License. + * License text available at https://opensource.org/licenses/MIT + */ + +import { v4 as uuidV4 } from 'uuid'; +import projectConfig from 'evolution-common/lib/config/project.config'; +import { personAuditChecks } from '../../PersonAuditChecks'; +import { createContextWithPerson } from './testHelper'; + +describe('P_W_VeryOldAge audit check', () => { + const validUuid = uuidV4(); + const warningAge = 100; + const originalWarningAge = projectConfig.addAuditWarningVeryOldAge; + + beforeEach(() => { + projectConfig.addAuditWarningVeryOldAge = warningAge; + }); + + afterEach(() => { + projectConfig.addAuditWarningVeryOldAge = originalWarningAge; + }); + + // [title, age, shouldWarn] + const cases: [string, number | undefined | null, boolean][] = [ + ['age below the warning threshold does not warn', warningAge - 1, false], + ['age at the warning threshold warns', warningAge, true], + ['age at the maximum warns', projectConfig.maxPersonAge, true], + ['age above the maximum does not warn', projectConfig.maxPersonAge + 1, false], + ['typical age does not warn', 30, false], + ['undefined age does not warn', undefined, false], + ['null age does not warn', null, false] + ]; + + it.each(cases)('%s', (_title, age, shouldWarn) => { + const context = createContextWithPerson({ age: age as number | undefined }, validUuid); + + const result = personAuditChecks.P_W_VeryOldAge(context); + + if (shouldWarn) { + expect(result).toMatchObject({ + objectType: 'person', + objectUuid: validUuid, + errorCode: 'P_W_VeryOldAge', + version: 1, + level: 'warning', + message: 'Person is very old, please validate', + ignore: false + }); + } else { + expect(result).toBeUndefined(); + } + }); + + it('does not warn when addAuditWarningVeryOldAge is not configured', () => { + projectConfig.addAuditWarningVeryOldAge = undefined; + + const context = createContextWithPerson({ age: warningAge }, validUuid); + + expect(personAuditChecks.P_W_VeryOldAge(context)).toBeUndefined(); + }); +}); diff --git a/packages/evolution-common/src/config/__tests__/project.config.test.ts b/packages/evolution-common/src/config/__tests__/project.config.test.ts index 1e971ec70..093622898 100644 --- a/packages/evolution-common/src/config/__tests__/project.config.test.ts +++ b/packages/evolution-common/src/config/__tests__/project.config.test.ts @@ -15,6 +15,8 @@ test('Expected default', () => { drivingLicenseAge: 16, workingAge: 15, schoolMandatoryAge: 15, + maxPersonAge: 125, + addAuditWarningVeryOldAge: undefined, logDatabaseUpdates: false, startDateTimeWithTimezoneOffset: undefined, endDateTimeWithTimezoneOffset: undefined, diff --git a/packages/evolution-common/src/config/project.config.ts b/packages/evolution-common/src/config/project.config.ts index 3f345f020..f65c354ca 100644 --- a/packages/evolution-common/src/config/project.config.ts +++ b/packages/evolution-common/src/config/project.config.ts @@ -83,6 +83,16 @@ export type EvolutionProjectConfiguration = { * Defaults to 15. */ schoolMandatoryAge: number; + /** + * Maximum plausible person age accepted by audits and age widgets. + * Defaults to 125. + */ + maxPersonAge: number; + /** + * Age from which a person age triggers a reviewer warning audit (inclusive). + * Applies up to {@link maxPersonAge}. When undefined, no age warning audit is raised. + */ + addAuditWarningVeryOldAge?: number; /** * Whether to show the support form on all pages of the participant app. If * set to `true`, a button will be displayed in the bottom right corner of @@ -245,6 +255,8 @@ const defaultConfig = { drivingLicenseAge: 16, workingAge: 15, schoolMandatoryAge: 15, + maxPersonAge: 125, + addAuditWarningVeryOldAge: undefined, surveySupportForm: false, mapDefaultCenter: { lat: 45.5, diff --git a/packages/evolution-common/src/services/widgets/validations/validations.ts b/packages/evolution-common/src/services/widgets/validations/validations.ts index 721b29d8a..12c6c15f1 100644 --- a/packages/evolution-common/src/services/widgets/validations/validations.ts +++ b/packages/evolution-common/src/services/widgets/validations/validations.ts @@ -220,7 +220,7 @@ export const bicycleNumberValidation: ValidationFunction = (value, _customValue, /** * Verify if the value is a valid age. * - * The age must be an integer between 0 and 115. + * The age must be an integer between 0 and {@link projectConfig.maxPersonAge}. * * @see {@link ValidationFunction} */ @@ -248,7 +248,7 @@ export const ageValidation: ValidationFunction = (value) => { } }, { - validation: Number(value) > 115, + validation: Number(value) > projectConfig.maxPersonAge, errorMessage: { fr: 'L\'âge est trop élevé, veuillez vérifier.', en: 'Age is too high, please validate.'