buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01'); $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19')); $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2026); // No exception → guard accepts current exercise. self::assertTrue(true); } public function testPaymentAllowedOnLastExerciseOfClosedPhase(): void { // Phase 39h closed 2026-04-30, FORFAIT from 2026-05-01. // Exercise 2026 (Juin 2025 → Mai 2026) contains the H39 phase end date. // Payment must be allowed on exercise 2026 even when current exercise is 2027. $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01'); $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2027-01-15')); $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2026); self::assertTrue(true); } public function testPaymentRejectedOnEarlierExerciseOfClosedPhase(): void { $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01'); $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2027-01-15')); $this->expectException(UnprocessableEntityHttpException::class); $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2024); } public function testPaymentRejectedOnFutureExercise(): void { $employee = $this->buildEmployeeWithTransition('2020-06-01', '2026-04-30', '2026-05-01'); $processor = $this->buildProcessorWithClock(new DateTimeImmutable('2026-05-19')); $this->expectException(UnprocessableEntityHttpException::class); $this->invokePrivate($processor, 'assertYearAllowedForPayment', $employee, 2030); } // ----------------------------------------------------------------------- // Test harness helpers. // ----------------------------------------------------------------------- /** * Build a two-period employee transitioning from H39 to FORFAIT. */ private function buildEmployeeWithTransition(string $h39Start, string $h39End, string $forfaitStart): Employee { $employee = new Employee(); $this->setEntityId($employee, 1); $h39Contract = new Contract(); $h39Contract->setName('39H'); $h39Contract->setTrackingMode(TrackingMode::TIME->value); $h39Contract->setWeeklyHours(39); $forfaitContract = new Contract(); $forfaitContract->setName('Forfait'); $forfaitContract->setTrackingMode(TrackingMode::PRESENCE->value); $forfaitContract->setWeeklyHours(null); $h39Period = new EmployeeContractPeriod(); $this->setEntityId($h39Period, 1); $h39Period->setEmployee($employee); $h39Period->setContract($h39Contract); $h39Period->setStartDate(new DateTimeImmutable($h39Start)); $h39Period->setEndDate(new DateTimeImmutable($h39End)); $h39Period->setContractNature(ContractNature::CDI); $h39Period->setIsDriver(false); $forfaitPeriod = new EmployeeContractPeriod(); $this->setEntityId($forfaitPeriod, 2); $forfaitPeriod->setEmployee($employee); $forfaitPeriod->setContract($forfaitContract); $forfaitPeriod->setStartDate(new DateTimeImmutable($forfaitStart)); $forfaitPeriod->setEndDate(null); $forfaitPeriod->setContractNature(ContractNature::CDI); $forfaitPeriod->setIsDriver(false); $employee->getContractPeriods()->add($h39Period); $employee->getContractPeriods()->add($forfaitPeriod); return $employee; } /** * Build an uninitialized processor with a fixed clock. The repositories are * declared on final classes that PHPUnit cannot double, so we bypass full * instantiation via newInstanceWithoutConstructor and only seed the * properties the tested private guard reads: phaseResolver + clock. */ private function buildProcessorWithClock(DateTimeImmutable $today): EmployeeRttPaymentProcessor { $reflection = new ReflectionClass(EmployeeRttPaymentProcessor::class); $processor = $reflection->newInstanceWithoutConstructor(); $clock = new readonly class($today) implements ClockInterface { public function __construct(private DateTimeImmutable $now) {} public function now(): DateTimeImmutable { return $this->now; } }; $this->setReadonlyProperty($processor, 'phaseResolver', new EmployeeContractPhaseResolver()); $this->setReadonlyProperty($processor, 'clock', $clock); $this->setReadonlyProperty($processor, 'exerciseYearResolver', new ExerciseYearResolver()); return $processor; } private function invokePrivate(object $obj, string $method, mixed ...$args): mixed { $reflection = new ReflectionClass($obj::class); $m = $reflection->getMethod($method); return $m->invoke($obj, ...$args); } private function setReadonlyProperty(object $obj, string $property, mixed $value): void { $reflection = new ReflectionProperty($obj::class, $property); $reflection->setValue($obj, $value); } private function setEntityId(object $entity, int $id): void { $reflection = new ReflectionProperty($entity::class, 'id'); $reflection->setValue($entity, $id); } }