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')); } public function testSundayCongeIsNotCounted(): void { // Congé (C) posé un dimanche (2026-06-07) : ne doit pas compter comme congé pris. $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-07', 'C')], ['C']); self::assertSame(0.0, $result['count']); self::assertSame('', $result['dates']); } public function testSaturdayCongeStillCounted(): void { // Le samedi reste hors périmètre (budget samedis dédié) : congé samedi toujours compté. $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-06', 'C')], ['C']); self::assertSame(1.0, $result['count']); self::assertSame('06/06', $result['dates']); } public function testWeekdayCongeCounted(): void { $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-01', 'C')], ['C']); self::assertSame(1.0, $result['count']); self::assertSame('01/06', $result['dates']); } public function testSundayMaladieStillCounted(): void { // L'exclusion du dimanche ne concerne que les congés (C) : maladie/AT inchangés. $result = $this->countByCode([$this->buildAbsenceWithCode('2026-06-07', 'M')], ['M', 'AT']); self::assertSame(1.0, $result['count']); self::assertSame('07/06', $result['dates']); } /** * @param list $absences * @param list $codes * * @return array{count: float, dates: string} */ private function countByCode(array $absences, array $codes): array { $provider = new ReflectionClass(SalaryRecapPrintProvider::class)->newInstanceWithoutConstructor(); return new ReflectionClass($provider::class) ->getMethod('countAbsencesByCode') ->invoke($provider, $absences, $codes) ; } private function buildAbsenceWithCode(string $date, string $code): Absence { return new Absence() ->setType(new AbsenceType()->setCode($code)->setLabel($code)->setColor('#000')) ->setStartDate(new DateTime($date)) ->setEndDate(new DateTime($date)) ->setStartHalf(HalfDay::AM) ->setEndHalf(HalfDay::PM) ; } 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) ; } }