CI / verify (pull_request) Canceled after 0s
Quatre champs se saisissaient en texte libre là où la valeur appartient à une liste : nationalité, pays de naissance, pays de l'adresse, département. « Frnace » y entrait aussi bien que « France », et le registre imprimait la faute. Les pays sont stockés par leur code ISO 3166-1 et nommés à l'affichage par `Intl.DisplayNames` : un nom dépend de la langue et des versions d'ICU, un code ne bouge pas, et deux cent cinquante traductions n'ont pas à être entretenues à la main. Les valeurs héritées — « France » en toutes lettres — sont reconnues à la lecture puis normalisées au premier enregistrement, plutôt que blanchies par un menu qui ne les retrouve pas. Les départements portent la Corse et l'outre-mer : « 2A » n'est pas un nombre, et le département de naissance sert à la déclaration sociale, qui attend le code. Les téléphones gagnent leur indicatif, choisi à part et recollé en un seul champ. L'enregistrer séparément obligerait chaque lecture — annuaire, export, SMS — à le recomposer, et un numéro à moitié lu ne joint personne. La découpe prend l'indicatif le plus long : « +352 » commence par « +3 ». Le titre de séjour se déclare enfin. `WorkPermit` existait depuis le début sans qu'aucun écran ne l'alimente. L'échéance est exigée dès qu'un titre est posé — employer quelqu'un dont l'autorisation a expiré est un délit, et sans date rien ne peut le signaler à temps. Décocher retire le titre plutôt que de le laisser alerter pour quelqu'un qui n'en relève plus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
190 lines
6.3 KiB
TypeScript
190 lines
6.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
RUP_COLUMNS,
|
||
missingMentions,
|
||
peopleWithGaps,
|
||
type RupPerson,
|
||
} from '@/domain/legal/rup';
|
||
|
||
/**
|
||
* Registre unique du personnel — mentions obligatoires.
|
||
*
|
||
* La sanction se compte **par salarié concerné**, pas par registre : c'est ce
|
||
* que le décompte doit refléter, et c'est ce qui décide de l'avertissement
|
||
* affiché avant l'édition.
|
||
*/
|
||
|
||
const complete: RupPerson = {
|
||
lastName: 'Ferrand',
|
||
firstName: 'Camille',
|
||
sex: 'F',
|
||
nationality: 'France',
|
||
birthDate: new Date('1990-04-12T00:00:00Z'),
|
||
jobTitle: 'Hôte de caisse',
|
||
qualification: 'Niveau 3',
|
||
contractLabel: 'CDI',
|
||
entryDate: new Date('2024-09-01T00:00:00Z'),
|
||
exitDate: null,
|
||
};
|
||
|
||
describe('registre unique du personnel', () => {
|
||
it('énumère les mentions fixées par le texte', () => {
|
||
expect(RUP_COLUMNS.map((column) => column.key)).toEqual([
|
||
'lastName',
|
||
'firstName',
|
||
'sex',
|
||
'nationality',
|
||
'birthDate',
|
||
'jobTitle',
|
||
'qualification',
|
||
'contractLabel',
|
||
'entryDate',
|
||
'exitDate',
|
||
]);
|
||
});
|
||
|
||
it('répartit la largeur sur la totalité de la page', () => {
|
||
const total = RUP_COLUMNS.reduce((sum, column) => sum + column.width, 0);
|
||
expect(total).toBeCloseTo(1, 5);
|
||
});
|
||
|
||
it('ne réclame rien quand toutes les mentions sont là', () => {
|
||
expect(missingMentions(complete)).toEqual([]);
|
||
});
|
||
|
||
it('tient une date de sortie absente pour une présence, pas pour un oubli', () => {
|
||
// Un salarié en poste n'a pas de date de sortie : la réclamer ferait
|
||
// apparaître tout l'effectif courant comme incomplet.
|
||
expect(missingMentions({ ...complete, exitDate: null })).toEqual([]);
|
||
});
|
||
|
||
it('nomme les mentions manquantes', () => {
|
||
expect(
|
||
missingMentions({ ...complete, nationality: null, qualification: '' }),
|
||
).toEqual(['Nationalité', 'Qualification']);
|
||
});
|
||
|
||
it('compte les personnes concernées, pas les champs vides', () => {
|
||
const bancal: RupPerson = {
|
||
...complete,
|
||
sex: null,
|
||
nationality: null,
|
||
birthDate: null,
|
||
};
|
||
// Trois manques sur une seule personne : une contravention, pas trois.
|
||
expect(peopleWithGaps([complete, bancal])).toBe(1);
|
||
});
|
||
});
|
||
|
||
describe('rendu du registre', () => {
|
||
it('produit un PDF paginé qui porte les manques en pied', async () => {
|
||
const { renderRegisterPdf } = await import('@/server/employees/rup-pdf');
|
||
|
||
// Assez de lignes pour dépasser une page : c'est la rupture que le rendu
|
||
// rate le plus volontiers.
|
||
const people: RupPerson[] = Array.from({ length: 60 }, (_, index) => ({
|
||
...complete,
|
||
lastName: `Nom manifestement trop long pour la colonne ${index}`,
|
||
sex: index % 2 === 0 ? null : 'F',
|
||
}));
|
||
|
||
const pdf = await renderRegisterPdf({
|
||
locationId: 'loc',
|
||
locationName: 'La Foir’Fouille',
|
||
siret: '12345678901234',
|
||
people,
|
||
incomplete: 30,
|
||
});
|
||
|
||
expect(Buffer.from(pdf).subarray(0, 5).toString()).toBe('%PDF-');
|
||
expect(pdf.length).toBeGreaterThan(1000);
|
||
});
|
||
|
||
it('édite un registre vide plutôt que de refuser', async () => {
|
||
const { renderRegisterPdf } = await import('@/server/employees/rup-pdf');
|
||
|
||
// Un établissement qui vient d'ouvrir n'a personne : le registre existe
|
||
// quand même, et son absence serait plus embarrassante que sa vacuité.
|
||
const pdf = await renderRegisterPdf({
|
||
locationId: 'loc',
|
||
locationName: 'Nouvel établissement',
|
||
siret: null,
|
||
people: [],
|
||
incomplete: 0,
|
||
});
|
||
|
||
expect(Buffer.from(pdf).subarray(0, 5).toString()).toBe('%PDF-');
|
||
});
|
||
});
|
||
|
||
describe('matricule proposé', () => {
|
||
it('part à E0001 sur un effectif vide', async () => {
|
||
const { nextEmployeeNumber } = await import('@/domain/hr/civil-status');
|
||
expect(nextEmployeeNumber([])).toBe('E0001');
|
||
});
|
||
|
||
it('suit le dernier rang attribué', async () => {
|
||
const { nextEmployeeNumber } = await import('@/domain/hr/civil-status');
|
||
expect(nextEmployeeNumber(['E0001', 'E0007', 'E0003'])).toBe('E0008');
|
||
});
|
||
|
||
it('ignore les matricules repris d’un autre outil', async () => {
|
||
const { nextEmployeeNumber } = await import('@/domain/hr/civil-status');
|
||
// Un client qui migre apporte ses propres formes : les compter fausserait
|
||
// le rang, et refuser l'embauche pour cette raison serait absurde.
|
||
expect(nextEmployeeNumber(['SILAE-42', '00012', 'E0002'])).toBe('E0003');
|
||
});
|
||
});
|
||
|
||
describe('référentiels géographiques', () => {
|
||
it('nomme les pays en français depuis leur code', async () => {
|
||
const { countryLabel } = await import('@/domain/hr/geo');
|
||
expect(countryLabel('FR')).toBe('France');
|
||
expect(countryLabel('DE')).toBe('Allemagne');
|
||
});
|
||
|
||
it('laisse passer une valeur héritée plutôt que de la blanchir', async () => {
|
||
const { countryLabel } = await import('@/domain/hr/geo');
|
||
// Les dossiers saisis avant le référentiel portent le nom en clair.
|
||
expect(countryLabel('Nouvelle-Calédonie')).toBe('Nouvelle-Calédonie');
|
||
});
|
||
|
||
it('ramène un nom hérité à son code', async () => {
|
||
const { toCountryCode } = await import('@/domain/hr/geo');
|
||
expect(toCountryCode('France')).toBe('FR');
|
||
expect(toCountryCode('FR')).toBe('FR');
|
||
expect(toCountryCode('Pays imaginaire')).toBe('');
|
||
expect(toCountryCode(null)).toBe('');
|
||
});
|
||
|
||
it('porte la Corse et l’outre-mer', async () => {
|
||
const { departmentLabel, isKnownDepartment } = await import('@/domain/hr/geo');
|
||
expect(departmentLabel('2A')).toBe('2A - Corse-du-Sud');
|
||
expect(departmentLabel('974')).toBe('974 - La Réunion');
|
||
expect(isKnownDepartment('20')).toBe(false);
|
||
});
|
||
|
||
it('coupe un numéro sur l’indicatif le plus long', async () => {
|
||
const { splitDial } = await import('@/domain/hr/geo');
|
||
// « +352 » commence par « +3 » : tester dans l'ordre de la table
|
||
// donnerait la France pour un numéro luxembourgeois.
|
||
expect(splitDial('+352 621 12 34 56')).toEqual({
|
||
dial: '+352',
|
||
rest: '621 12 34 56',
|
||
});
|
||
expect(splitDial('+33 6 12 34 56 78')).toEqual({
|
||
dial: '+33',
|
||
rest: '6 12 34 56 78',
|
||
});
|
||
});
|
||
|
||
it('retombe sur la France pour un numéro sans indicatif', async () => {
|
||
const { splitDial } = await import('@/domain/hr/geo');
|
||
expect(splitDial('06 12 34 56 78')).toEqual({
|
||
dial: '+33',
|
||
rest: '06 12 34 56 78',
|
||
});
|
||
});
|
||
});
|