fix(absence) : déduire les jours pris du report CP au changement de période
Pull Request — Quality gate / Frontend (build) (pull_request) Successful in 1m19s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 1m32s

Au passage d'une période de référence, le report de l'"en cours
d'acquisition" (N) vers l'"acquis" (N-1) ne déduisait pas les jours
déjà pris : un salarié récupérait les CP qu'il avait consommés.

Le report ne porte désormais que les jours non pris. Les congés sont
imputés au plus ancien bucket d'abord (l'acquis N-2, qui expire de toute
façon au changement de période), donc seuls les jours pris au-delà
réduisent le report.

Ajoute AccrueLeaveCommandTest couvrant le report avec jour pris,
l'imputation oldest-first et le report intégral sans jour pris.
This commit is contained in:
Matthieu
2026-06-24 10:52:05 +02:00
parent b467dbc584
commit cf3d11a8a3
2 changed files with 132 additions and 3 deletions
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace App\Tests\Functional\Command;
use App\Module\Absence\Domain\Entity\AbsenceBalance;
use App\Module\Absence\Domain\Enum\AbsenceType;
use App\Module\Absence\Infrastructure\Command\AccrueLeaveCommand;
use App\Module\Absence\Infrastructure\Doctrine\DoctrineAbsenceBalanceRepository;
use App\Module\Core\Domain\Entity\User;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
/**
* Covers the period roll-over: when a new reference period opens, the previous
* period's "en cours d'acquisition" (N) becomes the new "acquired" (N-1), but
* only for the days that were not already taken.
*
* @internal
*/
class AccrueLeaveCommandTest extends KernelTestCase
{
private EntityManagerInterface $em;
private DoctrineAbsenceBalanceRepository $balances;
protected function setUp(): void
{
self::bootKernel();
$this->em = self::getContainer()->get(EntityManagerInterface::class);
$this->balances = self::getContainer()->get(DoctrineAbsenceBalanceRepository::class);
}
/**
* Tristan's real case: 9.75 accruing, 1 day taken, nothing previously
* acquired → the day taken eats into the carry-over, so 8.75 rolls over
* (not 9.75).
*/
public function testCarryOverDeductsTakenDays(): void
{
$user = $this->createEmployee();
$this->seedPreviousBalance($user, acquired: 0.0, acquiring: 9.75, taken: 1.0);
$this->runForJune2026();
$rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
self::assertNotNull($rolled);
self::assertEqualsWithDelta(8.75, $rolled->getAcquired(), 0.0001);
}
/**
* Leave is charged oldest-first: the 3 days taken come out of the expiring
* N-2 "acquired" bucket (5), so the full 10 accruing days carry over intact.
*/
public function testCarryOverChargesOldestBucketFirst(): void
{
$user = $this->createEmployee();
$this->seedPreviousBalance($user, acquired: 5.0, acquiring: 10.0, taken: 3.0);
$this->runForJune2026();
$rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
self::assertNotNull($rolled);
self::assertEqualsWithDelta(10.0, $rolled->getAcquired(), 0.0001);
}
/** No day taken → the whole accruing bucket carries over. */
public function testFullCarryOverWhenNothingTaken(): void
{
$user = $this->createEmployee();
$this->seedPreviousBalance($user, acquired: 0.0, acquiring: 10.0, taken: 0.0);
$this->runForJune2026();
$rolled = $this->balances->findOneForPeriod($user, AbsenceType::PaidLeave, '2026-2027');
self::assertNotNull($rolled);
self::assertEqualsWithDelta(10.0, $rolled->getAcquired(), 0.0001);
}
private function createEmployee(): User
{
$user = new User();
$user->setUsername('accrue-test-'.uniqid());
$user->setPassword('x');
$user->setRoles(['ROLE_USER']);
$user->setIsEmployee(true);
$user->setHireDate(new DateTimeImmutable('2024-01-01'));
$user->setReferencePeriodStart('06-01');
$user->setAnnualLeaveDays(25.0);
$user->setWorkTimeRatio(1.0);
$user->setInitialLeaveBalance(0.0);
$this->em->persist($user);
$this->em->flush();
return $user;
}
private function seedPreviousBalance(User $user, float $acquired, float $acquiring, float $taken): void
{
$balance = new AbsenceBalance();
$balance->setUser($user);
$balance->setType(AbsenceType::PaidLeave);
$balance->setPeriod('2025-2026');
$balance->setAcquired($acquired);
$balance->setAcquiring($acquiring);
$balance->setTaken($taken);
$this->em->persist($balance);
$this->em->flush();
}
private function runForJune2026(): void
{
$command = self::getContainer()->get(AccrueLeaveCommand::class);
$tester = new CommandTester($command);
$tester->execute(['--month' => '2026-06']);
self::assertSame(0, $tester->getStatusCode());
}
}