docs(rtt) : design spec for solidarity-day deficit on CUSTOM <35h contracts
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user