employeeWithPeriod(39, '2020-01-01', null); $months = $calc->monthlyStructuralMinutes($employee, 2026); // (39 - 35) x 260 = 1040 minutes (17,33 h) chaque mois plein. self::assertSame(1040, $months[1]); self::assertSame(1040, $months[6]); self::assertSame(1040, $months[12]); self::assertSame(12 * 1040, $calc->totalStructuralMinutes($employee, 2026)); } public function testCustomAbove35hUsesGeneralizedFormula(): void { $calc = new StructuralOvertimeContingentCalculator(); $employee = $this->employeeWithPeriod(40, '2020-01-01', null); // (40 - 35) x 260 = 1300 minutes par mois. self::assertSame(1300, $calc->monthlyStructuralMinutes($employee, 2026)[1]); } public function test35hAndBelowCreditNothing(): void { $calc = new StructuralOvertimeContingentCalculator(); self::assertSame(0, $calc->totalStructuralMinutes($this->employeeWithPeriod(35, '2020-01-01', null), 2026)); self::assertSame(0, $calc->totalStructuralMinutes($this->employeeWithPeriod(28, '2020-01-01', null), 2026)); } public function testMidMonthEntryIsProratedByContractedDays(): void { $calc = new StructuralOvertimeContingentCalculator(); // Embauche le 16 janvier 2026 : 16 jours contractés sur 31. $employee = $this->employeeWithPeriod(39, '2026-01-16', null); $months = $calc->monthlyStructuralMinutes($employee, 2026); self::assertSame((int) round(1040 * 16 / 31), $months[1]); self::assertSame(1040, $months[2]); } public function testMonthsOutsidePeriodCreditNothing(): void { $calc = new StructuralOvertimeContingentCalculator(); // Contrat clos fin mars 2026. $employee = $this->employeeWithPeriod(39, '2020-01-01', '2026-03-31'); $months = $calc->monthlyStructuralMinutes($employee, 2026); self::assertSame(1040, $months[3]); self::assertSame(0, $months[4]); self::assertSame(0, $months[12]); } public function testForfaitPeriodCreditsNothing(): void { $calc = new StructuralOvertimeContingentCalculator(); $contract = new Contract() ->setName('Forfait') ->setTrackingMode(TrackingMode::PRESENCE) ->setWeeklyHours(null) ; $period = new EmployeeContractPeriod() ->setContract($contract) ->setStartDate(new DateTimeImmutable('2020-01-01')) ; $employee = new Employee(); $employee->getContractPeriods()->add($period); self::assertSame(0, $calc->totalStructuralMinutes($employee, 2026)); } public function testInterimAbove35hCreditsNothing(): void { $calc = new StructuralOvertimeContingentCalculator(); $contract = new Contract() ->setName('Interim') ->setTrackingMode(TrackingMode::TIME) ->setWeeklyHours(39) ; $period = new EmployeeContractPeriod() ->setContract($contract) ->setStartDate(new DateTimeImmutable('2020-01-01')) ; $employee = new Employee(); $employee->getContractPeriods()->add($period); self::assertSame(0, $calc->totalStructuralMinutes($employee, 2026)); } private function employeeWithPeriod(int $weeklyHours, string $start, ?string $end): Employee { $contract = new Contract() ->setName('CDI') ->setTrackingMode(TrackingMode::TIME) ->setWeeklyHours($weeklyHours) ; $period = new EmployeeContractPeriod() ->setContract($contract) ->setStartDate(new DateTimeImmutable($start)) ->setEndDate(null === $end ? null : new DateTimeImmutable($end)) ; $employee = new Employee(); $employee->getContractPeriods()->add($period); return $employee; } }