Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 052ef55c79 | |||
| 302d2c7221 | |||
| cf3d11a8a3 | |||
| b467dbc584 | |||
| 17a0566f77 |
+1
-1
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.4.34'
|
||||
app.version: '0.4.36'
|
||||
|
||||
@@ -111,9 +111,18 @@ class AccrueLeaveCommand extends Command
|
||||
$previousBalance = null !== $previousPeriod
|
||||
? $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $previousPeriod)
|
||||
: null;
|
||||
$balance->setAcquired(
|
||||
null !== $previousBalance ? $previousBalance->getAcquiring() : $profile->getInitialLeaveBalance(),
|
||||
);
|
||||
|
||||
if (null !== $previousBalance) {
|
||||
// Only the days *not yet taken* carry over. Leave is charged
|
||||
// oldest-first: it first consumes the previous "acquired"
|
||||
// (N-2) bucket — which expires at roll-over anyway — so only
|
||||
// days taken beyond that bucket eat into the carry-over.
|
||||
$carryOver = $previousBalance->getAcquiring()
|
||||
- max(0.0, $previousBalance->getTaken() - $previousBalance->getAcquired());
|
||||
$balance->setAcquired(max(0.0, $carryOver));
|
||||
} else {
|
||||
$balance->setAcquired($profile->getInitialLeaveBalance());
|
||||
}
|
||||
}
|
||||
|
||||
if ($monthKey === $balance->getLastAccruedMonth()) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user