Files
planflow/tests/unit/rup.test.ts
T
MichaelandClaude Opus 5 06198ddbce Recueillir à l'embauche ce qu'exige le contrat
Créer un salarié demandait cinq champs. Le contrat, la déclaration et le
registre en réclament une vingtaine, et rien ne permettait de les saisir :
le sexe, le nom de naissance, le pays et le département de naissance, la
situation de famille, les personnes à charge, le téléphone fixe, le
complément d'adresse et l'heure d'embauche n'existaient pas en base.

La migration les ajoute, toutes facultatives : un dossier incomplet doit
pouvoir exister — c'est au registre de signaler ce qui lui manque, pas à
la base de refuser l'embauche. Seul l'envoi des plannings par SMS fait
exception : c'est un consentement, donc faux par défaut, la charge de la
preuve pesant sur l'employeur.

Le pays et le département de naissance sont des colonnes à part et non une
commune saisie librement : la déclaration sociale les demande séparément,
et les rétro-extraire échouerait au premier « Bar-le-Duc (Meuse) ».

Le formulaire devient un panneau latéral : une trentaine de champs posés
au centre masquaient l'annuaire, et on embauche en regardant qui est déjà
là. Le matricule y est proposé à la suite du dernier, attribué dans la
transaction pour que deux embauches simultanées ne tombent pas sur le même
rang. Le nom de naissance vaut le nom de famille quand il n'en diffère
pas, plutôt que d'imposer une ressaisie à l'immense majorité des dossiers.

Le responsable hiérarchique et la politique RTT se posent enfin à
l'embauche : les deux modèles existaient sans qu'aucun écran ne les
alimente.

Le registre unique du personnel gagne sa colonne « sexe » et se tient
désormais au nom de naissance — le nom d'usage peut changer sans que la
personne change.

Migration écrite et validée contre le schéma, non appliquée : aucune base
n'était ouverte ici. À passer avec `pnpm db:migrate`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:28:28 +02:00

139 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
});
});