addOption( 'force', null, InputOption::VALUE_NONE, 'Run rollover regardless of business date (manual recovery mode).' ); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $today = new DateTimeImmutable('today'); $force = (bool) $input->getOption('force'); $this->logger->info('app:rtt:rollover started.', ['date' => $today->format('Y-m-d'), 'force' => $force]); if (!$force && '06-01' !== $today->format('m-d')) { $message = 'No RTT rollover today: business date is not 01/06.'; $this->logger->info($message, ['date' => $today->format('Y-m-d')]); $io->success($message); return Command::SUCCESS; } $targetYear = $this->resolveTargetYear($today); $created = 0; $skipped = 0; foreach ($this->employeeRepository->findAll() as $employee) { if (!$employee instanceof Employee) { continue; } if (!$this->isEligible($employee)) { $this->logger->info('Employee skipped: not eligible.', ['employeeId' => $employee->getId()]); ++$skipped; continue; } $existing = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $targetYear); if (null !== $existing) { $this->logger->info('Employee skipped: balance already exists.', ['employeeId' => $employee->getId(), 'year' => $targetYear]); ++$skipped; continue; } try { $previousYear = $targetYear - 1; $carryMinutes = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $previousYear); } catch (Throwable $e) { $this->logger->error('Error computing carry for employee.', ['employeeId' => $employee->getId(), 'error' => $e->getMessage()]); ++$skipped; continue; } $balance = new EmployeeRttBalance() ->setEmployee($employee) ->setYear($targetYear) ->setOpeningMinutes($carryMinutes) ->setIsLocked(false) ; $this->entityManager->persist($balance); $this->logger->info('Balance created.', ['employeeId' => $employee->getId(), 'year' => $targetYear, 'carryMinutes' => $carryMinutes]); ++$created; } try { $this->entityManager->flush(); } catch (Throwable $e) { $this->logger->error('Error flushing RTT balances.', ['error' => $e->getMessage()]); $io->error('RTT rollover failed: '.$e->getMessage()); return Command::FAILURE; } $message = sprintf('RTT rollover done: %d created, %d skipped.', $created, $skipped); $this->logger->info($message); $io->success($message); return Command::SUCCESS; } private function resolveTargetYear(DateTimeImmutable $today): int { $year = (int) $today->format('Y'); $month = (int) $today->format('n'); return $month >= 6 ? $year + 1 : $year; } private function isEligible(Employee $employee): bool { $contract = $employee->getContract(); if (null === $contract) { return false; } if (TrackingMode::PRESENCE->value === $contract->getTrackingMode()) { return false; } $type = ContractType::resolve( $contract->getName(), $contract->getTrackingMode(), $contract->getWeeklyHours() ); return ContractType::INTERIM !== $type; } }