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