From 089badbb5a9d6595ed6024d2512ae82803763e43 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Mon, 15 Jun 2026 17:58:28 +0330 Subject: [PATCH] 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 --- .../Command/CancelExpiredAppointmentsCommand.php | 13 +++++++++---- .../Repository/AppointmentRepository.php | 13 +++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Appointment/Command/CancelExpiredAppointmentsCommand.php b/src/Appointment/Command/CancelExpiredAppointmentsCommand.php index f1fd98b8..61d20a75 100644 --- a/src/Appointment/Command/CancelExpiredAppointmentsCommand.php +++ b/src/Appointment/Command/CancelExpiredAppointmentsCommand.php @@ -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)); diff --git a/src/Appointment/Repository/AppointmentRepository.php b/src/Appointment/Repository/AppointmentRepository.php index 00e6c155..f86aab47 100644 --- a/src/Appointment/Repository/AppointmentRepository.php +++ b/src/Appointment/Repository/AppointmentRepository.php @@ -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 {