# RTT — Déficit jour de solidarité (CUSTOM < 35h) — 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:** Sur le Lundi de Pentecôte, retrancher au cumul RTT des contrats CUSTOM < 35h un déficit forfaitaire de `7/35 × heuresHebdo` (= 12 min/heure hebdo), net et inconditionnel, sans rien changer aux autres contrats.

**Architecture :** Un service pur `SolidarityDayResolver` calcule le Lundi de Pentecôte par computus (Pâques + 50 j). `RttRecoveryComputationService::computeRecoveryByWeek` (calcul partagé : onglet RTT, clôture/rollover, commande de vérification) neutralise le jour de solidarité pour les CUSTOM < 35h et applique le prorata, en le faisant transiter par `totalMinutes` via le mécanisme `isFlatRecovery` existant (reporté en N+1, ne draine pas les tranches 25/50).

**Tech Stack :** PHP 8.4, Symfony, PHPUnit. Tests purs via `ReflectionClass::newInstanceWithoutConstructor` (pattern existant dans `RttRecoveryComputationServiceTest`).

**Spec :** `docs/superpowers/specs/2026-06-11-rtt-solidarity-day-deficit-design.md`

---

## File Structure

- **Create** `src/Service/Rtt/SolidarityDayResolver.php` — service pur, computus de Pâques + Lundi de Pentecôte. Responsabilité unique : donner la date du jour de solidarité d'une année.
- **Create** `tests/Service/Rtt/SolidarityDayResolverTest.php` — tests des dates 2024/2025/2026.
- **Modify** `src/Service/Rtt/RttRecoveryComputationService.php` — injecter `SolidarityDayResolver` ; ajouter `resolveSolidarityDatesInRange()` + `computeSolidarityDeficitAdjustment()` ; appliquer dans `computeRecoveryByWeek()`.
- **Modify** `tests/Service/Rtt/RttRecoveryComputationServiceTest.php` — tests réflexion de `computeSolidarityDeficitAdjustment()`.
- **Modify (docs)** `CLAUDE.md`, `frontend/data/documentation-content.ts`, `doc/rtt-tab.md`, `doc/functional-rules.md`.
- **Inchangé** : `config/services.yaml` (autowiring : `SolidarityDayResolver` est un service autowireable, et `RttRecoveryComputationService` n'override que `$rttStartDate` — les autres args s'autowirent), `DumpVerificationSnapshotCommand.php` (consomme `WeekRecoveryDetail.totalMinutes`, hérite du déficit), `RttTab.vue`, migrations.

---

## Task 1: SolidarityDayResolver (computus Pâques + Pentecôte)

**Files:**
- Create: `src/Service/Rtt/SolidarityDayResolver.php`
- Test: `tests/Service/Rtt/SolidarityDayResolverTest.php`

- [ ] **Step 1: Write the failing test**

Create `tests/Service/Rtt/SolidarityDayResolverTest.php`:

```php
<?php

declare(strict_types=1);

namespace App\Tests\Service\Rtt;

use App\Service\Rtt\SolidarityDayResolver;
use PHPUnit\Framework\TestCase;

/**
 * @internal
 */
final class SolidarityDayResolverTest extends TestCase
{
    /**
     * Lundi de Pentecôte = dimanche de Pâques + 50 jours.
     * 2024 : Pâques 31/03 → 20/05 ; 2025 : Pâques 20/04 → 09/06 ; 2026 : Pâques 05/04 → 25/05.
     *
     * @dataProvider pentecostCases
     */
    public function testPentecostMonday(int $year, string $expected): void
    {
        $resolver = new SolidarityDayResolver();

        self::assertSame($expected, $resolver->pentecostMonday($year)->format('Y-m-d'));
    }

    /**
     * @return iterable<string, array{int, string}>
     */
    public static function pentecostCases(): iterable
    {
        yield '2024' => [2024, '2024-05-20'];
        yield '2025' => [2025, '2025-06-09'];
        yield '2026' => [2026, '2026-05-25'];
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `make test` (or `docker exec php-sirh-fpm php bin/phpunit tests/Service/Rtt/SolidarityDayResolverTest.php`)
Expected: FAIL — `Class "App\Service\Rtt\SolidarityDayResolver" not found`.

- [ ] **Step 3: Write the implementation**

Create `src/Service/Rtt/SolidarityDayResolver.php`:

```php
<?php

declare(strict_types=1);

namespace App\Service\Rtt;

use DateTimeImmutable;

/**
 * Résout le jour de solidarité (Lundi de Pentecôte) d'une année.
 *
 * Pur et déterministe : Pâques via l'algorithme de Meeus/Jones/Butcher (calendrier
 * grégorien), sans dépendance à l'extension calendar ni au réseau. Lundi de Pentecôte
 * = dimanche de Pâques + 50 jours.
 */
final class SolidarityDayResolver
{
    public function pentecostMonday(int $year): DateTimeImmutable
    {
        return $this->easterSunday($year)->modify('+50 days');
    }

    private function easterSunday(int $year): DateTimeImmutable
    {
        $a = $year % 19;
        $b = intdiv($year, 100);
        $c = $year % 100;
        $d = intdiv($b, 4);
        $e = $b % 4;
        $f = intdiv($b + 8, 25);
        $g = intdiv($b - $f + 1, 3);
        $h = (19 * $a + $b - $d - $g + 15) % 30;
        $i = intdiv($c, 4);
        $k = $c % 4;
        $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7;
        $m = intdiv($a + 11 * $h + 22 * $l, 451);

        $month = intdiv($h + $l - 7 * $m + 114, 31);
        $day   = (($h + $l - 7 * $m + 114) % 31) + 1;

        return new DateTimeImmutable(sprintf('%04d-%02d-%02d', $year, $month, $day));
    }
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `make test`
Expected: PASS (3 nouveaux tests verts, le reste de la suite inchangé).

- [ ] **Step 5: Commit**

```bash
git add src/Service/Rtt/SolidarityDayResolver.php tests/Service/Rtt/SolidarityDayResolverTest.php
git commit -m "feat(rtt) : add SolidarityDayResolver (Pentecost Monday via computus)"
```

---

## Task 2: Déficit solidarité dans RttRecoveryComputationService

**Files:**
- Modify: `src/Service/Rtt/RttRecoveryComputationService.php`
- Test: `tests/Service/Rtt/RttRecoveryComputationServiceTest.php`

Le helper `computeSolidarityDeficitAdjustment()` est **pur** (n'utilise que `ContractType::resolve` et les getters de `Contract`) → testable via `newInstanceWithoutConstructor` comme les autres helpers du fichier. Il renvoie le **delta** à ajouter à `weeklyOvertimeTotalMinutes`.

Rappel arithmétique (Ewa, 4h, lundi, `expected = workDaysHours[lundi] = 120`, `prorata = round(4×12) = 48`) :
- RTT posé / jour vide (`worked = 0`) → delta `(120−0)−48 = +72` ; appliqué au naturel `−120` ⇒ semaine **−48 min**.
- travaillé normalement (`worked = 120`) → delta `(120−120)−48 = −48` ; naturel `0` ⇒ **−48 min**.
- travaillé en plus (`worked = 240`) → delta `(120−240)−48 = −168` ; naturel `+120` ⇒ **−48 min**.

- [ ] **Step 1: Write the failing test (pure helper)**

Add these methods to `tests/Service/Rtt/RttRecoveryComputationServiceTest.php` (before the `invokePrivate` helper). Note `use` additions at top: `use App\Enum\TrackingMode;` (already imports `App\Entity\Contract`).

```php
    private static function customContract(int $weeklyHours): Contract
    {
        return new Contract()
            ->setName('Temps partiel')
            ->setTrackingMode(TrackingMode::TIME)
            ->setWeeklyHours($weeklyHours)
        ;
    }

    /**
     * CUSTOM 4h, jour de solidarité non travaillé (RTT posé ou vide) : delta = (attendu − 0) − prorata.
     * attendu lundi = workDaysHours = 120 ; prorata = round(4×12) = 48 ; delta = 120 − 48 = 72.
     * (Combiné au naturel −120 de la semaine, donne −48 min.)
     */
    public function testSolidarityAdjustmentCustomNotWorkedNeutralisesToProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate(
            $service,
            'computeSolidarityDeficitAdjustment',
            self::customContract(4),
            120, // expectedMinutes (workDaysHours du lundi)
            0,   // workedMinutes (RTT posé / vide)
        );

        self::assertSame(72, $delta);
    }

    /**
     * CUSTOM 4h, jour de solidarité travaillé normalement (120) : delta = (120 − 120) − 48 = −48.
     */
    public function testSolidarityAdjustmentCustomWorkedNormallyChargesProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 120);

        self::assertSame(-48, $delta);
    }

    /**
     * CUSTOM 4h, jour de solidarité travaillé en plus (240) : delta = (120 − 240) − 48 = −168.
     * Le surplus du jour de solidarité n'est PAS crédité (jour neutralisé, net forcé à −prorata).
     */
    public function testSolidarityAdjustmentCustomWorkedExtraStillNetsProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 120, 240);

        self::assertSame(-168, $delta);
    }

    /**
     * CUSTOM 28h : prorata = round(28×12) = 336 (5h36). worked 0, expected 336 → delta 0.
     */
    public function testSolidarityAdjustmentCustom28hUsesProrata(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(28), 336, 0);

        self::assertSame(0, $delta);
    }

    /**
     * CUSTOM ≥ 35h (36h) : hors périmètre → delta 0.
     */
    public function testSolidarityAdjustmentCustom36hOutOfScope(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(36), 999, 0);

        self::assertSame(0, $delta);
    }

    /**
     * 35h : type H35 (pas CUSTOM) → delta 0 (comportement inchangé, RTT posé fait foi).
     */
    public function testSolidarityAdjustment35hOutOfScope(): void
    {
        $service  = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
        $contract = new Contract()->setName('35h')->setTrackingMode(TrackingMode::TIME)->setWeeklyHours(35);

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', $contract, 420, 0);

        self::assertSame(0, $delta);
    }

    /**
     * Aucun contrat ce jour-là (salarié parti / pas encore embauché) → delta 0.
     */
    public function testSolidarityAdjustmentNoContractIsZero(): void
    {
        $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();

        $delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', null, 0, 0);

        self::assertSame(0, $delta);
    }
```

- [ ] **Step 2: Run test to verify it fails**

Run: `make test`
Expected: FAIL — `computeSolidarityDeficitAdjustment` n'existe pas (réflexion : `Method ... does not exist`).

- [ ] **Step 3: Add the constructor dependency**

In `src/Service/Rtt/RttRecoveryComputationService.php`, add `SolidarityDayResolver` to the constructor (BEFORE the defaulted `$rttStartDate`, sinon erreur « param non-défaut après défaut »). `SolidarityDayResolver` est dans le même namespace `App\Service\Rtt` → aucun `use` à ajouter.

```php
    public function __construct(
        private WorkHourRepository $workHourRepository,
        private AbsenceRepository $absenceRepository,
        private AbsenceSegmentsResolver $absenceSegmentsResolver,
        private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
        private EmployeeContractResolver $contractResolver,
        private DailyReferenceMinutesResolver $dailyReferenceResolver,
        private HolidayVirtualHoursResolver $holidayVirtualHoursResolver,
        private SolidarityDayResolver $solidarityDayResolver,
        string $rttStartDate = '',
    ) {
        $this->rttStartDate = '' !== $rttStartDate ? new DateTimeImmutable($rttStartDate) : null;
    }
```

- [ ] **Step 4: Add the two private methods**

In the same file, add these methods (e.g. just after `resolveWeekAnchorDate`, alongside the other private helpers):

```php
    /**
     * Lundi(s) de Pentecôte (jour de solidarité) inclus dans [from, to]. Un exercice
     * Juin N-1 → Mai N couvre les années civiles N-1 et N ; on retient les dates dans la fenêtre.
     *
     * @return list<string> dates au format 'Y-m-d'
     */
    private function resolveSolidarityDatesInRange(DateTimeImmutable $from, DateTimeImmutable $to): array
    {
        $dates     = [];
        $firstYear = (int) $from->format('Y');
        $lastYear  = (int) $to->format('Y');

        for ($year = $firstYear; $year <= $lastYear; ++$year) {
            $candidate = $this->solidarityDayResolver->pentecostMonday($year);
            if ($candidate >= $from && $candidate <= $to) {
                $dates[] = $candidate->format('Y-m-d');
            }
        }

        return $dates;
    }

    /**
     * Déficit forfaitaire du jour de solidarité pour les contrats CUSTOM < 35h.
     *
     * Le jour est neutralisé puis chargé du prorata légal : on remplace la valeur réelle
     * du jour ($workedMinutes : RTT posé, heures saisies, vide, ou crédit férié virtuel)
     * par l'attendu contractuel du jour ($expectedMinutes = workDaysHours), puis on
     * retranche le prorata = 7h/35h × heuresHebdo = 12 min par heure hebdo. Sur une
     * semaine par ailleurs normale, le net vaut exactement −prorata. Renvoie le delta à
     * ajouter à weeklyOvertimeTotalMinutes (0 hors périmètre : non-CUSTOM ou ≥ 35h).
     */
    private function computeSolidarityDeficitAdjustment(
        ?Contract $contractAtSolidarity,
        int $expectedMinutes,
        int $workedMinutes,
    ): int {
        $weeklyHours = $contractAtSolidarity?->getWeeklyHours();
        $type        = ContractType::resolve(
            $contractAtSolidarity?->getName(),
            $contractAtSolidarity?->getTrackingMode(),
            $weeklyHours,
        );

        if (ContractType::CUSTOM !== $type || null === $weeklyHours || $weeklyHours >= 35) {
            return 0;
        }

        $prorata = (int) round($weeklyHours * 12);

        return ($expectedMinutes - $workedMinutes) - $prorata;
    }
```

- [ ] **Step 5: Wire it into `computeRecoveryByWeek`**

(a) Just before the weeks loop, after `$results = [];` (≈ line 165), resolve the solidarity dates once:

```php
        $results         = [];
        $solidarityDates = $this->resolveSolidarityDatesInRange($periodFrom, $periodTo);
```

(b) Inside the week loop, immediately after `$weeklyOvertimeTotalMinutes = ...` is computed (the `$isWeekPresenceTracking ? 0 : $weeklyTotalMinutes - $overtimeReferenceMinutes;` assignment, ≈ line 243-245) and BEFORE the `[$rawBase25, $rawBase50] = ...` line, insert:

```php
            foreach ($solidarityDates as $solidarityDate) {
                // isset ⇒ le jour de solidarité fait partie du sommage de CETTE semaine
                // (donc ≤ limitDate et ≥ rttStartDate). Sinon : jour futur ou hors service → pas de déficit.
                if (!isset($dailyWorkedMinutes[$solidarityDate])) {
                    continue;
                }

                $contractAtSolidarity = $employeeContractsByDate[$solidarityDate] ?? null;
                $solidarityIsoDay     = (int) new DateTimeImmutable($solidarityDate)->format('N');
                // Attendu RÉEL du jour (planning workDaysHours), pas la répartition uniforme :
                // c'est ce qui rend la neutralisation correcte (cf. spec).
                $solidarityExpected = $this->dailyReferenceResolver->resolve(
                    $contractAtSolidarity?->getWeeklyHours(),
                    $solidarityIsoDay,
                    $workDaysByDate[$employeeId][$solidarityDate] ?? null,
                );

                $weeklyOvertimeTotalMinutes += $this->computeSolidarityDeficitAdjustment(
                    $contractAtSolidarity,
                    $solidarityExpected,
                    $dailyWorkedMinutes[$solidarityDate],
                );
            }
```

- [ ] **Step 6: Run tests to verify they pass**

Run: `make test`
Expected: PASS — les 7 nouveaux tests verts, toute la suite verte (le pre-commit relance aussi la suite complète).

- [ ] **Step 7: Verify the service container still builds (autowiring)**

Run: `docker exec php-sirh-fpm php bin/console debug:container App\\Service\\Rtt\\RttRecoveryComputationService 2>&1 | tail -20`
Expected: le service est listé, sans erreur d'argument non résolu (`SolidarityDayResolver` autowiré). Si erreur d'autowiring : ajouter explicitement l'argument dans `config/services.yaml` sous `RttRecoveryComputationService` — mais normalement inutile.

- [ ] **Step 8: Commit**

```bash
git add src/Service/Rtt/RttRecoveryComputationService.php tests/Service/Rtt/RttRecoveryComputationServiceTest.php
git commit -m "feat(rtt) : solidarity-day deficit for CUSTOM <35h contracts"
```

---

## Task 3: Vérification sur données de production

**Files:** aucun fichier de code. Génère des snapshots « after » et compare.

Contexte : le workflow before/after existe déjà (`docs/verifications/` = avant, `docs/verifications-after/` = après). La commande `app:verification:snapshot` rend la vue onglet RTT par mois.

- [ ] **Step 1: Generate the "after" snapshot for the witnesses + a control**

Ewa (id 31, CUSTOM 4h), Nadia (id 22, CUSTOM 4h), et un témoin 35h ou 39h (choisir un id présent — vérifier en base) pour prouver la non-régression.

Run:
```bash
docker exec php-sirh-fpm php bin/console app:verification:snapshot 31 22 --rtt-year=2026 --output-dir=docs/verifications-after
```
Expected: génère les fichiers Markdown sans erreur.

- [ ] **Step 2: Inspect Ewa's solidarity week (S22, semaine du 25/05/2026)**

Ouvrir le snapshot d'Ewa (`docs/verifications-after/…`) et vérifier :
- Semaine du 2026-05-25 : **Heure** et **Total** = −0h48 (−48 min), **Cumul** réduit de 48 min.
- Colonnes 25 % / 50 % = 0 sur cette semaine.
- La semaine du 2026-06-01 (lundi 1er juin) conserve son −2h existant, distinct.

Si l'écart ne vaut pas −48 min : NE PAS « ajuster jusqu'à ce que ça passe ». Relire `computeSolidarityDeficitAdjustment` et la valeur `expectedMinutes` (doit valoir `workDaysHours[lundi]`, ex. 120) — un écart signale un bug réel (utiliser systematic-debugging).

- [ ] **Step 3: Confirm non-regression for a standard contract**

Snapshot d'un employé 35h/39h ayant un RTT posé sur le 25/05/2026 : la semaine doit être **inchangée** vs `docs/verifications/` (le déficit solidarité ne s'applique pas, le RTT posé garde son effet).

- [ ] **Step 4: Commit the after-snapshots (regression baseline)**

```bash
git add docs/verifications-after
git commit -m "test(rtt) : after-snapshot proving solidarity deficit on Ewa/Nadia S22"
```

---

## Task 4: Documentation (règle projet obligatoire)

**Files:**
- Modify: `CLAUDE.md`
- Modify: `doc/functional-rules.md`
- Modify: `doc/rtt-tab.md`
- Modify: `frontend/data/documentation-content.ts`

- [ ] **Step 1: CLAUDE.md — section Overtime Rules**

Sous la puce CUSTOM existante (« CUSTOM contracts … Le déficit … réduit le cumul RTT 1:1 »), ajouter une sous-puce :

```markdown
  - **Jour de solidarité (Lundi de Pentecôte) — CUSTOM < 35h** : le jour est neutralisé et chargé d'un déficit forfaitaire `7/35 × weeklyHours` = **12 min par heure hebdo** (4h→48 min, 25h→5h, 28h→5h36), retranché du cumul RTT (signé, reporté N+1, ne draine pas les tranches 25/50). Net = exactement −prorata quel que soit ce qui est posé ce jour-là (RTT, heures, vide) → pas de double comptage avec le RTT que la RH pose aussi sur ce jour. Hors périmètre : 35h/39h/Forfait/Intérim et CUSTOM ≥ 35h (inchangés ; la RH pose un RTT qui draine ~7h). Date via `App\Service\Rtt\SolidarityDayResolver` (computus, indépendant d'`EXCLUDED_PUBLIC_HOLIDAYS`). Appliqué dans `RttRecoveryComputationService::{resolveSolidarityDatesInRange, computeSolidarityDeficitAdjustment}`.
```

- [ ] **Step 2: doc/functional-rules.md**

Dans la section RTT / heures supplémentaires (près des règles CUSTOM), ajouter un paragraphe :

```markdown
### Jour de solidarité (contrats CUSTOM < 35h)

Le Lundi de Pentecôte (jour de solidarité) impose une contribution proratisée aux temps
partiels < 35h. La RH pose un RTT sur ce jour pour tous les salariés ; pour les contrats
standard (35h/39h) cela draine ~7h du cumul RTT (comportement inchangé). Pour les CUSTOM
< 35h, poser un RTT entier n'a pas de sens : le logiciel **neutralise** le jour (quel que
soit ce qui y est saisi) et applique un déficit forfaitaire `7/35 × heuresHebdo`
(= 12 min par heure hebdo : 4h → 48 min, 28h → 5h36). Ce déficit réduit le cumul RTT
(peut le rendre négatif, reporté à l'exercice suivant) et se cumule avec les autres
déficits/surplus de la semaine. Date calculée par computus (Pâques + 50 jours),
indépendante de la liste `EXCLUDED_PUBLIC_HOLIDAYS`.
```

- [ ] **Step 3: doc/rtt-tab.md**

Dans la section « Règle de calcul — contrats CUSTOM », ajouter un sous-bloc :

```markdown
#### Jour de solidarité (CUSTOM < 35h)

Sur la semaine du Lundi de Pentecôte, un contrat CUSTOM < 35h porte un déficit
forfaitaire de `7/35 × heuresHebdo` (12 min/h hebdo, ex. 4h → −0h48) dans les colonnes
Heure / Total / Cumul (25 %/50 % restent à 0). Le montant est fixe et inconditionnel :
il ne dépend pas des heures saisies ni du RTT que la RH pose ce jour-là. Les contrats
35h/39h ne sont pas concernés ici (leur RTT posé draine le cumul normalement).
```

- [ ] **Step 4: frontend/data/documentation-content.ts**

Repérer l'article RTT pour les contrats partiels / CUSTOM (recherche `CUSTOM` ou `rtt-compteurs`). Ajouter un bloc de texte (échapper les apostrophes `\'`) décrivant la règle :

```
Jour de solidarité : pour un contrat de moins de 35h, le Lundi de Pentecôte applique un
déficit fixe proportionnel (7/35 des heures hebdo, soit 12 minutes par heure
hebdomadaire : 4h → 48 min). Ce déficit réduit le cumul RTT, peu importe ce qui est saisi
ce jour-là.
```

Respecter la structure `DocBlock` existante (même type de bloc que les paragraphes voisins).

- [ ] **Step 5: Run the suite and commit**

Run: `make test`
Expected: PASS (les docs ne cassent rien ; la suite reste verte).

```bash
git add CLAUDE.md doc/functional-rules.md doc/rtt-tab.md frontend/data/documentation-content.ts
git commit -m "docs(rtt) : document solidarity-day deficit for CUSTOM <35h"
```

---

## Self-review (rempli pendant la rédaction)

- **Couverture spec** : SolidarityDayResolver (T1) ✓ ; injection + neutralisation + prorata dans computeRecoveryByWeek (T2) ✓ ; périmètre CUSTOM < 35h + garde ≥ 35h (T2, `computeSolidarityDeficitAdjustment`) ✓ ; robustesse limitDate/rttStartDate via `isset($dailyWorkedMinutes)` (T2 step 5) ✓ ; contrat lu au jour de solidarité (T2 step 5) ✓ ; propagation clôture/rollover/snapshot via totalMinutes (inchangé, vérifié T3) ✓ ; cas limites (T2 tests + T3) ✓ ; docs (T4) ✓.
- **Pas de placeholder** : tout le code est fourni.
- **Cohérence des noms** : `pentecostMonday`, `resolveSolidarityDatesInRange`, `computeSolidarityDeficitAdjustment`, `$solidarityDayResolver`, `$dailyReferenceResolver`, `$workDaysByDate`, `$employeeId`, `$dailyWorkedMinutes`, `$employeeContractsByDate` — alignés avec le code existant de `computeRecoveryByWeek`.
