Files
SIRH/tests/State/EmployeeRttPaymentProcessorTest.php

168 lines
6.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\State;
use App\Entity\Contract;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Enum\ContractNature;
use App\Enum\TrackingMode;
use App\Service\Contracts\EmployeeContractPhaseResolver;
use App\State\EmployeeRttPaymentProcessor;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Psr\Clock\ClockInterface;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* @internal
*
* Exercises the year-acceptance guard of EmployeeRttPaymentProcessor.
*
* The processor depends on final repositories (EmployeeRepository,
* EmployeeRttPaymentRepository) which PHPUnit cannot double. The guard logic
* lives in a private helper (assertYearAllowedForPayment) tested directly via
* reflection — same pattern used in EmployeeRttSummaryProviderTest.
*/
final class EmployeeRttPaymentProcessorTest extends TestCase
{
public function testPaymentAllowedOnCurrentExercise(): void
{
// Today = 2026-05-19 (env clock) → current exercise = 2026.
$employee = $this->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);
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);
}
}