Files
SIRH/tests/State/SalaryRecapPrintProviderTest.php
T
tristan c8e7f80c72
Auto Tag Develop / tag (push) Successful in 11s
fix(conges) : un congé posé un dimanche n'est plus décompté (récap salaire)
Le récap salaire comptait les congés (C) tombant un dimanche via
countAbsencesByCode, alors que l'onglet Congés, le rollover et les jours de
présence l'ignoraient déjà. Garde ajoutée (C + dimanche → ignoré) pour aligner :
poser une période à cheval sur un week-end (ex. jeu→mar) ne fait plus perdre le
dimanche. Correctif au comptage uniquement : les lignes d'absence du dimanche
restent créées et affichées sur le calendrier (volonté RH), l'existant cesse de
compter sans migration. Périmètre strict : code C (maladie/AT inchangés), samedi
inchangé (budget dédié).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:03:49 +02:00

212 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\State;
use App\Entity\Absence;
use App\Entity\AbsenceType;
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'));
}
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<Absence> $absences
* @param list<string> $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<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)
;
}
}