feat(appointment): expire pending bookings past their payment window
Add findPaymentExpired (pending with expires_at < now) and have the cancel-expired command flip both payment-expired and slot-time-passed pendings to expired (deduped). Run it on a schedule (e.g. every minute: * * * * * php bin/console app:cancel-expired-appointments) to free locks held by unpaid bookings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:cancel-expired-appointments',
|
||||
description: 'Marks pending appointments whose slot_start is in the past as expired',
|
||||
description: 'Expires pending bookings whose 15-min payment window lapsed or whose slot time has passed',
|
||||
)]
|
||||
class CancelExpiredAppointmentsCommand extends Command
|
||||
{
|
||||
@@ -22,9 +22,14 @@ class CancelExpiredAppointmentsCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$expired = $this->appointmentRepo->findExpiredPending(time());
|
||||
$count = 0;
|
||||
$now = time();
|
||||
|
||||
$expired = [];
|
||||
foreach ([...$this->appointmentRepo->findPaymentExpired($now), ...$this->appointmentRepo->findExpiredPending($now)] as $appointment) {
|
||||
$expired[$appointment->getUuid()] = $appointment;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($expired as $appointment) {
|
||||
$appointment->transitionTo(Appointment::STATUS_EXPIRED);
|
||||
$this->appointmentRepo->save($appointment, false);
|
||||
@@ -32,7 +37,7 @@ class CancelExpiredAppointmentsCommand extends Command
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
$this->appointmentRepo->save($expired[0]); // flush once
|
||||
$this->appointmentRepo->save(reset($expired)); // flush once
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('Expired %d appointments.', $count));
|
||||
|
||||
@@ -80,6 +80,19 @@ class AppointmentRepository extends ServiceEntityRepository
|
||||
return $this->findBy($criteria, ['slotStart' => 'DESC']);
|
||||
}
|
||||
|
||||
/** @return Appointment[] pending bookings whose 15-minute payment window has lapsed */
|
||||
public function findPaymentExpired(int $now): array
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->where('a.status = :status')
|
||||
->andWhere('a.expiresAt IS NOT NULL')
|
||||
->andWhere('a.expiresAt < :now')
|
||||
->setParameter('status', Appointment::STATUS_PENDING)
|
||||
->setParameter('now', $now)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/** @return Appointment[] pending appointments older than given timestamp */
|
||||
public function findExpiredPending(int $before): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user