From 46b610370a9544ed154b8f63ff372d5d6d9e74d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 6 Jan 2026 14:06:04 +0100 Subject: [PATCH] feat: draft inspection cli command --- src/Command/DraftInspectCommand.php | 131 ++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 src/Command/DraftInspectCommand.php diff --git a/src/Command/DraftInspectCommand.php b/src/Command/DraftInspectCommand.php new file mode 100644 index 0000000..2fd8821 --- /dev/null +++ b/src/Command/DraftInspectCommand.php @@ -0,0 +1,131 @@ +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->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', '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); + } +}