Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions locales/en/audits.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions locales/fr/audits.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
};
Original file line number Diff line number Diff line change
@@ -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();
}
});
});
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ test('Expected default', () => {
drivingLicenseAge: 16,
workingAge: 15,
schoolMandatoryAge: 15,
maxPersonAge: 125,
addAuditWarningVeryOldAge: undefined,
logDatabaseUpdates: false,
startDateTimeWithTimezoneOffset: undefined,
endDateTimeWithTimezoneOffset: undefined,
Expand Down
12 changes: 12 additions & 0 deletions packages/evolution-common/src/config/project.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -245,6 +255,8 @@ const defaultConfig = {
drivingLicenseAge: 16,
workingAge: 15,
schoolMandatoryAge: 15,
maxPersonAge: 125,
addAuditWarningVeryOldAge: undefined,
surveySupportForm: false,
mapDefaultCenter: {
lat: 45.5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand Down Expand Up @@ -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.'
Expand Down
Loading