Commit Graph
4 Commits
Author SHA1 Message Date
MichaelandClaude Opus 5 98b289784a Séparer les absences par état
Le calendrier portait tout : les demandes en attente, les décisions rendues et
celles qui avaient expiré, distinguées par un simple badge. Une file d'attente
se lit mal dans un calendrier — elle se lit par ancienneté, et le calendrier
range par date d'absence.

Quatre routes désormais : le calendrier, la file à traiter, les traitées et les
expirées. Le calendrier a déménagé sous /absences plutôt que d'être dupliqué, et
/conges redirige en conservant le mois : un lien partagé dans un message
continue d'ouvrir la bonne page.

Les trois files partagent une requête et un rendu. C'est délibéré : la règle qui
masque le motif d'une absence Sécurité sociale vaut pour les trois, et trois
copies de cette règle garantiraient qu'une finisse par diverger. Une donnée de
santé qui ne fuit que sur l'écran « traitées » reste une fuite.

La file à traiter classe par ancienneté de demande, les files closes par date
d'absence décroissante : on cherche d'un côté ce qui attend depuis trop
longtemps, de l'autre ce qui vient d'être décidé.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:23:16 +02:00
Claude 92a0ebf853 Configurer les rôles et leurs capacités
Critère d'acceptation de WP-01 resté sans écran : « un rôle personnalisé créé
par un client modifie effectivement l'accès, sans changement de code ». Le
catalogue de capacités existait, l'autorisation s'y adossait déjà, mais seul le
semis pouvait attribuer quoi que ce soit.

Deux dangers opposés, et il faut se protéger des deux. L'escalade d'abord : on
ne peut accorder qu'une capacité qu'on détient soi-même, sans quoi la première
personne autorisée à éditer un rôle s'accorde l'accès aux rémunérations en
trois clics et le catalogue ne sert plus à rien. Le retrait, lui, reste permis
même sur une capacité qu'on n'a pas — réduire un droit n'a jamais élargi le
sien, et l'interdire empêcherait de corriger un rôle trop large.

Le verrouillage ensuite : au moins un rôle doit conserver la gestion des
droits. La condition se juge sur l'ensemble des rôles après coup, non sur celui
qu'on édite — ce qui compte n'est pas que ce rôle-ci garde la capacité, mais
qu'un rôle la garde. Sans cela une organisation se fermerait dehors, et le seul
recours serait une intervention en base.

Un rôle naît sans aucune capacité : en hériter de celles de son créateur
distribuerait des droits que personne n'a demandés. Le niveau propriétaire se
délègue à part. Un rôle système ne se supprime pas, un rôle attribué non plus —
ses membres se retrouveraient dehors sans que personne l'ait décidé pour eux.

Le test va jusqu'au bout : créer le rôle, l'attribuer, se connecter, constater
qu'un écran s'ouvre et qu'un autre reste fermé. S'arrêter à « la case est
cochée » n'aurait rien dit du critère.

Trois défauts d'isolation des tests corrigés au passage, tous du même genre —
un état qui s'accumule d'une exécution à l'autre :

- la file des congés désignait « la première ligne de ce salarié » pour son
  nettoyage, et tombait sur une ligne laissée par un passage antérieur ;
- l'écran de conservation tronquait à quarante pièces triées par ancienneté,
  faisant disparaître les pièces échues derrière celles dont il n'y a rien à
  dire — corrigé dans le produit, pas seulement dans le test ;
- le test de verrouillage de période consommait un mois par exécution sans
  jamais le rendre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-09 08:07:36 +00:00
Claude 2e9a4c5efe WP-07 : heures prévu/réalisé/payé et périodes de paie verrouillables
Les trois grandeurs
Sans pointeuse, le réalisé est saisi par le manager. La matrice impose de
distinguer trois grandeurs et non deux, avec deux règles qui ne se négocient
pas :
- **Sans heures réelles, le prévu fait foi.** Attendre une saisie qui ne
  viendra pas ne produirait aucune paie. Une saisie partielle — un début sans
  fin — ne suffit pas : elle donnerait une durée fantaisiste.
- **Le paiement ne dépend jamais de la validation.** Une ligne non validée part
  en paie sur la base du réalisé. Bloquer le paiement d'heures accomplies faute
  de validation est précisément ce que la matrice interdit ; l'écran l'énonce
  pour qu'aucun manager ne croie l'inverse.

Toute correction d'heures déjà saisies exige un motif — sans lui, une
correction est indistinguable d'une erreur de saisie. Une première saisie n'en
demande pas : il n'y a rien à corriger, et exiger un motif à chaque ligne le
ferait remplir machinalement.

Le verrou de période
- Verrouiller fige les instantanés **et** ferme le mois : plus aucun créneau,
  aucune absence, aucune heure réelle ne peut être écrit sur ces dates. Le
  contrôle passe avant l'écriture, dans planning, absences et heures.
- Déverrouiller exige une justification. Rouvrir périme les fichiers déjà
  transmis au cabinet, et six mois plus tard personne ne saurait pourquoi.
- La **péremption d'un export est déduite** de `generatedAt < unlockedAt`,
  jamais stockée : la stocker obligerait à réécrire une trace qui doit rester
  append-only, et une trace réécrite ne prouve plus rien.
- Supprimer une période demande de **retaper son libellé** : un « êtes-vous
  sûr » se clique sans lire, et la suppression efface des instantanés sur
  lesquels un export a pu être bâti.

Un seul calcul pour trois écrans
Les instantanés viennent de `buildPayrollPeriod`, la même fonction que le
rapport de paie et l'export. C'est ce qui satisfait le critère croisé : trois
calculs séparés divergeraient, et l'écart ne se verrait qu'au bulletin. Un test
d'intégration compare instantané et rapport sur le même jeu de données.

Tests enfin idempotents
La suite e2e échouait au second passage : les absences acceptées d'une
exécution bloquaient les demandes de la suivante. Trois corrections, dans
l'ordre d'importance — chaque test travaille sur **son** salarié (le
chevauchement se juge par personne), chaque test libère ce qu'il a créé, et les
fenêtres de dates sont écartées d'une exécution à l'autre. Trois passages
consécutifs passent désormais.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-08 12:25:02 +00:00
Claude 0213375a8f WP-06 : absences, registre de compteurs, calendrier
Le dernier module de démonstration disparaît : `src/lib/demo/` ne contient plus
que l'aperçu.

Le décompte, là où l'on se trompe
- **`endDate` est le dernier jour d'absence, pas la date de reprise.** C'est la
  confusion la plus fréquente du domaine : elle décompte un jour de trop ou de
  trop peu à chaque demande, et le salarié s'en aperçoit au solde, des mois plus
  tard. Le champ du formulaire s'appelle « Dernier jour d'absence », et un test
  couvre explicitement la confusion.
- **Un jour férié dans un congé ne se décompte pas**, sans que l'utilisateur ait
  à scinder sa demande. Exiger la scission, c'est lui déplacer la charge d'un
  calcul que l'outil sait faire.
- Le rythme du contrat est respecté : un temps partiel qui ne travaille jamais
  le mercredi ne consomme pas de congé ce jour-là.
- Ouvrables ou ouvrés est un **paramètre**, pas une constante : se tromper
  fausse tous les soldes de la même façon.

Jours fériés calculés, pas listés
Une table écrite à la main est juste l'année où on l'écrit et fausse dès la
suivante — et un férié manquant se décompte comme un jour de congé, sans que
personne ne le voie. Les onze jours légaux sont donc calculés, Pâques comprise
(algorithme grégorien anonyme, vérifié sur quatre années de référence). Quand
aucun férié n'est enregistré pour l'année demandée, la demande passe mais
l'écran le dit : mieux vaut l'annoncer que laisser croire à un décompte complet.

Le registre est la source de vérité
- Aucun solde n'est stocké : le solde est la **somme** des écritures. Un solde
  stocké se désynchronise, et la désynchronisation ne se voit qu'au moment où un
  salarié conteste.
- `UPDATE` et `DELETE` sont refusés par un trigger PostgreSQL, pas seulement par
  l'application : une règle applicative finit par être contournée par un script
  de reprise. Les tests d'intégration écrivent directement en base pour le
  prouver.
- Annuler une absence acceptée **contre-passe** la prise au lieu de l'effacer,
  à la date de la correction — antidater masquerait la correction dans les
  soldes déjà communiqués. `reversesId` est unique : contre-passer deux fois
  ferait repartir le solde dans l'autre sens.
- Un ajustement manuel sans justification est refusé.

Confidentialité du motif médical
Un manager voit qu'un salarié est absent, pas de quoi il souffre (matrice n° 9).
Le motif d'un arrêt n'est **pas chargé** pour qui n'a pas la capacité de le
lire — un champ absent de la réponse ne peut fuiter ni par le HTML ni par un
journal. Sur la grille de planning, l'absence s'affiche « Absence » : c'est
suffisant pour ne pas planifier quelqu'un.

Acquisition
2,5 jours ouvrables par mois travaillé ; 2 jours par mois d'arrêt maladie non
professionnelle, plafonnés à 24 par an — droit issu de la réforme de 2024, dont
l'oubli prive le salarié d'un droit acquis.

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