createStub(PublicHolidayServiceInterface::class); $holidayService->method('getHolidaysDayByYears')->willReturnCallback( static fn (string $zone, string $year): array => [ // Mon 14/07/2025 (lundi) '2025-07-14' => '14 juillet', // Fri 15/08/2025 (vendredi) '2025-08-15' => '15 août', // Sat 11/11/2025 (samedi) '2025-11-15' => 'Samedi test', // Thu 25/12/2025 '2025-12-25' => 'Noël', ] ); $this->resolver = new HolidayVirtualHoursResolver( new DailyReferenceMinutesResolver(), $holidayService, $this->createStub(EmployeeContractResolver::class), ); } public function testReturnsZeroWhenContractIsNull(): void { self::assertSame(0, $this->resolver->resolveVirtualCredit(null, new DateTimeImmutable('2025-07-14'))); } public function testReturnsZeroForForfaitPresenceContract(): void { $contract = new Contract() ->setName('Forfait') ->setTrackingMode('PRESENCE') ->setWeeklyHours(null) ; self::assertSame(0, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'))); } public function testReturnsZeroWhenDayIsNotHoliday(): void { $contract = $this->build35hContract(); self::assertSame(0, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-07'))); } public function testReturnsZeroWhenHolidayFallsOnSaturday(): void { $contract = $this->build35hContract(); self::assertSame(0, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-11-15'))); } public function test35hMondayGetsSevenHours(): void { $contract = $this->build35hContract(); self::assertSame(7 * 60, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'))); } public function test39hMondayGetsEightHours(): void { $contract = $this->build39hContract(); self::assertSame(8 * 60, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'))); } public function test39hFridayGetsSevenHours(): void { $contract = $this->build39hContract(); self::assertSame(7 * 60, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-08-15'))); } public function testCustomContractUsesProRataReference(): void { $contract = new Contract() ->setName('28h') ->setTrackingMode('TIME') ->setWeeklyHours(28) ; // 28h / 5 = 5.6h = 336 min self::assertSame(336, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'))); } public function testInterimContractAlsoReceivesCredit(): void { $contract = new Contract() ->setName('Interim') ->setTrackingMode('TIME') ->setWeeklyHours(35) ; self::assertSame(7 * 60, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'))); } public function testEffectiveDailyMinutesReturnsActualWhenGreaterThanReference(): void { $contract = $this->build39hContract(); // 10h worked on a férié Monday with 39h contract (ref = 8h) self::assertSame(600, $this->resolver->resolveEffectiveDailyMinutes($contract, new DateTimeImmutable('2025-07-14'), 600)); } public function testEffectiveDailyMinutesReturnsReferenceWhenActualLower(): void { $contract = $this->build39hContract(); // 4h worked on a férié Monday with 39h contract (ref = 8h) → 8h self::assertSame(8 * 60, $this->resolver->resolveEffectiveDailyMinutes($contract, new DateTimeImmutable('2025-07-14'), 240)); } public function testEffectiveDailyMinutesDelegatesWhenRuleDoesNotApply(): void { $contract = $this->build39hContract(); // Non-holiday day: rule does not apply, return actual self::assertSame(420, $this->resolver->resolveEffectiveDailyMinutes($contract, new DateTimeImmutable('2025-07-07'), 420)); } public function testFallsBackGracefullyWhenHolidayServiceFails(): void { $failingService = $this->createStub(PublicHolidayServiceInterface::class); $failingService->method('getHolidaysDayByYears')->willThrowException(new RuntimeException('boom')); $resolver = new HolidayVirtualHoursResolver( new DailyReferenceMinutesResolver(), $failingService, $this->createStub(EmployeeContractResolver::class), ); self::assertSame(0, $resolver->resolveVirtualCredit($this->build35hContract(), new DateTimeImmutable('2025-07-14'))); } public function testScheduledWorkdayGetsCreditOnHoliday(): void { // 4h contract, schedule Mon 2h + Thu 2h $contract = new Contract()->setName('4h')->setTrackingMode('TIME')->setWeeklyHours(4); // Holiday 2025-07-14 is a Monday → 120 min credit self::assertSame(120, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'), false, [1 => 120, 4 => 120])); } public function testUnscheduledWorkdayGetsZeroOnHoliday(): void { $contract = new Contract()->setName('4h')->setTrackingMode('TIME')->setWeeklyHours(4); // Holiday 2025-07-14 is a Monday but schedule only Tue+Fri → 0 self::assertSame(0, $this->resolver->resolveVirtualCredit($contract, new DateTimeImmutable('2025-07-14'), false, [2 => 120, 5 => 120])); } private function build35hContract(): Contract { return new Contract() ->setName('35h') ->setTrackingMode('TIME') ->setWeeklyHours(35) ; } private function build39hContract(): Contract { return new Contract() ->setName('39h') ->setTrackingMode('TIME') ->setWeeklyHours(39) ; } }