buildConge('2026-01-05'), $this->buildConge('2026-01-06'), $this->buildConge('2026-01-07'), ]; $result = $this->split($conges, 2.5, '2026-01-01', '2026-01-31'); self::assertSame(2.5, $result['n1PresenceDays']); self::assertSame(0.5, $result['count']); self::assertSame('07/01', $result['dates']); } public function testN1BudgetConsumedInPriorMonthLeavesCurrentMonthFullyDisplayed(): void { // Budget 1 j, consommé par le congé de janvier. Récap de février → le congé de février // est entièrement imputé N (affiché, 0 présence N-1 dans le mois). $conges = [ $this->buildConge('2026-01-12'), $this->buildConge('2026-02-09'), ]; $result = $this->split($conges, 1.0, '2026-02-01', '2026-02-28'); self::assertSame(0.0, $result['n1PresenceDays']); self::assertSame(1.0, $result['count']); self::assertSame('09/02', $result['dates']); } public function testZeroBudgetDisplaysAllCongesInMonth(): void { $conges = [$this->buildConge('2026-03-03')]; $result = $this->split($conges, 0.0, '2026-03-01', '2026-03-31'); self::assertSame(0.0, $result['n1PresenceDays']); self::assertSame(1.0, $result['count']); self::assertSame('03/03', $result['dates']); } public function testTerminatedContractExcludedFromMonth(): void { // Marine : contrat terminé le 26/02 → absente du récap de juin. $employee = $this->buildEmployeeWithPeriod('2025-02-10', '2026-02-26'); self::assertFalse($this->hasInRange($employee, '2026-06-01', '2026-06-30')); } public function testOngoingContractIncluded(): void { $employee = $this->buildEmployeeWithPeriod('2025-01-01', null); self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30')); } public function testContractEndingOnFromDayIncluded(): void { $employee = $this->buildEmployeeWithPeriod('2025-01-01', '2026-06-01'); self::assertTrue($this->hasInRange($employee, '2026-06-01', '2026-06-30')); } public function testNoPeriodsExcluded(): void { self::assertFalse($this->hasInRange(new Employee(), '2026-06-01', '2026-06-30')); } private function hasInRange(Employee $employee, string $from, string $to): bool { $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor(); return new ReflectionClass($provider::class) ->getMethod('hasContractInRange') ->invoke($provider, $employee, new DateTimeImmutable($from), new DateTimeImmutable($to)); } private function buildEmployeeWithPeriod(string $start, ?string $end): Employee { $employee = new Employee(); $period = new EmployeeContractPeriod(); $period->setEmployee($employee); $period->setStartDate(new DateTimeImmutable($start)); $period->setEndDate(null !== $end ? new DateTimeImmutable($end) : null); $employee->getContractPeriods()->add($period); return $employee; } /** * @param list $conges * * @return array{count: float, dates: string, n1PresenceDays: float} */ private function split(array $conges, float $budget, string $from, string $to): array { $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor(); new ReflectionProperty(SalaryRecapPrintProvider::class, 'absenceSegmentsResolver') ->setValue($provider, new AbsenceSegmentsResolver()); return new ReflectionClass($provider::class) ->getMethod('splitForfaitCongesByN1') ->invoke($provider, $conges, $budget, new DateTimeImmutable($from), new DateTimeImmutable($to)); } private function buildConge(string $date): Absence { return new Absence() ->setStartDate(new DateTime($date)) ->setEndDate(new DateTime($date)) ->setStartHalf(HalfDay::AM) ->setEndHalf(HalfDay::PM) ; } }