From 066ed7746f69a829dfcaa6ba02c2f1f5af1a6c89 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 17:10:10 +0200 Subject: [PATCH] fix(rtt) : anchor week contract type on first contracted day MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mid-week hire weeks (e.g. CDD starting Thursday) had their first day with no contract, so the week was classified CUSTOM and the 25%/50% overtime bonuses were disabled. Anchor the week's contract type/nature on the first contracted day instead, so a 35h/39h hire week keeps its overtime tiers. Dylan CHABOISSON week 12: 7h → 8h45 (7h × 1.25). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Rtt/RttRecoveryComputationService.php | 27 ++++++- .../Rtt/RttRecoveryComputationServiceTest.php | 74 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/Service/Rtt/RttRecoveryComputationServiceTest.php diff --git a/src/Service/Rtt/RttRecoveryComputationService.php b/src/Service/Rtt/RttRecoveryComputationService.php index ebd4b5b..766945e 100644 --- a/src/Service/Rtt/RttRecoveryComputationService.php +++ b/src/Service/Rtt/RttRecoveryComputationService.php @@ -221,8 +221,9 @@ final readonly class RttRecoveryComputationService continue; } - $weekAnchorNature = $naturesByDate[$employeeId][$weekDays[0]] ?? ContractNature::CDI; - $weekAnchorContract = $employeeContractsByDate[$weekDays[0]] ?? null; + $weekAnchorDate = $this->resolveWeekAnchorDate($weekDays, $employeeContractsByDate); + $weekAnchorNature = $naturesByDate[$employeeId][$weekAnchorDate] ?? ContractNature::CDI; + $weekAnchorContract = $employeeContractsByDate[$weekAnchorDate] ?? null; $isWeekPresenceTracking = TrackingMode::PRESENCE->value === $weekAnchorContract?->getTrackingMode(); $disableOvertimeBonuses = $this->hasDisabledOvertimeBonuses($weekAnchorContract, $weekAnchorNature); $weekContractType = ContractType::resolve( @@ -388,9 +389,29 @@ final readonly class RttRecoveryComputationService } /** - * @param list $days * @param array $contractsByDate */ + /** + * Date d'ancrage de la semaine pour résoudre le type/nature de contrat : premier jour + * de la semaine couvert par un contrat. Évite qu'une semaine d'embauche en milieu de + * semaine (premiers jours hors contrat) soit classée CUSTOM — ce qui désactiverait à + * tort les bonus 25 %/50 % d'un contrat 35h/39h. Fallback sur le 1er jour si aucun jour + * n'est contracté (semaine entièrement hors contrat → 0 de toute façon). + * + * @param list $weekDays + * @param array $contractsByDate + */ + private function resolveWeekAnchorDate(array $weekDays, array $contractsByDate): string + { + foreach ($weekDays as $date) { + if (null !== ($contractsByDate[$date] ?? null)) { + return $date; + } + } + + return $weekDays[0]; + } + private function computeWeeklyOvertimeReferenceMinutes(array $days, array $contractsByDate): int { $total = 0; diff --git a/tests/Service/Rtt/RttRecoveryComputationServiceTest.php b/tests/Service/Rtt/RttRecoveryComputationServiceTest.php new file mode 100644 index 0000000..5500e56 --- /dev/null +++ b/tests/Service/Rtt/RttRecoveryComputationServiceTest.php @@ -0,0 +1,74 @@ +newInstanceWithoutConstructor(); + $contract = new Contract(); + + $weekDays = ['2026-03-16', '2026-03-17', '2026-03-18', '2026-03-19', '2026-03-20', '2026-03-21', '2026-03-22']; + $contractsByDate = [ + '2026-03-16' => null, + '2026-03-17' => null, + '2026-03-18' => null, + '2026-03-19' => $contract, + '2026-03-20' => $contract, + '2026-03-21' => $contract, + '2026-03-22' => $contract, + ]; + + $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate); + + self::assertSame('2026-03-19', $anchor); + } + + public function testResolveWeekAnchorDateReturnsFirstDayWhenItIsContracted(): void + { + $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor(); + $contract = new Contract(); + + $weekDays = ['2026-03-23', '2026-03-24', '2026-03-25']; + $contractsByDate = [ + '2026-03-23' => $contract, + '2026-03-24' => $contract, + '2026-03-25' => $contract, + ]; + + $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate); + + self::assertSame('2026-03-23', $anchor); + } + + public function testResolveWeekAnchorDateFallsBackToFirstDayWhenNoContract(): void + { + $service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor(); + + $weekDays = ['2026-03-16', '2026-03-17']; + $contractsByDate = ['2026-03-16' => null, '2026-03-17' => null]; + + $anchor = $this->invokePrivate($service, 'resolveWeekAnchorDate', $weekDays, $contractsByDate); + + self::assertSame('2026-03-16', $anchor); + } + + private function invokePrivate(object $obj, string $method, mixed ...$args): mixed + { + return new ReflectionClass($obj::class)->getMethod($method)->invoke($obj, ...$args); + } +}