Files
planflow/src/domain/payroll/elements.ts
T
Claude af9429901d WP-08 : export Silae, format relevé sur un export réel du dossier
Le format n'est plus déduit d'une documentation. Il est vérifié octet par
octet sur un export du dossier (juillet 2026, 55 lignes) : un contrôle
d'aller-retour a reproduit ce fichier **sans aucune ligne divergente**.

Ce que le fichier réel a corrigé dans la spécification
- L'export est en **ASCII pur**, pas en UTF-8 : aucun accent, y compris dans
  les libellés (« Heures travaillees », « Date debut »). Le sérialiseur
  translittère, pour qu'un salarié nommé « Rémi » n'introduise pas le premier
  octet non-ASCII du fichier.
- Fins de ligne **CRLF**, la dernière comprise ; ni guillemet ni point-virgule
  final ; décimale point.
- Heures avec au moins une décimale et au plus deux — `96.0`, `52.5`, `69.67` ;
  jours en entier nu — `14`, `22`. L'arrondi se fait **par ligne**, au
  centième : recomposer un total depuis les lignes peut donc s'écarter de
  quelques centièmes. C'est le comportement de l'export existant, et le
  reproduire est délibéré.
- Un salarié sous contrat sans aucun créneau planifié figure quand même, avec
  sa durée contractuelle entière en heures manquantes.

Ce que le fichier n'a pas dit
Les codes sont maintenant connus — `AB-100`, `AB-200`, `AB-300`, `AB-630`,
`EV-HDimanche`, `EV-HFerie`, `HS-HS25` — mais **savoir qu'un code existe ne dit
pas ce qu'il désigne**. `EV-HDimanche` se lit ; `AB-300` non. Les premiers sont
proposés, les seconds restent vides, et rien n'est confirmé d'office : l'export
refuse de tourner tant que le gestionnaire de paie n'a pas validé chaque
correspondance. C'est le signal d'arrêt de PLAN.md §8.2, maintenu.

Refus plutôt que fichier partiel
Un CSV incomplet se charge sans erreur dans Silae et rend la paie fausse pour
les salariés qui en sont absents — l'échec est silencieux jusqu'au bulletin.
L'export liste donc les manques et ne produit rien.

Données personnelles
L'export de référence porte les heures et les absences de salariés
identifiables. Il **n'est pas versionné**, ni comme fixture ni comme donnée de
démonstration. Ce sont les règles de forme qui sont figées dans les tests, avec
les valeurs observées mais sans les matricules ni les volumes réels. Le fichier
produit revient dans la réponse de l'action plutôt que par une URL : un fichier
de paie ne doit pas rester adressable, mis en cache ou présent dans un
historique de navigation. En base, seule l'empreinte est conservée — elle
suffit à prouver qu'un réexport est identique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-08 07:58:34 +00:00

128 lines
3.6 KiB
TypeScript

/**
* Éléments de paie calculés par PlanFlow — PLAN.md §8.2.
*
* Ces clés sont **stables et internes**. Elles ne sont pas des codes Silae :
* la correspondance vit en base, parce que les codes appartiennent au dossier
* du client et que deux clients du même cabinet n'ont pas les mêmes.
*
* La distinction n'est pas théorique. Écrire `EV-HDimanche` dans le calcul
* rendrait l'outil inutilisable pour le deuxième client, et impossible à
* corriger sans livraison le jour où le cabinet renumérote ses rubriques.
*/
export const PAYROLL_ELEMENTS = {
WORKED_DAYS: 'WORKED_DAYS',
WORKED_HOURS: 'WORKED_HOURS',
MISSING_HOURS: 'MISSING_HOURS',
SUNDAY_HOURS: 'SUNDAY_HOURS',
HOLIDAY_HOURS: 'HOLIDAY_HOURS',
OVERTIME_25: 'OVERTIME_25',
OVERTIME_50: 'OVERTIME_50',
COMPLEMENTARY_10: 'COMPLEMENTARY_10',
COMPLEMENTARY_25: 'COMPLEMENTARY_25',
FORFAIT_DAYS: 'FORFAIT_DAYS',
} as const;
export type PayrollElementKey =
(typeof PAYROLL_ELEMENTS)[keyof typeof PAYROLL_ELEMENTS];
export interface PayrollElementDefinition {
key: PayrollElementKey;
label: string;
/** Nature de la valeur, qui décide de la mise en forme du CSV. */
unit: 'HOURS' | 'DAYS';
/** Famille Silae attendue, qui oriente le choix du code dans l'écran. */
kind: 'SERVICE' | 'OVERTIME' | 'VARIABLE' | 'ABSENCE';
/**
* Code proposé par défaut lorsqu'il se **lit** dans le libellé du code
* observé — `EV-HDimanche` désigne des heures du dimanche sans ambiguïté.
* `null` quand le code est opaque : `AB-300` ne dit pas quelle absence il
* désigne, et le deviner serait une faute.
*/
suggestedCode: string | null;
}
export const PAYROLL_ELEMENT_DEFINITIONS: PayrollElementDefinition[] = [
{
key: 'WORKED_DAYS',
label: 'Jours travaillés',
unit: 'DAYS',
kind: 'SERVICE',
suggestedCode: 'Nombre total de jours travailles',
},
{
key: 'WORKED_HOURS',
label: 'Heures travaillées',
unit: 'HOURS',
kind: 'SERVICE',
suggestedCode: 'Heures travaillees',
},
{
key: 'MISSING_HOURS',
label: 'Heures manquantes au contrat',
unit: 'HOURS',
kind: 'SERVICE',
suggestedCode: 'Heures manquantes au contrat',
},
{
key: 'SUNDAY_HOURS',
label: 'Heures du dimanche',
unit: 'HOURS',
kind: 'VARIABLE',
suggestedCode: 'EV-HDimanche',
},
{
key: 'HOLIDAY_HOURS',
label: 'Heures de jour férié',
unit: 'HOURS',
kind: 'VARIABLE',
suggestedCode: 'EV-HFerie',
},
{
key: 'OVERTIME_25',
label: 'Heures supplémentaires à 25 %',
unit: 'HOURS',
kind: 'OVERTIME',
suggestedCode: 'HS-HS25',
},
{
key: 'OVERTIME_50',
label: 'Heures supplémentaires à 50 %',
unit: 'HOURS',
kind: 'OVERTIME',
// Aucune ligne à 50 % dans l'export de référence : le code existe
// probablement, il n'a simplement pas été observé.
suggestedCode: null,
},
{
key: 'COMPLEMENTARY_10',
label: 'Heures complémentaires à 10 %',
unit: 'HOURS',
kind: 'OVERTIME',
suggestedCode: null,
},
{
key: 'COMPLEMENTARY_25',
label: 'Heures complémentaires à 25 %',
unit: 'HOURS',
kind: 'OVERTIME',
suggestedCode: null,
},
{
key: 'FORFAIT_DAYS',
label: 'Jours de forfait',
unit: 'DAYS',
kind: 'SERVICE',
// À obtenir du dossier : aucun salarié au forfait dans l'export observé.
suggestedCode: null,
},
];
export function elementDefinition(
key: PayrollElementKey,
): PayrollElementDefinition {
const found = PAYROLL_ELEMENT_DEFINITIONS.find((entry) => entry.key === key);
if (!found) throw new Error(`Élément de paie inconnu : ${key}`);
return found;
}