Files
SIRH/docs/superpowers/plans/2026-06-11-rtt-solidarity-day-deficit.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

23 KiB
Raw Blame History

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

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

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
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 (1200)48 = +72 ; appliqué au naturel 120 ⇒ semaine 48 min.

  • travaillé normalement (worked = 120) → delta (120120)48 = 48 ; naturel 048 min.

  • travaillé en plus (worked = 240) → delta (120240)48 = 168 ; naturel +12048 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).

    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.

    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):

    /**
     * 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:

        $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:

            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
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:

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)
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 :

  - **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 :

### 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 :

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

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.