a41bd632cf
Auto Tag Develop / tag (push) Successful in 11s
## Correctifs RH (branche fix/retour-rh) ### Vue Jour (Heures) - Mode saisie/présence, libellé de contrat et sauvegarde résolus **à la date affichée** (et non au contrat courant). Corrige les salariés passés 39h/35h → Forfait. ### RTT — heures supplémentaires - Proratisation du **plafond 25%/50%** pour les embauches en milieu de semaine (la bande +25% se décale au lieu de rester bloquée à 43h). Témoin Dylan : 4h à 25% + 3h à 50%. ### Récap salaire (PDF mensuel) - Forfait : congés imputés **N-1** non affichés et comptés en présence. - Colonne « Heures payés » **scindée 25% / 50%** (en-tête fusionné). - **Exclusion des salariés sans contrat** sur le mois (ex. Marine, contrat terminé). ### Exports heures annuelles (par salarié + tous) - **Tous les jours sous contrat** affichés, même vides/non saisis (corrige les lignes manquantes). - Samedis/dimanches en **gris plus foncé**. ### Panier de nuit - **Ne s'applique pas aux conducteurs** (vue semaine + récap salaire). ## Tests - 11 tests ajoutés. Suite verte hors un test legacy pré-existant dépendant de la date (`EmployeeRttSummaryProviderTest::testNoQueryParamsKeepsLegacyYearDefaulting`, non modifié par cette branche). ## À noter (hors scope) - L'export heures annuelles *tous salariés* peut dépasser `memory_limit=256M` (Dompdf) — limitation **pré-existante**, non corrigée ici. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #21 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
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)
|
|
;
|
|
}
|
|
}
|