diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index 828363a..bed16d3 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -134,11 +134,18 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year, $phase); - // Forfait : jours à travailler sur l'exercice = jours ouvrés de la période − congés acquis. - // Année pleine → 218 (252 − 34) ; entrée en cours d'année → prorata (ex. 168 − 13 ≈ 155). + // Forfait : jours à travailler sur l'exercice. + // Année pleine → cible contractuelle 218 ; les bonus week-end/férié et les jours + // fractionnés sont des congés EN PLUS, ils ne réduisent pas la cible. Entrée en cours + // d'année → jours ouvrés de la période − congés acquis de l'entrée (repos proratisés + + // CP reportés), via yearSummary['acquiredDays'] (hors fractionnés/bonus). Ex. Grégory : 168 − 13 ≈ 155. if (LeaveRuleCode::FORFAIT_218->value === $summary->ruleCode) { $businessDaysInPeriod = $this->countBusinessDays($periodFrom, $periodTo, $this->buildRawPublicHolidayMap($periodFrom, $periodTo)); - $summary->forfaitWorkTargetDays = $businessDaysInPeriod - $summary->acquiredDays; + $summary->forfaitWorkTargetDays = $this->computeForfaitWorkTargetDays( + $businessDaysInPeriod, + $this->isForfaitEntryYear($phase, $year), + $yearSummary['acquiredDays'], + ); } // Forfait-only: leaves taken from N-1 stock do NOT decrement presence days. @@ -890,6 +897,24 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface return $repoDaysYear * $businessDaysPeriod / $businessDaysYear; } + /** + * Jours à travailler d'un forfait sur l'exercice consulté. + * + * - Année pleine : cible contractuelle 218 (bornée aux jours ouvrés de la période si + * celle-ci en compte moins). Les bonus week-end/férié et jours fractionnés sont des + * congés EN PLUS et ne réduisent pas la cible. + * - Entrée en cours d'année : jours ouvrés de la période − congés acquis de l'entrée + * (repos proratisés + CP reportés, hors fractionnés/bonus). Ex. Grégory : 168 − 13 ≈ 155. + */ + private function computeForfaitWorkTargetDays(int $businessDaysInPeriod, bool $isEntryYear, float $entryAcquiredDays): float + { + if ($isEntryYear) { + return $businessDaysInPeriod - $entryAcquiredDays; + } + + return (float) min($businessDaysInPeriod, self::FORFAIT_TARGET_WORKED_DAYS); + } + /** * Vrai si la phase FORFAIT démarre en cours de l'année civile consultée * (donc avec une période partielle), faux pour une année pleine ou un démarrage le 1er janvier. diff --git a/tests/State/EmployeeLeaveSummaryProviderTest.php b/tests/State/EmployeeLeaveSummaryProviderTest.php index 6d4fbb0..893f559 100644 --- a/tests/State/EmployeeLeaveSummaryProviderTest.php +++ b/tests/State/EmployeeLeaveSummaryProviderTest.php @@ -86,6 +86,34 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase self::assertEqualsWithDelta(25.0 / 12.0, $result, 0.0001); } + public function testComputeForfaitWorkTargetDaysEntryYearProrates(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // Grégory : 168 jours ouvrés sur la période − 12.94 congés acquis (repos + CP reportés) ≈ 155.06 + $result = $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 168, true, 12.94); + + self::assertEqualsWithDelta(155.06, $result, 0.001); + } + + public function testComputeForfaitWorkTargetDaysFullYearIs218IgnoringBonusAndFractioned(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // Année pleine : la cible reste 218 quelle que soit la valeur des congés acquis + // (les bonus week-end/férié et jours fractionnés ne réduisent pas la cible). + self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 34.0)); + self::assertSame(218.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 252, false, 40.0)); + } + + public function testComputeForfaitWorkTargetDaysFullYearCapsAtBusinessDaysWhenFewer(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // Période avec moins de 218 jours ouvrés (phase forfait clôturée en cours d'année) → cap aux jours ouvrés. + self::assertSame(200.0, $this->invokePrivate($provider, 'computeForfaitWorkTargetDays', 200, false, 5.0)); + } + public function testComputeProratedForfaitRepoDaysGregoryCase(): void { $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor();