From 67988f73e45464a1e0740ceb6a3d3d07bcb8320a Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 11 Jun 2026 09:41:28 +0200 Subject: [PATCH] docs(rtt) : design spec for solidarity-day deficit on CUSTOM <35h contracts Co-Authored-By: Claude Opus 4.8 (1M context) --- ...06-11-rtt-solidarity-day-deficit-design.md | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-11-rtt-solidarity-day-deficit-design.md diff --git a/docs/superpowers/specs/2026-06-11-rtt-solidarity-day-deficit-design.md b/docs/superpowers/specs/2026-06-11-rtt-solidarity-day-deficit-design.md new file mode 100644 index 0000000..5310352 --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-rtt-solidarity-day-deficit-design.md @@ -0,0 +1,154 @@ +# Design — Déficit « jour de solidarité » pour les contrats CUSTOM < 35h + +Date : 2026-06-11 +Branche : `feature/SIRH-36-corriger-le-calcule-des-rtt-des-contrat-4h` +Statut : validé (brainstorming) + +## Contexte + +Le jour de solidarité (Lundi de Pentecôte) impose à chaque salarié une contribution +de travail non rémunérée : 7h/an pour un temps plein (35h), proratisée pour les temps +partiels. La RH matérialise cette contribution en **posant une absence de type RTT** +sur le Lundi de Pentecôte pour **tous** les salariés, y compris les contrats < 35h. + +Pour les contrats standard (35h/39h), poser un RTT d'une journée draine ~7h du cumul RTT +accumulé — ce qui correspond exactement à l'obligation. Ce comportement **fonctionne +déjà et ne doit pas changer**. + +Pour les contrats **CUSTOM < 35h** (ex. 4h, 25h, 28h — `weeklyHours ≠ 35 et ≠ 39`, +mode TIME), poser une absence RTT (type `R`, `countAsWorkedHours = false`) produit un +déficit égal au **créneau travaillé du jour** (ex. Ewa, 4h, travaille 2h le lundi → +−2h), et non au prorata légal attendu (`7/35 × 4h = 48 min`). Le montant naturel dépend +du planning du jour, pas de l'obligation. C'est le bug à corriger. + +## Règle métier validée + +- **Périmètre** : contrats **CUSTOM avec `weeklyHours < 35`** uniquement. 35h, 39h, + Forfait, Intérim, et CUSTOM ≥ 35h : aucun changement. +- **Date** : Lundi de Pentecôte (= Pâques + 50 jours), calculé par computus, indépendant + de l'env `EXCLUDED_PUBLIC_HOLIDAYS` (qui n'est plus la source de vérité). +- **Montant** : `prorata = round(weeklyHours × 12)` minutes (7h/35h × 60 = 12 min par + heure hebdo). Ex. 4h → 48 min, 25h → 5h00, 28h → 5h36. +- **Net forfaitaire et inconditionnel** : au net, le jour de solidarité vaut **exactement + `−prorata`** dans le cumul RTT, quel que soit ce qui est posé ce jour-là (absence RTT, + heures travaillées, ou rien). On **neutralise** l'effet naturel du jour puis on applique + le forfait. Garantit l'absence de double comptage avec le RTT posé par la RH, et reste + correct même si la RH oublie de poser le RTT. +- **Cumul** : le déficit se cumule avec tout autre déficit/surplus de la même semaine, + réduit le cumul RTT (peut le rendre négatif), et est reporté à l'exercice suivant. + +## Architecture + +### Point d'injection unique + +Tout passe par `App\Service\Rtt\RttRecoveryComputationService::computeRecoveryByWeek`, +le calcul **partagé** consommé par : +- `EmployeeRttSummaryProvider` (onglet RTT), +- `computeTotalRecoveryForExercise` → `RttClosingBalanceService` (clôture / rollover), +- `DumpVerificationSnapshotCommand` (commande de vérification). + +En posant le déficit dans `totalMinutes` / `overtimeMinutes` à cet endroit, il se propage +partout sans duplication. Le drapeau `isFlatRecovery` (déjà existant pour les CUSTOM) +reste `true` → le provider ne draine pas les tranches 25/50 et le fold reporte le déficit +en N+1. + +### Nouveau service pur : `SolidarityDayResolver` + +``` +final class SolidarityDayResolver +{ + // Lundi de Pentecôte = dimanche de Pâques + 50 jours. + public function pentecostMonday(int $year): DateTimeImmutable; + + // Easter via l'algorithme de Meeus/Jones/Butcher (calendrier grégorien), + // sans dépendance à l'extension calendar PHP. + private function easterSunday(int $year): DateTimeImmutable; +} +``` + +Pur, déterministe, aucune dépendance réseau (le chemin de calcul RTT n'a aujourd'hui +aucune dépendance HTTP — on le préserve). Trivial à tester unitairement. + +### Modification de `computeRecoveryByWeek` + +Le service reçoit `SolidarityDayResolver` par injection. Avant la boucle des semaines, on +résout les Lundi de Pentecôte des années civiles couvertes par `[periodFrom, periodTo]` +(exercice Juin N-1 → Mai N → années N-1 et N) et on retient ceux dans la fenêtre. + +Dans la boucle, après le calcul de `weeklyOvertimeTotalMinutes` et **uniquement** quand +un jour de solidarité `S` tombe dans la semaine **et** a été inclus dans le sommage +(`isset($dailyWorkedMinutes[S])`, donc `S ≤ limitDate` et `S ≥ rttStartDate`) : + +``` +$contractAtS = $employeeContractsByDate[$S] ?? null; +if ($contractAtS est CUSTOM && $contractAtS->getWeeklyHours() < 35) { + $isoDayS = (int) (new DateTimeImmutable($S))->format('N'); + $refS = $this->resolveDailyReferenceMinutes($contractAtS->getWeeklyHours(), $isoDayS); + $workedS = $dailyWorkedMinutes[$S]; // déjà calculé dans la boucle des jours + $prorata = (int) round($contractAtS->getWeeklyHours() * 12); + + // 1) neutraliser le net naturel du jour (annule worked - ref, même fonction de + // référence que computeWeeklyCustomReferenceMinutes → annulation exacte) + // 2) appliquer le forfait + $weeklyOvertimeTotalMinutes += $refS - $workedS - $prorata; +} +``` + +Puis `buildWeekRecoveryDetail(...)` est appelé tel quel : pour un CUSTOM, +`totalMinutes = overtimeMinutes = weeklyOvertimeTotalMinutes` (signé), bandes 25/50 = 0, +`isFlatRecovery = true`. + +> Note : la référence `$refS` doit être calculée avec la **même** fonction que +> `computeWeeklyCustomReferenceMinutes` (`resolveDailyReferenceMinutes(weeklyHours, isoDay)`) +> pour que la neutralisation annule exactement la contribution du jour, quelle que soit la +> façon dont la référence journalière est répartie. + +## Cas limites + +| Cas | Comportement | +|-----|--------------| +| Jour de solidarité futur (`> limitDate`) | Pas de déficit (semaine/jour non sommés). Appliqué une fois le jour passé. | +| Jour de solidarité avant `rttStartDate` | Pas de déficit (semaine zéro-ée en amont). | +| Changement de contrat dans la semaine | Contrat lu **au jour de solidarité**, pas à l'ancre de semaine. | +| Salarié non contracté ce jour-là | `contractAtS = null` → pas de déficit. | +| CUSTOM ≥ 35h (36–38h) | Hors périmètre → pas de déficit. | +| 35h/39h avec RTT posé | Inchangé (drainage ~7h via la cascade existante). | +| Autre déficit/surplus la même semaine | Le forfait s'y cumule. | + +## Tests + +### `SolidarityDayResolverTest` +- Pentecôte 2024 = 20 mai 2024 ; 2025 = 9 juin 2025 ; 2026 = 25 mai 2026. +- (optionnel) Pâques pivot : 2025 = 20 avril. + +### `RttRecoveryComputationServiceTest` (ajouts) +- CUSTOM 4h, RTT posé sur le jour de solidarité → semaine = −48 min, `isFlatRecovery = true`, + base/bonus 25/50 = 0. +- CUSTOM 4h, heures travaillées ce jour-là → semaine = −48 min (net forcé). +- CUSTOM 4h, rien de posé → semaine = −48 min. +- CUSTOM 4h avec un autre jour vide la même semaine → −48 min + l'autre déficit (cumul). +- CUSTOM 36h → 0 (hors périmètre). +- 35h avec RTT posé sur le jour de solidarité → inchangé (déficit plein, drainage tranches). +- Jour de solidarité au-delà de `limitDate` → 0. +- `computeTotalRecoveryForExercise` : le déficit solidarité se retrouve dans le total + d'exercice (→ clôture/report N+1). + +### Vérification données prod +- Ewa (id 31, 4h, Lun+Jeu) : semaine du 25 mai 2026 (S22) = −48 min ; le −2h de la S23 + (lundi 1er juin non saisi) reste distinct et inchangé. + +## Hors scope / inchangé + +- Front `RttTab.vue` : déjà propre (clamp des sous-colonnes 25/50 à 0 pour les semaines + déficitaires) → aucun changement. +- Migrations : aucune. +- `EXCLUDED_PUBLIC_HOLIDAYS`, `HolidayVirtualHoursResolver`, traitement des autres fériés : + inchangés. +- Comportement des contrats 35h/39h/Forfait/Intérim sur le jour de solidarité : inchangé. + +## Documentation à mettre à jour (règle projet) + +- `CLAUDE.md` — section Overtime Rules / contrats CUSTOM : ajouter la règle du jour de + solidarité (prorata 12 min/h, net forcé, périmètre < 35h). +- `frontend/data/documentation-content.ts` — doc in-app RTT. +- `doc/rtt-tab.md` et/ou `doc/functional-rules.md` — règle métier détaillée.