feat: cli command to clean up outdated or stale booking draft records

This commit is contained in:
Björn Fromme
2026-01-09 09:22:51 +01:00
parent 0992c05539
commit da517b37b6
6 changed files with 142 additions and 3 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260109081315 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE booking_edit_draft ADD travel_date DATE NOT NULL COMMENT \'(DC2Type:date_immutable)\'');
$this->addSql('CREATE INDEX IDX_DRAFT_TRAVEL_DATE ON booking_edit_draft (travel_date)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX IDX_DRAFT_TRAVEL_DATE ON booking_edit_draft');
$this->addSql('ALTER TABLE booking_edit_draft DROP travel_date');
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace App\Command;
use App\Repository\BookingEditDraftRepository;
use Psr\Log\LoggerInterface;
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:cleanup',
description: 'Delete booking edit drafts for departed travels',
)]
class DraftCleanupCommand extends Command
{
public function __construct(
private readonly BookingEditDraftRepository $draftRepository,
private readonly LoggerInterface $logger,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be deleted without actually deleting');
}
/**
* @see \Symfony\Component\Console\Command\Command::execute()
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
if ($dryRun) {
$expiredDrafts = $this->draftRepository->createQueryBuilder('d')
->where('d.travelDate < :today')
->setParameter('today', new \DateTimeImmutable('today'))
->getQuery()
->getResult();
$count = \count($expiredDrafts);
if (0 === $count) {
$io->success('No expired drafts found.');
return Command::SUCCESS;
}
$io->note(sprintf('[DRY RUN] Would delete %d expired draft(s)', $count));
return Command::SUCCESS;
}
$deletedCount = $this->draftRepository->deleteExpiredDrafts();
if (0 === $deletedCount) {
$io->success('No expired drafts found.');
return Command::SUCCESS;
}
$this->logger->info('Deleted expired booking edit drafts', [
'count' => $deletedCount,
]);
$io->success(sprintf('Deleted %d expired draft(s).', $deletedCount));
return Command::SUCCESS;
}
}
+2 -1
View File
@@ -75,6 +75,7 @@ class DraftInspectCommand extends Command
$draft->getId(), $draft->getId(),
$draft->getBookingId(), $draft->getBookingId(),
$draft->getUser()->getEmail(), $draft->getUser()->getEmail(),
$draft->getTravelDate()->format('Y-m-d'),
$draft->getCreatedAt()->format('Y-m-d H:i:s'), $draft->getCreatedAt()->format('Y-m-d H:i:s'),
$draft->getUpdatedAt()->format('Y-m-d H:i:s'), $draft->getUpdatedAt()->format('Y-m-d H:i:s'),
$this->getAgeDays($draft->getUpdatedAt()), $this->getAgeDays($draft->getUpdatedAt()),
@@ -83,7 +84,7 @@ class DraftInspectCommand extends Command
} }
$io->table( $io->table(
['ID', 'Booking ID', 'User Email', 'Created At', 'Updated At', 'Days Old', 'Participants'], ['ID', 'Booking ID', 'User Email', 'Travel Date', 'Created At', 'Updated At', 'Days Old', 'Participants'],
$rows $rows
); );
+11 -1
View File
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BookingEditDraftRepository::class)] #[ORM\Entity(repositoryClass: BookingEditDraftRepository::class)]
#[ORM\Table(name: 'booking_edit_draft')] #[ORM\Table(name: 'booking_edit_draft')]
#[ORM\UniqueConstraint(name: 'user_booking_unique', columns: ['user_id', 'booking_id'])] #[ORM\UniqueConstraint(name: 'user_booking_unique', columns: ['user_id', 'booking_id'])]
#[ORM\Index(name: 'IDX_DRAFT_TRAVEL_DATE', columns: ['travel_date'])]
class BookingEditDraft class BookingEditDraft
{ {
#[ORM\Id] #[ORM\Id]
@@ -24,6 +25,9 @@ class BookingEditDraft
#[ORM\Column(type: 'integer')] #[ORM\Column(type: 'integer')]
private int $bookingId; private int $bookingId;
#[ORM\Column(type: 'date_immutable')]
private \DateTimeImmutable $travelDate;
#[ORM\Column(type: 'json')] #[ORM\Column(type: 'json')]
private array $formData = []; private array $formData = [];
@@ -33,10 +37,11 @@ class BookingEditDraft
#[ORM\Column(type: 'datetime_immutable')] #[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $updatedAt; private \DateTimeImmutable $updatedAt;
public function __construct(User $user, int $bookingId, array $formData) public function __construct(User $user, int $bookingId, \DateTimeImmutable $travelDate, array $formData)
{ {
$this->user = $user; $this->user = $user;
$this->bookingId = $bookingId; $this->bookingId = $bookingId;
$this->travelDate = $travelDate;
$this->formData = $formData; $this->formData = $formData;
$this->createdAt = new \DateTimeImmutable(); $this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new \DateTimeImmutable();
@@ -57,6 +62,11 @@ class BookingEditDraft
return $this->bookingId; return $this->bookingId;
} }
public function getTravelDate(): \DateTimeImmutable
{
return $this->travelDate;
}
public function getFormData(): array public function getFormData(): array
{ {
return $this->formData; return $this->formData;
@@ -52,4 +52,19 @@ class BookingEditDraftRepository extends ServiceEntityRepository
->getQuery() ->getQuery()
->execute(); ->execute();
} }
/**
* Deletes all drafts where the travel date has passed.
*
* @return int Number of deleted drafts
*/
public function deleteExpiredDrafts(): int
{
return (int) $this->createQueryBuilder('d')
->delete()
->where('d.travelDate < :today')
->setParameter('today', new \DateTimeImmutable('today'))
->getQuery()
->execute();
}
} }
+2 -1
View File
@@ -59,12 +59,13 @@ class BookingEditDraftService
public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void public function saveDraft(User $user, int $bookingId, BookingDto $bookingDto): void
{ {
$formData = $this->fingerprintService->extractUserData($bookingDto); $formData = $this->fingerprintService->extractUserData($bookingDto);
$travelDate = $bookingDto->travel->dateFrom;
$existingDraft = $this->findDraft($user, $bookingId); $existingDraft = $this->findDraft($user, $bookingId);
if (null !== $existingDraft) { if (null !== $existingDraft) {
$existingDraft->setFormData($formData); $existingDraft->setFormData($formData);
} else { } else {
$draft = new BookingEditDraft($user, $bookingId, $formData); $draft = new BookingEditDraft($user, $bookingId, $travelDate, $formData);
$this->entityManager->persist($draft); $this->entityManager->persist($draft);
} }