| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #27 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
9.0 KiB
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 < 35uniquement. 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
−proratadans 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;
$weeklyHours = $contractAtS?->getWeeklyHours();
$typeAtS = ContractType::resolve($contractAtS?->getName(), $contractAtS?->getTrackingMode(), $weeklyHours);
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;
}
Puis buildWeekRecoveryDetail(...) est appelé tel quel : pour un CUSTOM,
totalMinutes = overtimeMinutes = weeklyOvertimeTotalMinutes (signé), bandes 25/50 = 0,
isFlatRecovery = true.
Pourquoi
workDaysHourset pas la référence hebdo CUSTOM : la référence CUSTOM (computeWeeklyCustomReferenceMinutes) répartitweeklyHoursuniformé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 quandworkDaysMinutesest 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
| 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.mdet/oudoc/functional-rules.md— règle métier détaillée.