- Backend: DTO NIR 15 car 2A/2B, validation format+clé, warning cohérence - BDD: nir_chiffre NOT NULL, migration pour bases existantes - Seeds: 02 nir_chiffre, 03 Marie 2A / Fatima 99 - Frontend: nir_utils, nir_text_field, formulaire pro, mock inscription AM Made-with: Cursor
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
/**
|
|
* Utilitaire de validation du NIR (numéro de sécurité sociale français).
|
|
* - Format 15 caractères (chiffres ou 2A/2B pour la Corse).
|
|
* - Clé de contrôle : 97 - (NIR13 mod 97). Pour 2A/2B, conversion temporaire (INSEE : 2A→19, 2B→20).
|
|
* - En cas d'incohérence avec les données (sexe, date, lieu) : warning uniquement, pas de rejet.
|
|
*/
|
|
|
|
const NIR_CORSE_2A = '19';
|
|
const NIR_CORSE_2B = '20';
|
|
|
|
/** Regex 15 caractères : sexe (1-3) + 4 chiffres + (2A|2B|2 chiffres) + 6 chiffres + 2 chiffres clé */
|
|
const NIR_FORMAT = /^[1-3]\d{4}(?:2A|2B|\d{2})\d{6}\d{2}$/i;
|
|
|
|
/**
|
|
* Convertit le NIR en chaîne de 13 chiffres pour le calcul de la clé (2A→19, 2B→20).
|
|
*/
|
|
export function nirTo13Digits(nir: string): string {
|
|
const n = nir.toUpperCase().replace(/\s/g, '');
|
|
if (n.length !== 15) return '';
|
|
const dept = n.slice(5, 7);
|
|
let deptNum: string;
|
|
if (dept === '2A') deptNum = NIR_CORSE_2A;
|
|
else if (dept === '2B') deptNum = NIR_CORSE_2B;
|
|
else deptNum = dept;
|
|
return n.slice(0, 5) + deptNum + n.slice(7, 13);
|
|
}
|
|
|
|
/**
|
|
* Vérifie que le format NIR est valide (15 caractères, 2A/2B acceptés).
|
|
*/
|
|
export function isNirFormatValid(nir: string): boolean {
|
|
if (!nir || typeof nir !== 'string') return false;
|
|
const n = nir.replace(/\s/g, '').toUpperCase();
|
|
return NIR_FORMAT.test(n);
|
|
}
|
|
|
|
/**
|
|
* Calcule la clé de contrôle attendue (97 - (NIR13 mod 97)).
|
|
* Retourne un nombre entre 1 et 97.
|
|
*/
|
|
export function computeNirKey(nir13: string): number {
|
|
const num = parseInt(nir13, 10);
|
|
if (Number.isNaN(num) || nir13.length !== 13) return -1;
|
|
return 97 - (num % 97);
|
|
}
|
|
|
|
/**
|
|
* Vérifie la clé de contrôle du NIR (15 caractères).
|
|
* Retourne true si le NIR est valide (format + clé).
|
|
*/
|
|
export function isNirKeyValid(nir: string): boolean {
|
|
const n = nir.replace(/\s/g, '').toUpperCase();
|
|
if (n.length !== 15) return false;
|
|
const nir13 = nirTo13Digits(n);
|
|
if (nir13.length !== 13) return false;
|
|
const expectedKey = computeNirKey(nir13);
|
|
const actualKey = parseInt(n.slice(13, 15), 10);
|
|
return expectedKey === actualKey;
|
|
}
|
|
|
|
export interface NirValidationResult {
|
|
valid: boolean;
|
|
error?: string;
|
|
warning?: string;
|
|
}
|
|
|
|
/**
|
|
* Valide le NIR (format + clé). En cas d'incohérence avec date de naissance ou sexe, ajoute un warning sans invalider.
|
|
*/
|
|
export function validateNir(
|
|
nir: string,
|
|
options?: { dateNaissance?: string; genre?: 'H' | 'F' },
|
|
): NirValidationResult {
|
|
const n = (nir || '').replace(/\s/g, '').toUpperCase();
|
|
if (n.length === 0) return { valid: false, error: 'Le NIR est requis' };
|
|
if (!isNirFormatValid(n)) {
|
|
return { valid: false, error: 'Le NIR doit contenir 15 caractères (chiffres, ou 2A/2B pour la Corse)' };
|
|
}
|
|
if (!isNirKeyValid(n)) {
|
|
return { valid: false, error: 'Clé de contrôle du NIR invalide' };
|
|
}
|
|
let warning: string | undefined;
|
|
if (options?.genre) {
|
|
const sexNir = n[0];
|
|
const expectedSex = options.genre === 'F' ? '2' : '1';
|
|
if (sexNir !== expectedSex) {
|
|
warning = 'Le NIR ne correspond pas au genre indiqué (position 1 du NIR).';
|
|
}
|
|
}
|
|
if (options?.dateNaissance) {
|
|
try {
|
|
const d = new Date(options.dateNaissance);
|
|
if (!Number.isNaN(d.getTime())) {
|
|
const year2 = d.getFullYear() % 100;
|
|
const month = d.getMonth() + 1;
|
|
const nirYear = parseInt(n.slice(1, 3), 10);
|
|
const nirMonth = parseInt(n.slice(3, 5), 10);
|
|
if (nirYear !== year2 || nirMonth !== month) {
|
|
warning = warning
|
|
? `${warning} Le NIR ne correspond pas à la date de naissance (positions 2-5).`
|
|
: 'Le NIR ne correspond pas à la date de naissance indiquée (positions 2-5).';
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
return { valid: true, warning };
|
|
}
|