# RTT — Déficit pris en compte pour les contrats CUSTOM — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Pour les contrats CUSTOM (4h, 25h…), une semaine travaillée sous les heures contractuelles doit réduire le cumul RTT (déficit compté), avec un affichage propre (colonnes 25%/50% à 0). **Architecture:** On retire l'écrêtage `max(0, …)` du total hebdo CUSTOM dans `RttRecoveryComputationService` (le déficit signé circule dans `totalMinutes`) et on marque ces semaines `isFlatRecovery = true`. Ce drapeau désactive la cascade de drainage 25/50 dans `EmployeeRttSummaryProvider`, de sorte que le déficit n'impacte que les colonnes Heure/Total/Cumul. Le `RttClosingBalanceService::fold` gère déjà les totaux négatifs (report N+1 cohérent). **Tech Stack:** Symfony, API Platform, Doctrine ORM, PHPUnit. Frontend Nuxt/Vue (aucun changement de code, docs uniquement). **Spec:** `docs/superpowers/specs/2026-06-09-rtt-custom-deficit-design.md` --- ### Task 1: Ajouter le drapeau `isFlatRecovery` aux DTOs **Files:** - Modify: `src/Dto/Rtt/WeekRecoveryDetail.php` - Modify: `src/Dto/Rtt/EmployeeRttWeekSummary.php` Ces deux DTOs sont de simples porteurs de données ; ils sont couverts par les tests des tâches 3 et 4. Pas de test dédié. - [ ] **Step 1: Ajouter le champ à `WeekRecoveryDetail`** Dans `src/Dto/Rtt/WeekRecoveryDetail.php`, ajouter un dernier paramètre au constructeur (après `$dailyMinutes`) : ```php /** * @param array $dailyMinutes date (Y-m-d) => worked minutes */ public function __construct( public int $overtimeMinutes = 0, public int $base25Minutes = 0, public int $bonus25Minutes = 0, public int $base50Minutes = 0, public int $bonus50Minutes = 0, public int $totalMinutes = 0, public array $dailyMinutes = [], public bool $isFlatRecovery = false, ) {} ``` - [ ] **Step 2: Ajouter le champ à `EmployeeRttWeekSummary`** Dans `src/Dto/Rtt/EmployeeRttWeekSummary.php`, ajouter un dernier paramètre au constructeur (après `$cumulativeBalanceMinutes`) : ```php public function __construct( public int $month, public int $weekNumber, public string $weekStart, public string $weekEnd, public int $overtimeMinutes = 0, public int $base25Minutes = 0, public int $bonus25Minutes = 0, public int $base50Minutes = 0, public int $bonus50Minutes = 0, public int $totalMinutes = 0, public int $cumulativeBalanceMinutes = 0, public bool $isFlatRecovery = false, ) {} ``` - [ ] **Step 3: Vérifier que rien n'est cassé (DTO à valeur par défaut)** Run: `make test` Expected: PASS (aucun appel existant ne casse — le nouveau paramètre a une valeur par défaut). - [ ] **Step 4: Commit** ```bash git add src/Dto/Rtt/WeekRecoveryDetail.php src/Dto/Rtt/EmployeeRttWeekSummary.php git commit -m "feat(rtt): add isFlatRecovery flag to recovery DTOs" ``` --- ### Task 2: Test de clôture — déficit CUSTOM diminue le report (aucun code à changer) **Files:** - Test: `tests/Service/Rtt/RttClosingBalanceServiceTest.php` `RttClosingBalanceService::fold` gère déjà les `totalMinutes` négatifs. On ajoute un test explicite « déficit CUSTOM » pour verrouiller le comportement. - [ ] **Step 1: Écrire le test** Ajouter cette méthode dans `tests/Service/Rtt/RttClosingBalanceServiceTest.php` (après `testCustomRecoveryWithoutBucketsStillCountsInTotal`) : ```php public function testCustomDeficitWeekReducesClosingBalance(): void { // CUSTOM (4h) : une semaine de récup +3h puis une semaine déficitaire -1h // (toutes deux sans tranches 25/50). Le déficit doit réduire la clôture. $recovery = new WeekRecoveryDetail(totalMinutes: 180, isFlatRecovery: true); // +3h $deficit = new WeekRecoveryDetail(totalMinutes: -60, isFlatRecovery: true); // -1h $closing = $this->service()->fold(new WeekRecoveryDetail(), [$recovery, $deficit], $this->payments()); // 3h - 1h = 2h reportées, et la somme des buckets égale toujours le total. self::assertSame(120, $closing->totalMinutes); self::assertSame( 120, $closing->base25Minutes + $closing->bonus25Minutes + $closing->base50Minutes + $closing->bonus50Minutes, ); } ``` - [ ] **Step 2: Lancer le test** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && vendor/bin/phpunit tests/Service/Rtt/RttClosingBalanceServiceTest.php --filter testCustomDeficitWeekReducesClosingBalance'` Expected: PASS (le `fold` gère déjà les totaux négatifs). - [ ] **Step 3: Commit** ```bash git add tests/Service/Rtt/RttClosingBalanceServiceTest.php git commit -m "test(rtt): custom deficit week reduces closing balance" ``` --- ### Task 3: `RttRecoveryComputationService` — récup plate signée pour CUSTOM **Files:** - Modify: `src/Service/Rtt/RttRecoveryComputationService.php` (`computeRecoveryByWeek` lignes ~243-270, + nouvelle méthode privée) - Test: `tests/Service/Rtt/RttRecoveryComputationServiceTest.php` On extrait la construction du `WeekRecoveryDetail` dans une méthode pure `buildWeekRecoveryDetail`, testable par réflexion (style existant du fichier de test), et on y applique le changement CUSTOM. - [ ] **Step 1: Écrire les tests (méthode pure)** Ajouter dans `tests/Service/Rtt/RttRecoveryComputationServiceTest.php` (le fichier instancie déjà le service via `newInstanceWithoutConstructor` et possède le helper `invokePrivate`) : ```php public function testBuildWeekDetailCustomDeficitKeepsSignedTotalAndFlatFlag(): void { $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor(); // CUSTOM, semaine sous les heures : overtime -120 (worked 2h sur réf 4h). $detail = $this->invokePrivate( $service, 'buildWeekRecoveryDetail', false, // isPresence false, // disableBonuses true, // isCustom -120, // overtimeTotalMinutes 0, // rawBase25 0, // rawBase50 [], // dailyMinutes ); self::assertSame(-120, $detail->totalMinutes); self::assertTrue($detail->isFlatRecovery); self::assertSame(0, $detail->base25Minutes); self::assertSame(0, $detail->base50Minutes); } public function testBuildWeekDetailCustomPositiveIsFlatOneToOne(): void { $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor(); $detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, true, 180, 0, 0, []); self::assertSame(180, $detail->totalMinutes); // 1h = 1h self::assertTrue($detail->isFlatRecovery); } public function testBuildWeekDetailStandardKeepsBucketsAndBonuses(): void { $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor(); // 39h : overtime 300, base25 240, base50 60. $detail = $this->invokePrivate($service, 'buildWeekRecoveryDetail', false, false, false, 300, 240, 60, []); self::assertFalse($detail->isFlatRecovery); self::assertSame(240, $detail->base25Minutes); self::assertSame(60, $detail->bonus25Minutes); // round(240 * 0.25) self::assertSame(60, $detail->base50Minutes); self::assertSame(30, $detail->bonus50Minutes); // round(60 * 0.5) self::assertSame(300 + 60 + 30, $detail->totalMinutes); } ``` - [ ] **Step 2: Lancer les tests pour vérifier qu'ils échouent** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && vendor/bin/phpunit tests/Service/Rtt/RttRecoveryComputationServiceTest.php --filter testBuildWeekDetail'` Expected: FAIL — `buildWeekRecoveryDetail` n'existe pas encore (ReflectionException / method does not exist). - [ ] **Step 3: Ajouter la méthode privée `buildWeekRecoveryDetail`** Dans `src/Service/Rtt/RttRecoveryComputationService.php`, ajouter cette méthode (par ex. juste après `computeRecoveryByWeek`, avant `computeMetrics`) : ```php /** * Assemble le détail de récupération d'une semaine à partir des drapeaux résolus et * des bandes d'heures sup brutes. * * - PRESENCE / INTERIM (bonus désactivés) : aucune récupération. * - CUSTOM : récupération plate 1h = 1h, sans tranches 25/50 ; l'heure sup signée EST * le total, donc une semaine travaillée sous les heures contractuelles produit un * total négatif (déficit qui réduit le solde). Marquée isFlatRecovery pour que le * provider ne draine pas les tranches 25/50. * - Standard 35h/39h : heures sup + bonus 25 %/50 %. * * @param array $dailyMinutes */ private function buildWeekRecoveryDetail( bool $isPresence, bool $disableBonuses, bool $isCustom, int $overtimeTotalMinutes, int $rawBase25, int $rawBase50, array $dailyMinutes, ): WeekRecoveryDetail { $noBands = $isPresence || $disableBonuses || $isCustom; $base25 = $noBands ? 0 : $rawBase25; $bonus25 = $noBands ? 0 : (int) round($base25 * 0.25); $base50 = $noBands ? 0 : $rawBase50; $bonus50 = $noBands ? 0 : (int) round($base50 * 0.5); if ($isPresence || $disableBonuses) { $totalMinutes = 0; } elseif ($isCustom) { $totalMinutes = $overtimeTotalMinutes; // signé : le déficit réduit le solde } else { $totalMinutes = $overtimeTotalMinutes + $bonus25 + $bonus50; } return new WeekRecoveryDetail( overtimeMinutes: $overtimeTotalMinutes, base25Minutes: $base25, bonus25Minutes: $bonus25, base50Minutes: $base50, bonus50Minutes: $bonus50, totalMinutes: $totalMinutes, dailyMinutes: $dailyMinutes, isFlatRecovery: $isCustom, ); } ``` - [ ] **Step 4: Brancher l'appelant sur la nouvelle méthode** Dans `computeRecoveryByWeek`, remplacer le bloc existant (depuis `[$rawBase25, $rawBase50] = …` jusqu'à la fin du `new WeekRecoveryDetail(...)` qui assigne `$results[$weekKey]`) par : ```php [$rawBase25, $rawBase50] = $this->computeOvertimeBaseMinutes($weeklyTotalMinutes, $overtime25StartMinutes, $overtime50StartMinutes); $results[$weekKey] = $this->buildWeekRecoveryDetail( $isWeekPresenceTracking, $disableOvertimeBonuses, $isCustomContract, $weeklyOvertimeTotalMinutes, $rawBase25, $rawBase50, $dailyWorkedMinutes, ); ``` (Conserver les lignes précédentes qui calculent `$weeklyOvertimeTotalMinutes`, `$overtime25StartMinutes`, `$overtime50StartMinutes`.) - [ ] **Step 5: Lancer les tests pour vérifier qu'ils passent** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && vendor/bin/phpunit tests/Service/Rtt/RttRecoveryComputationServiceTest.php'` Expected: PASS (nouveaux tests + tests existants des helpers). - [ ] **Step 6: Commit** ```bash git add src/Service/Rtt/RttRecoveryComputationService.php tests/Service/Rtt/RttRecoveryComputationServiceTest.php git commit -m "feat(rtt): custom contract deficit counts as signed recovery (1h=1h, no bands)" ``` --- ### Task 4: `EmployeeRttSummaryProvider` — sauter la cascade pour les semaines plates **Files:** - Modify: `src/State/EmployeeRttSummaryProvider.php` (cascade lignes ~145-174 → méthode extraite ; `buildWeekSummaries` lignes ~385-396 et ~425-436) - Test: `tests/State/EmployeeRttSummaryProviderTest.php` - [ ] **Step 1: Écrire les tests (méthode pure `applyDeficitCascade`)** Ajouter dans `tests/State/EmployeeRttSummaryProviderTest.php`. Le fichier importe déjà `EmployeeRttSummaryProvider`, `ReflectionClass`, et possède `invokePrivate` / `buildProvider`. Ajouter d'abord ce petit helper de fabrication en bas de la classe (si un helper équivalent n'existe pas déjà) : ```php private function weekSummary(int $totalMinutes, bool $isFlat, int $base25 = 0, int $base50 = 0): \App\Dto\Rtt\EmployeeRttWeekSummary { return new \App\Dto\Rtt\EmployeeRttWeekSummary( month: 6, weekNumber: 1, weekStart: '2026-06-01', weekEnd: '2026-06-07', overtimeMinutes: $totalMinutes, base25Minutes: $base25, base50Minutes: $base50, totalMinutes: $totalMinutes, isFlatRecovery: $isFlat, ); } ``` Puis les tests : ```php public function testFlatDeficitWeekIsNotDrainedFromTiers(): void { $provider = $this->buildProvider([]); // Semaine CUSTOM déficitaire (-120), aucune tranche accumulée. $weeks = [$this->weekSummary(-120, true)]; $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 0, 0); // Buckets restent à 0 ; le total négatif est conservé (le cumul est calculé ailleurs). self::assertSame(0, $result[0]->base25Minutes); self::assertSame(0, $result[0]->base50Minutes); self::assertSame(-120, $result[0]->totalMinutes); self::assertTrue($result[0]->isFlatRecovery); } public function testStandardDeficitWeekDrainsFiftyThenTwentyFive(): void { $provider = $this->buildProvider([]); // Semaine 35h/39h déficitaire (-100), avec 60 en 50% et 120 en 25% accumulés. $weeks = [$this->weekSummary(-100, false)]; $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 120, 60); self::assertSame(-60, $result[0]->base50Minutes); // 60 drainés du 50% self::assertSame(-40, $result[0]->base25Minutes); // 40 restants drainés du 25% self::assertSame(-100, $result[0]->totalMinutes); } public function testFlatPositiveWeekIsUntouched(): void { $provider = $this->buildProvider([]); $weeks = [$this->weekSummary(180, true)]; $result = $this->invokePrivate($provider, 'applyDeficitCascade', $weeks, 0, 0); self::assertSame(180, $result[0]->totalMinutes); self::assertSame(0, $result[0]->base25Minutes); } ``` NB : si `invokePrivate` n'accepte pas d'arguments variadiques dans ce fichier, vérifier sa signature en haut du fichier de test et adapter (l'autre fichier de test du dépôt l'utilise déjà avec des arguments). - [ ] **Step 2: Lancer les tests pour vérifier qu'ils échouent** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && vendor/bin/phpunit tests/State/EmployeeRttSummaryProviderTest.php --filter Deficit'` Expected: FAIL — `applyDeficitCascade` n'existe pas encore. - [ ] **Step 3: Extraire la cascade en méthode privée** Dans `src/State/EmployeeRttSummaryProvider.php`, ajouter cette méthode privée (par ex. juste avant `buildWeekSummaries`) : ```php /** * Distribue les semaines déficitaires sur les tranches 25/50 accumulées (50 % d'abord, * puis 25 %), en réécrivant les buckets affichés de chaque semaine déficitaire avec les * montants négatifs drainés. * * Les semaines à récupération plate (CUSTOM 1h = 1h) sont ignorées : elles n'ont pas de * tranches 25/50, donc leur déficit ne réduit que le cumul courant (calculé ensuite à * partir de totalMinutes) et les colonnes 25/50 restent à 0. * * @param list $weeks * * @return list */ private function applyDeficitCascade(array $weeks, int $cumulative25, int $cumulative50): array { foreach ($weeks as $i => $week) { if ($week->totalMinutes >= 0 || $week->isFlatRecovery) { $cumulative50 += $week->base50Minutes + $week->bonus50Minutes; $cumulative25 += $week->base25Minutes + $week->bonus25Minutes; continue; } $deficit = -$week->totalMinutes; $from50 = min($deficit, max(0, $cumulative50)); $from25 = $deficit - $from50; $cumulative50 -= $from50; $cumulative25 -= $from25; $weeks[$i] = new EmployeeRttWeekSummary( month: $week->month, weekNumber: $week->weekNumber, weekStart: $week->weekStart, weekEnd: $week->weekEnd, overtimeMinutes: $week->overtimeMinutes, base25Minutes: $from25 > 0 ? -$from25 : 0, bonus25Minutes: 0, base50Minutes: $from50 > 0 ? -$from50 : 0, bonus50Minutes: 0, totalMinutes: $week->totalMinutes, isFlatRecovery: $week->isFlatRecovery, ); } return $weeks; } ``` - [ ] **Step 4: Brancher `provide()` sur la méthode extraite** Dans `provide()`, remplacer le bloc commentaire + boucle (depuis `// Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%)` et la déclaration de `$cumulative50`/`$cumulative25` jusqu'à la fin du `foreach ($summary->weeks as $i => $week) { … }`) par : ```php // Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%). // Flat-recovery (CUSTOM) weeks are skipped — their deficit only reduces the running cumul. $summary->weeks = $this->applyDeficitCascade( $summary->weeks, $carry->base25Minutes + $carry->bonus25Minutes, $carry->base50Minutes + $carry->bonus50Minutes, ); ``` - [ ] **Step 5: Propager `isFlatRecovery` dans `buildWeekSummaries`** Dans `buildWeekSummaries`, ajouter `isFlatRecovery: $detail->isFlatRecovery,` comme dernier argument des DEUX appels `new EmployeeRttWeekSummary(...)` : - le cas mono-mois (`if ($startMonth === $endMonth)`, après `totalMinutes: $detail->totalMinutes,`) - le cas semaine à cheval (boucle `foreach ([$startMonth, $endMonth] …`, après `totalMinutes: (int) round($detail->totalMinutes * $ratio),`) - [ ] **Step 6: Lancer les tests** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && vendor/bin/phpunit tests/State/EmployeeRttSummaryProviderTest.php'` Expected: PASS (nouveaux tests + tests existants du provider). - [ ] **Step 7: Commit** ```bash git add src/State/EmployeeRttSummaryProvider.php tests/State/EmployeeRttSummaryProviderTest.php git commit -m "feat(rtt): skip 25/50 deficit cascade for flat (custom) recovery weeks" ``` --- ### Task 5: `DumpVerificationSnapshotCommand` — refléter le drapeau **Files:** - Modify: `src/Command/DumpVerificationSnapshotCommand.php` (`distributeDeficits` ligne ~689 ; build des week summaries lignes ~628-639 et ~664-675) Ce command duplique la logique du provider pour produire les snapshots de vérification. Sans mise à jour, les snapshots « after » seraient faux pour les semaines CUSTOM. - [ ] **Step 1: Propager `isFlatRecovery` dans les week summaries dupliquées** Ajouter `isFlatRecovery: $detail->isFlatRecovery,` comme dernier argument des deux appels `new EmployeeRttWeekSummary(...)` (cas mono-mois ligne ~638 après `totalMinutes: $detail->totalMinutes,`, et cas à cheval ligne ~674 après `totalMinutes: (int) round($detail->totalMinutes * $ratio),`). - [ ] **Step 2: Sauter la cascade pour les semaines plates dans `distributeDeficits`** Modifier la condition de la boucle : ```php if ($week->totalMinutes >= 0 || $week->isFlatRecovery) { ``` et ajouter `isFlatRecovery: $week->isFlatRecovery,` comme dernier argument du `new EmployeeRttWeekSummary(...)` de reconstruction (après `totalMinutes: $week->totalMinutes,`). - [ ] **Step 3: Vérifier la compilation (lint)** Run: `docker exec php-sirh-fpm sh -c 'cd /var/www/html && php -l src/Command/DumpVerificationSnapshotCommand.php'` Expected: `No syntax errors detected`. - [ ] **Step 4: Commit** ```bash git add src/Command/DumpVerificationSnapshotCommand.php git commit -m "chore(rtt): mirror flat-recovery cascade skip in verification snapshot command" ``` --- ### Task 6: Documentation (obligatoire — même intervention) **Files:** - Modify: `CLAUDE.md` (ligne ~68) - Modify: `frontend/data/documentation-content.ts` (lignes ~367-368 et section RTT ~520) - Modify: `doc/rtt-tab.md` - Modify: `doc/rtt-rollover.md` - [ ] **Step 1: `CLAUDE.md`** Remplacer la ligne : ``` - CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery), deficit doesn't impact balance ``` par : ``` - CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery). **Le déficit (heures travaillées < heures contractuelles) réduit le cumul RTT 1:1** (peut devenir négatif, reporté à l'exercice suivant). Implémenté via `WeekRecoveryDetail::isFlatRecovery` / `EmployeeRttWeekSummary::isFlatRecovery` : ces semaines portent leur récup/déficit signé dans `totalMinutes` (`RttRecoveryComputationService::buildWeekRecoveryDetail`) et `EmployeeRttSummaryProvider::applyDeficitCascade` **ne draine pas** les tranches 25/50 pour elles (colonnes 25%/50% restent à 0). Le `RttClosingBalanceService::fold` reporte le déficit en N+1. ``` - [ ] **Step 2: In-app docs — règles d'heures sup (ligne ~367)** Dans `frontend/data/documentation-content.ts`, remplacer le contenu de la liste ligne ~367 : ``` Contrats CUSTOM (4h, 25h, etc.) : 1h supplémentaire = 1h de récupération, pas de bonus ``` par : ``` Contrats CUSTOM (4h, 25h, etc.) : 1h supplémentaire = 1h de récupération, pas de bonus. Une semaine sous les heures contractuelles réduit le cumul RTT (1h manquante = -1h), sans passer par les tranches 25/50 ``` - [ ] **Step 3: In-app docs — note déficit (ligne ~368)** Remplacer le contenu de la note ligne ~368 : ``` En cas de déficit hebdomadaire (heures travaillées < heures contrat), le déficit est déduit du cumul RTT : d'abord des heures à 50%, puis des heures à 25%. ``` par : ``` En cas de déficit hebdomadaire (heures travaillées < heures contrat), le déficit est déduit du cumul RTT. Pour un 35h/39h, il est puisé d'abord dans les heures à 50%, puis à 25%. Pour un contrat CUSTOM (4h, etc.), il réduit directement le cumul (pas de tranches 25/50) ; le cumul peut devenir négatif et est reporté à l'exercice suivant. ``` - [ ] **Step 4: In-app docs — section RTT « Compteurs » (ajout d'une note)** Dans l'article `rtt-compteurs` (id `'rtt-compteurs'`, vers la ligne ~520), ajouter une note à la fin du tableau `blocks` (après le dernier `paragraph`) : ```javascript { type: 'note', content: 'Contrats CUSTOM (ex. 4h) : une semaine travaillée sous les heures contractuelles génère un déficit qui réduit le cumul RTT (1h manquante = -1h), sans tranches 25/50. Le cumul peut devenir négatif et est reporté à l\'exercice suivant.' }, ``` - [ ] **Step 5: `doc/rtt-tab.md` — ajouter une sous-section règle CUSTOM** Après la section « Période affichée » (avant « Sélecteur d'année »), insérer : ```markdown ## Règle de calcul — contrats CUSTOM (4h, 25h…) Pour un contrat CUSTOM, la récupération est **plate** (1h sup = 1h récup, sans bonus 25 %/50 %). Depuis 2026-06, une semaine **travaillée sous les heures contractuelles** produit un **déficit signé** dans la colonne « Heure » qui **réduit le « Total » et le « Cumul »** (1h manquante = -1h). Les colonnes Base/25 %/50 % restent à **0** (pas de tranches pour ces contrats). Le cumul peut devenir négatif ; il est reporté à l'exercice suivant. Techniquement : `WeekRecoveryDetail::isFlatRecovery` marque ces semaines ; `EmployeeRttSummaryProvider::applyDeficitCascade` les exclut du drainage des tranches 25/50. ``` - [ ] **Step 6: `doc/rtt-rollover.md` — préciser que le déficit CUSTOM est reporté** Sous le point 3 (« calculer le solde de clôture… ») / la « Règle clef » (ligne ~97), ajouter : ```markdown > Contrats CUSTOM : le solde de clôture intègre désormais les **déficits** hebdomadaires > (semaines travaillées sous les heures contractuelles), via `RttClosingBalanceService::fold` > qui gère les totaux négatifs. La clôture (donc le report d'ouverture N+1) peut être négative. > Après une mise à jour de cette règle, rejouer `app:rtt:rollover --force --recompute` pour > recalculer les lignes `employee_rtt_balances` non verrouillées calculées avec l'ancienne règle. ``` - [ ] **Step 7: Build frontend (vérifier que le TS compile)** Run: `docker exec php-sirh-fpm true` (no-op) puis localement : ne PAS lancer `npm run build` (préférence utilisateur). Vérifier visuellement que les chaînes ajoutées échappent bien les apostrophes (`\'`). - [ ] **Step 8: Commit** ```bash git add CLAUDE.md frontend/data/documentation-content.ts doc/rtt-tab.md doc/rtt-rollover.md git commit -m "docs(rtt): custom contract deficit now reduces the balance" ``` --- ### Task 7: Vérification métier sur données prod + suite complète **Files:** aucun (vérification). - [ ] **Step 1: Lancer toute la suite de tests** Run: `make test` Expected: PASS (aucune régression). - [ ] **Step 2: Vérifier Ewa / Nadia via le snapshot de vérification (ou requête API)** Générer un snapshot « after » et confirmer que pour Ewa (id 31, exercice 2027) la semaine 23 affiche : Heure −2h, Total −2h, Cumul −2h, colonnes 25/50 = 0 ; et que Nadia (id 22) reste cohérente. Run (exemple, adapter à la signature réelle du command) : `docker exec php-sirh-fpm sh -c 'cd /var/www/html && php bin/console app:dump-verification-snapshot --help'` Comparer `docs/verifications/` (before) et le nouveau snapshot. - [ ] **Step 3: Note de déploiement** Consigner dans la PR : après déploiement, exécuter `php bin/console app:rtt:rollover --force --recompute` pour rafraîchir les reports stockés (lignes non verrouillées) calculés avec l'ancienne règle (déficit = 0). --- ## Self-Review - **Spec coverage** : (1) déficit signé CUSTOM → Task 3 ; (2) cumul réduit + cascade non drainée → Task 4 ; (3) report N+1 → Task 2 (fold déjà OK) + note rollover Task 6/7 ; (4) affichage propre (frontend inchangé) → couvert par buckets 0 ; (5) command de vérification → Task 5 ; (6) docs → Task 6. ✓ - **Placeholders** : aucun — code complet à chaque étape. - **Cohérence des types** : `isFlatRecovery` (bool) ajouté de façon identique aux 2 DTOs ; `buildWeekRecoveryDetail` et `applyDeficitCascade` ont des signatures fixes utilisées de manière cohérente entre tâches et tests. ✓