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>
139 lines
4.4 KiB
TypeScript
139 lines
4.4 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');
|
||
});
|
||
});
|