Files
SIRH/docs/superpowers/specs/2026-06-09-rtt-custom-deficit-design.md
T
tristan f0387233e4
Auto Tag Develop / tag (push) Successful in 7s
[#SIRH-36] corriger calcule rtt contrat custom (#27)
| 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>
2026-06-11 08:36:57 +00:00

119 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Déficit RTT pris en compte pour les contrats CUSTOM (4h, etc.)
Date : 2026-06-09
Branche : `feature/SIRH-36-corriger-le-calcule-des-rtt-des-contrat-4h`
## Contexte & problème
Les salariées avec un contrat 4h (type CUSTOM : `weeklyHours` ≠ 35/39, non INTERIM/FORFAIT,
mode TIME) voient, dans l'onglet RTT, des semaines travaillées **en dessous** de leurs heures
contractuelles afficher un déficit dans la colonne « Heure » (ex. Ewa S23 : 2h) **sans aucun
effet** : « Total » = 0 et « Cumul » = 0.
Cause : `RttRecoveryComputationService::computeRecoveryByWeek` écrête le total hebdo des CUSTOM
avec `totalMinutes = max(0, $weeklyOvertimeTotalMinutes)`. Le déficit est donc supprimé. C'est
le comportement métier documenté jusqu'ici (« CUSTOM : le déficit n'impacte pas le solde »).
Décision métier (validée avec le client) : **le déficit doit être pris en compte et réduire le
cumul**, comme pour les 35h/39h, avec un affichage propre dans l'onglet RTT.
## Décisions validées
1. **Le cumul peut devenir négatif** (identique aux 35h/39h, comportement déjà assumé dans le
code — cf. `RttClosingBalanceService::fold` ligne 98 « leftover may push the balance
negative, as on screen »). Le négatif est reporté à l'exercice suivant.
2. **Déficit visible** en colonnes « Heure » + « Total » + « Cumul ». Les colonnes 25%/50%
restent à **0** pour un contrat CUSTOM (un 4h n'a pas de bonus, donc pas de tranches).
## Principe technique
Retirer l'écrêtage `max(0, …)` pour les semaines CUSTOM : le déficit (négatif) circule dans
`WeekRecoveryDetail::totalMinutes` et réduit le cumul. La seule spécificité CUSTOM reste :
récupération 1h = 1h, sans bonus 25/50.
### Le point délicat : ne pas drainer les tranches 25/50
Pour un 35h/39h, une semaine déficitaire **draine** les tranches 25/50 accumulées via la cascade
de `EmployeeRttSummaryProvider` (lignes 149-174) et de `RttClosingBalanceService::fold` (lignes
92-99), ce qui affiche des valeurs négatives en « Total 25% / 50% ».
Pour un CUSTOM, la récup n'est jamais bucketisée (elle vit uniquement dans `totalMinutes`). La
cascade ne doit donc **pas** s'appliquer aux semaines CUSTOM, sinon le déficit apparaîtrait en
négatif dans « Total 25% » (affichage sale, et incohérent avec les récups positives qui, elles,
n'y figurent pas).
Solution : un drapeau **`isFlatRecovery`** (récupération plate 1:1, sans tranches) porté par la
semaine, qui désactive la cascade dans le provider.
## Changements
### Backend
1. **`src/Dto/Rtt/WeekRecoveryDetail.php`** : ajout `public bool $isFlatRecovery = false`.
2. **`src/Dto/Rtt/EmployeeRttWeekSummary.php`** : ajout `public bool $isFlatRecovery = false`.
3. **`src/Service/Rtt/RttRecoveryComputationService.php`**
(`computeRecoveryByWeek`, branche `$isCustomContract`) :
- `totalMinutes = $weeklyOvertimeTotalMinutes` (signé — plus de `max(0, …)`).
- `isFlatRecovery: true` sur le `WeekRecoveryDetail` retourné.
- Les buckets `base25/bonus25/base50/bonus50` restent à 0 (inchangé).
- Cas non-CUSTOM et PRESENCE/INTERIM inchangés (`isFlatRecovery` reste `false`).
4. **`src/State/EmployeeRttSummaryProvider.php`**
- `buildWeekSummaries` : propager `isFlatRecovery` du `WeekRecoveryDetail` vers
l'`EmployeeRttWeekSummary` — dans le cas mono-mois **et** dans le cas semaine à cheval sur
deux mois (les deux instances héritent du drapeau).
- Cascade déficit (ligne 150) : condition devient
`if ($week->totalMinutes >= 0 || $week->isFlatRecovery)`. Pour une semaine CUSTOM
déficitaire, on ne draine pas : les buckets restent 0, `cumulativeBalanceMinutes`
(déjà basé sur `totalMinutes`, ligne 197) intègre le déficit.
- Dans la reconstruction de la branche `else` (semaine déficitaire normale), conserver
explicitement `isFlatRecovery: $week->isFlatRecovery` (toujours `false` à ce point, mais
explicite pour la clarté).
5. **`src/Command/DumpVerificationSnapshotCommand.php`** : ce command duplique la cascade du
provider (lignes ~695-716). Mettre à jour la condition de cascade pour respecter
`isFlatRecovery`, et propager le drapeau dans sa reconstruction des week summaries, afin que
les snapshots before/after restent fidèles à l'app.
### Aucun changement
- **`src/Service/Rtt/RttClosingBalanceService.php`** : `fold` gère déjà `totalMinutes` négatif
(branche déficit lignes 92-99) et le remainder CUSTOM (lignes 83-87). Le report N+1 intègre
donc automatiquement le déficit. Pas de modification.
- **`frontend/components/employees/RttTab.vue`** : aucun. Les sous-colonnes Base/25%/50% sont
déjà écrêtées à 0 sur les semaines déficitaires (`totalMinutes >= 0 ? … : 0`). Avec buckets =
0 côté back, « Total 25%/50% » = 0, et « Heure »/« Total »/« Cumul » affichent le déficit.
- **Pas de migration** : aucun changement de schéma.
## Effets de bord (assumés, cohérents)
- **Récap congés** (`LeaveRecapRowBuilder::…` via `computeTotalRecoveryForExercise`) : la valeur
RTT reflètera aussi les déficits et peut devenir négative. Cohérent avec la décision.
- **Rollover / report** : la clôture d'exercice (`computeClosingBalance`) intègre désormais les
déficits. Les lignes `employee_rtt_balances` déjà stockées (calculées avec l'ancienne logique,
déficit = 0) doivent être rafraîchies après déploiement :
`php bin/console app:rtt:rollover --force --recompute` (ne touche pas les lignes
`is_locked`). Ex. Ewa : clôture 2026 passe de 0 à 2h, donc report d'ouverture 2027 = 2h.
- **Carry / Report row** : pour un CUSTOM, le report reste stocké/affiché dans la tranche
`base25` (convention pré-existante du `fold`, remainder parking) — comportement inchangé, hors
périmètre.
## Tests (TDD)
- **`tests/Service/Rtt/RttClosingBalanceServiceTest.php`** : nouveau cas — un `WeekRecoveryDetail`
CUSTOM **déficitaire** (`totalMinutes` négatif) diminue bien la clôture (somme = report +
Σ semaines payés, négatif inclus).
- **`tests/State/EmployeeRttSummaryProviderTest.php`** : semaine CUSTOM déficitaire
(`isFlatRecovery = true`, `totalMinutes < 0`) → buckets 25/50 restent 0, `cumulativeBalance`
réduit du déficit (pas de drainage des tranches). Vérifier aussi qu'une semaine 35h/39h
déficitaire continue de drainer (non-régression).
- **`tests/Service/Rtt/RttRecoveryComputationServiceTest.php`** : si réalisable en intégration —
contrat CUSTOM, semaine travaillée sous les heures → `totalMinutes` négatif et
`isFlatRecovery = true`.
## Documentation (obligatoire, même intervention)
- `doc/rtt-rollover.md` et/ou `doc/rtt-tab.md` : mettre à jour la règle CUSTOM (déficit
désormais compté).
- `frontend/data/documentation-content.ts` : section RTT — déficit des contrats CUSTOM.
- `CLAUDE.md` : section « Overtime Rules » — corriger « deficit doesn't impact balance » pour les
CUSTOM ; documenter `isFlatRecovery`.