feat: draft inspection cli command

This commit is contained in:
Björn Fromme
2026-01-06 14:06:04 +01:00
parent 1c1766bbd4
commit 8bdc33b4f0
+131
View File
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\BookingEditDraftRepository;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:draft:inspect',
description: 'Inspect booking edit drafts in the database',
)]
class DraftInspectCommand extends Command
{
public function __construct(
private readonly BookingEditDraftRepository $draftRepository,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->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);
}
}