94cf8eb7a9
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>
146 lines
5.1 KiB
PHP
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)
|
|
;
|
|
}
|
|
}
|