addOption('days', 'd', InputOption::VALUE_OPTIONAL, 'Show only drafts older than N days', null) ->addOption('booking-id', 'b', InputOption::VALUE_OPTIONAL, 'Filter by booking ID', null) ->addOption('delete', null, InputOption::VALUE_NONE, 'Delete the listed drafts (use with caution)'); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $days = $input->getOption('days'); $bookingId = $input->getOption('booking-id'); $shouldDelete = $input->getOption('delete'); $qb = $this->draftRepository->createQueryBuilder('d') ->join('d.user', 'u') ->select('d', 'u') ->orderBy('d.updatedAt', 'DESC'); if (null !== $days) { $cutoffDate = new \DateTimeImmutable(sprintf('-%d days', (int) $days)); $qb->andWhere('d.updatedAt < :cutoff') ->setParameter('cutoff', $cutoffDate); } if (null !== $bookingId) { $qb->andWhere('d.bookingId = :bookingId') ->setParameter('bookingId', (int) $bookingId); } $drafts = $qb->getQuery()->getResult(); if (0 === \count($drafts)) { $io->success('No drafts found matching the criteria.'); return Command::SUCCESS; } $io->title(sprintf('Found %d draft(s)', \count($drafts))); $rows = []; foreach ($drafts as $draft) { $formData = $draft->getFormData(); $participantCount = isset($formData['participants']) ? \count($formData['participants']) : 0; $rows[] = [ $draft->getId(), $draft->getBookingId(), $draft->getUser()->getEmail(), $draft->getTravelDate()->format('Y-m-d'), $draft->getCreatedAt()->format('Y-m-d H:i:s'), $draft->getUpdatedAt()->format('Y-m-d H:i:s'), $this->getAgeDays($draft->getUpdatedAt()), $participantCount, ]; } $io->table( ['ID', 'Booking ID', 'User Email', 'Travel Date', 'Created At', 'Updated At', 'Days Old', 'Participants'], $rows ); $io->section('Investigation Tips'); $io->listing([ 'Check logs for "Booking update successful" with matching booking_id after the draft\'s updated_at', 'If a successful update exists after draft creation, the draft should have been deleted (potential bug)', 'Drafts are preserved when: user cancels, API rejects submission, or user simply leaves', ]); if ($shouldDelete) { if (false === $io->confirm(sprintf('Are you sure you want to delete %d draft(s)?', \count($drafts)), false)) { $io->warning('Deletion cancelled.'); return Command::SUCCESS; } $em = $this->draftRepository->getEntityManager(); foreach ($drafts as $draft) { $em->remove($draft); } $em->flush(); $io->success(sprintf('Deleted %d draft(s).', \count($drafts))); } return Command::SUCCESS; } private function getAgeDays(\DateTimeImmutable $date): string { $now = new \DateTimeImmutable(); $diff = $now->diff($date); if ($diff->days > 0) { return sprintf('%d days', $diff->days); } if ($diff->h > 0) { return sprintf('%d hours', $diff->h); } return sprintf('%d min', $diff->i); } }