|
|
|
@@ -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());
|
|
|
|
|
}
|
|
|
|
|
}
|