docs(rtt) : implementation plan + spec fix (workDaysHours neutralisation) for solidarity deficit

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:51:11 +02:00
parent 67988f73e4
commit 5e8cec7067
2 changed files with 551 additions and 13 deletions
@@ -81,16 +81,22 @@ un jour de solidarité `S` tombe dans la semaine **et** a été inclus dans le s
```
$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);
$weeklyHours = $contractAtS?->getWeeklyHours();
$typeAtS = ContractType::resolve($contractAtS?->getName(), $contractAtS?->getTrackingMode(), $weeklyHours);
// 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;
if (ContractType::CUSTOM === $typeAtS && null !== $weeklyHours && $weeklyHours < 35) {
$isoDayS = (int) (new DateTimeImmutable($S))->format('N');
$workDaysForS = $workDaysByDate[$employeeId][$S] ?? null; // {iso_day: minutes}
// Heures contractuelles RÉELLES du jour (planning workDaysHours), PAS la
// répartition uniforme weeklyHours/5 — c'est ce qui rend le net = -prorata.
$expectedS = $this->dailyReferenceResolver->resolve($weeklyHours, $isoDayS, $workDaysForS);
$workedS = $dailyWorkedMinutes[$S]; // déjà calculé dans la boucle des jours
$prorata = (int) round($weeklyHours * 12);
// 1) faire compter le jour comme s'il était travaillé normalement (annule la
// valeur réelle du jour, quelle qu'elle soit : RTT posé, heures, vide, crédit
// férié virtuel) ; 2) appliquer le forfait solidarité.
$weeklyOvertimeTotalMinutes += ($expectedS - $workedS) - $prorata;
}
```
@@ -98,10 +104,20 @@ 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.
> **Pourquoi `workDaysHours` et pas la référence hebdo CUSTOM** : la référence CUSTOM
> (`computeWeeklyCustomReferenceMinutes`) répartit `weeklyHours` uniformément sur les 5
> jours ouvrés (`weeklyHours/5`), sans tenir compte du planning réel. Neutraliser le jour
> avec cette valeur uniforme (48 min pour le lundi d'Ewa) laisserait le manque des autres
> jours → 2h au lieu de 48 min. En neutralisant avec l'attendu RÉEL du jour
> (`workDaysHours[lundi] = 120 min`), le terme `(attendu travaillé)` ramène la semaine à
> son net « normal » (0 pour une semaine pleine), puis le forfait applique exactement
> prorata. `DailyReferenceMinutesResolver::resolve(weeklyHours, isoDay, workDaysMinutes)`
> renvoie déjà cet attendu réel quand `workDaysMinutes` est fourni (obligatoire pour tout
> CUSTOM < 35h). Fallback uniforme si absent.
>
> **Robustesse `EXCLUDED` / férié** : `(attendu travaillé)` annule n'importe quelle
> valeur de `$workedS`, y compris un éventuel crédit férié virtuel si le Lundi de Pentecôte
> cessait d'être exclu. Le résultat ne dépend donc pas de l'état d'`EXCLUDED_PUBLIC_HOLIDAYS`.
## Cas limites