buildResolverStub(), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 35, countAsWorked: true); $minutes = $policy->computeCreditedMinutes($absence, '2026-02-16', true, false); self::assertSame(210, $minutes); } public function testComputeCreditedMinutesFor4hContractUsesWorkDaysSchedule(): void { // 4h contract with schedule Mon 2h + Thu 2h $schedule = [1 => 120, 4 => 120]; $policy = new WorkedHoursCreditPolicy($this->buildResolverStub($schedule), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 4, countAsWorked: true); // 2026-02-16 is a Monday: full day absence credits 2h (matches scheduled Monday) self::assertSame(120, $policy->computeCreditedMinutes($absence, '2026-02-16', true, true)); } public function testComputeCreditedMinutesFor4hContractOnUnscheduledDayReturnsZero(): void { // 4h contract with schedule Mon 2h + Thu 2h $schedule = [1 => 120, 4 => 120]; $policy = new WorkedHoursCreditPolicy($this->buildResolverStub($schedule), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence( trackMode: Contract::TRACKING_TIME, weeklyHours: 4, countAsWorked: true, start: '2026-02-17', end: '2026-02-17', ); // 2026-02-17 is a Tuesday — not a scheduled workday → 0 credit self::assertSame(0, $policy->computeCreditedMinutes($absence, '2026-02-17', true, true)); } public function testComputeCreditedMinutesHalfDayOnAsymmetricScheduleDay(): void { // Asymmetric schedule: Monday is a 3h day (180 min) $schedule = [1 => 180]; $policy = new WorkedHoursCreditPolicy($this->buildResolverStub($schedule), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 3, countAsWorked: true); // 2026-02-16 Monday, morning only → round(180/2 * 1) = 90 min self::assertSame(90, $policy->computeCreditedMinutes($absence, '2026-02-16', true, false)); // Afternoon only → same 90 min (half of day) self::assertSame(90, $policy->computeCreditedMinutes($absence, '2026-02-16', false, true)); // Full day → 180 min self::assertSame(180, $policy->computeCreditedMinutes($absence, '2026-02-16', true, true)); } public function testComputeCreditedPresenceUnitsForPresenceContract(): void { $policy = new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence(trackMode: Contract::TRACKING_PRESENCE, weeklyHours: null, countAsWorked: true); // Forfait : les absences ne créditent jamais de présence, seules les checkboxes comptent. self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, false)); self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, true)); } public function testNoCreditWhenAbsenceTypeDoesNotCount(): void { $policy = new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()); $absence = $this->buildAbsence(trackMode: Contract::TRACKING_TIME, weeklyHours: 35, countAsWorked: false); self::assertSame(0, $policy->computeCreditedMinutes($absence, '2026-02-16', true, true)); self::assertSame(0.0, $policy->computeCreditedPresenceUnits($absence, '2026-02-16', true, true)); } private function buildAbsence( string $trackMode, ?int $weeklyHours, bool $countAsWorked, string $start = '2026-02-16', string $end = '2026-02-16', ): Absence { $contract = new Contract() ->setName('Contrat test') ->setTrackingMode($trackMode) ->setWeeklyHours($weeklyHours) ; $employee = new Employee() ->setFirstName('Alice') ->setLastName('Durand') ->setContract($contract) ; $type = new AbsenceType() ->setCode('CP') ->setLabel('Congés') ->setColor('#000') ->setCountAsWorkedHours($countAsWorked) ; return new Absence() ->setEmployee($employee) ->setType($type) ->setStartDate(new DateTime($start)) ->setEndDate(new DateTime($end)) ; } /** * @param null|array $schedule */ private function buildResolverStub(?array $schedule = null): EmployeeContractResolver { $resolver = $this->createStub(EmployeeContractResolver::class); $resolver ->method('resolveForEmployeeAndDate') ->willReturnCallback(static fn (Employee $employee): ?Contract => $employee->getContract()) ; $resolver ->method('resolveWorkDaysMinutesForEmployeeAndDate') ->willReturn($schedule) ; return $resolver; } }