Files
SIRH/tests/State/SalaryRecapPrintProviderTest.php
T
tristan 94cf8eb7a9 [#SIRH] Récap salaire: exclure les salariés sans contrat sur le mois
Le récap listait tous les employés sans filtrer le contrat: un salarié au
contrat terminé (ex. Marine, fin 26/02) apparaissait sur le récap de juin.
Ajout du filtre hasContractInRange (même règle que l'impression absences) sur
la période [from, to] du mois imprimé.

4 tests ajoutés. Vérifié sur données prod (Marine + 6 autres contrats terminés
exclus du mois de juin, 39 salariés contractés conservés).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 08:10:54 +02:00

146 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\State;
use App\Entity\Absence;
use App\Entity\Employee;
use App\Entity\EmployeeContractPeriod;
use App\Enum\HalfDay;
use App\Service\WorkHours\AbsenceSegmentsResolver;
use App\State\SalaryRecapPrintProvider;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionProperty;
/**
* Forfait N-1 split for the salary recap. The provider's collaborators are final classes
* PHPUnit cannot double, so the pure split helper is exercised via reflection, with a real
* AbsenceSegmentsResolver (no deps) injected into the uninitialized property.
*
* @internal
*/
final class SalaryRecapPrintProviderTest extends TestCase
{
public function testN1BudgetPartiallyCoversADayAndOverflowsToN(): void
{
// Budget N-1 = 2.5 j ; 3 congés pleins (1 j) lun/mar/mer de janvier.
// 1.0 + 1.0 + 0.5 consommés en N-1 → reste 0.5 j affiché en congé (le mercredi).
$conges = [
$this->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<Absence> $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)
;
}
}