From 2718d05cfe3c0a2fc29d21e42b2241af625f09c3 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 16:04:00 +0200 Subject: [PATCH] feat(leave) : add prorated forfait repo days helper Co-Authored-By: Claude Opus 4.7 (1M context) --- src/State/EmployeeLeaveSummaryProvider.php | 18 +++++++++ .../EmployeeLeaveSummaryProviderTest.php | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index f4c3896..ea92f04 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -38,6 +38,7 @@ use Throwable; final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface { private const int FORFAIT_TARGET_WORKED_DAYS = 218; + private const int FORFAIT_STANDARD_CP_DAYS = 25; private const float CDI_NON_FORFAIT_STANDARD_ACQUIRED_DAYS = 25.0; private const float CDI_NON_FORFAIT_STANDARD_ACQUIRED_SATURDAYS = 5.0; private const float CDI_NON_FORFAIT_STANDARD_ACCRUAL_PER_MONTH = self::CDI_NON_FORFAIT_STANDARD_ACQUIRED_DAYS / 12.0; @@ -768,6 +769,23 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface ]; } + /** + * Jours de repos forfait proratisés sur la fraction de jours ouvrés couverte. + * + * Repos année pleine = jours_ouvrés_année − 218 (cible travaillée) − 25 (CP standard). + * Pour 2026 : 252 − 218 − 25 = 9, proratisés au ratio jours_ouvrés_période / jours_ouvrés_année. + */ + private function computeProratedForfaitRepoDays(int $businessDaysYear, int $businessDaysPeriod): float + { + if ($businessDaysYear <= 0) { + return 0.0; + } + + $repoDaysYear = max(0, $businessDaysYear - self::FORFAIT_TARGET_WORKED_DAYS - self::FORFAIT_STANDARD_CP_DAYS); + + return $repoDaysYear * $businessDaysPeriod / $businessDaysYear; + } + /** * @param null|array $publicHolidays pre-built map (built if null) */ diff --git a/tests/State/EmployeeLeaveSummaryProviderTest.php b/tests/State/EmployeeLeaveSummaryProviderTest.php index 4360828..69007ee 100644 --- a/tests/State/EmployeeLeaveSummaryProviderTest.php +++ b/tests/State/EmployeeLeaveSummaryProviderTest.php @@ -86,6 +86,44 @@ final class EmployeeLeaveSummaryProviderTest extends TestCase self::assertEqualsWithDelta(25.0 / 12.0, $result, 0.0001); } + public function testComputeProratedForfaitRepoDaysGregoryCase(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // 2026 : 252 jours ouvrés/an, 168 sur la période 01/05→31/12. + // repos année = 252 - 218 - 25 = 9 ; proratisé = 9 × 168/252 = 6.0 + $result = $this->invokePrivate($provider, 'computeProratedForfaitRepoDays', 252, 168); + + self::assertEqualsWithDelta(6.0, $result, 0.001); + } + + public function testComputeProratedForfaitRepoDaysFullYearEquals9(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // Année pleine : 9 × 252/252 = 9.0 + $result = $this->invokePrivate($provider, 'computeProratedForfaitRepoDays', 252, 252); + + self::assertEqualsWithDelta(9.0, $result, 0.001); + } + + public function testComputeProratedForfaitRepoDaysClampsNegativeToZero(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + // Année avec trop peu de jours ouvrés (240 - 218 - 25 < 0) → 0 + $result = $this->invokePrivate($provider, 'computeProratedForfaitRepoDays', 240, 160); + + self::assertSame(0.0, $result); + } + + public function testComputeProratedForfaitRepoDaysZeroYearGuard(): void + { + $provider = new ReflectionClass(EmployeeLeaveSummaryProvider::class)->newInstanceWithoutConstructor(); + + self::assertSame(0.0, $this->invokePrivate($provider, 'computeProratedForfaitRepoDays', 0, 0)); + } + // ----------------------------------------------------------------------- // Phase resolution tests (Task 3 — phaseId support). // The repository / service dependencies are typed against final classes