# 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`.
