addOption('month', null, InputOption::VALUE_REQUIRED, 'Target month (YYYY-MM), defaults to the current month') ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Compute and display without persisting') ; } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $monthOpt = $input->getOption('month'); $dryRun = (bool) $input->getOption('dry-run'); try { $firstDay = $monthOpt ? new DateTimeImmutable($monthOpt.'-01') : new DateTimeImmutable('first day of this month'); } catch (Exception) { $io->error('Invalid --month, expected format YYYY-MM.'); return Command::FAILURE; } $firstDay = $firstDay->setTime(0, 0); $lastDay = $firstDay->modify('last day of this month'); $monthKey = $firstDay->format('Y-m'); $io->title(sprintf('Acquisition CP — %s%s', $monthKey, $dryRun ? ' (dry-run)' : '')); $employees = $this->userRepository->findActiveEmployees($lastDay); if ([] === $employees) { $io->warning('Aucun salarié actif pour ce mois.'); return Command::SUCCESS; } $rows = []; $accrued = 0; $skipped = 0; foreach ($employees as $user) { $rate = ($user->getAnnualLeaveDays() / 12) * $user->getWorkTimeRatio(); $period = $this->balanceService->periodFor($user, AbsenceType::PaidLeave, $firstDay); $balance = $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $period); $isNew = null === $balance; if ($isNew) { $balance = $this->balanceService->getOrCreateBalance($user, AbsenceType::PaidLeave, $period); // On a new period, the previous period's "en cours d'acquisition" (N) // becomes this period's acquired (N-1). At roll-out (no prior balance) // seed the configured initial balance instead. $previousPeriod = self::previousPeriod($period); $previousBalance = null !== $previousPeriod ? $this->balanceRepository->findOneForPeriod($user, AbsenceType::PaidLeave, $previousPeriod) : null; $balance->setAcquired( null !== $previousBalance ? $previousBalance->getAcquiring() : $user->getInitialLeaveBalance(), ); } if ($monthKey === $balance->getLastAccruedMonth()) { ++$skipped; $rows[] = [$user->getUsername(), $period, number_format($balance->getAcquired(), 2), number_format($balance->getAcquiring(), 2), 'déjà fait']; continue; } $balance->setAcquiring($balance->getAcquiring() + $rate); $balance->setLastAccruedMonth($monthKey); ++$accrued; $seeded = $isNew && (null !== self::previousPeriod($period) || $user->getInitialLeaveBalance() > 0); $rows[] = [ $user->getUsername(), $period, number_format($balance->getAcquired(), 2), number_format($balance->getAcquiring(), 2), sprintf('+%s%s', number_format($rate, 2), $seeded && $balance->getAcquired() > 0 ? ' (N-1 reporté)' : ''), ]; } if (!$dryRun) { $this->entityManager->flush(); } $io->table(['Salarié', 'Période', 'Acquis (N-1)', 'En cours (N)', 'Action'], $rows); $io->success(sprintf('%d crédité(s), %d ignoré(s)%s.', $accrued, $skipped, $dryRun ? ' (dry-run, rien enregistré)' : '')); return Command::SUCCESS; } /** Previous reference period for a "YYYY-YYYY" paid-leave period, or null. */ private static function previousPeriod(string $period): ?string { if (1 !== preg_match('/^(\d{4})-(\d{4})$/', $period, $m)) { return null; } return sprintf('%d-%d', (int) $m[1] - 1, (int) $m[2] - 1); } }