feat(payment): implement expiration handling for appointment payments

This commit is contained in:
hamed
2026-07-03 09:53:55 +03:30
parent ab83a64d9d
commit 34d521434f
4 changed files with 232 additions and 0 deletions
+9
View File
@@ -170,6 +170,15 @@ class Appointment
return $this;
}
/** آیا پنجرهٔ ۱۵ دقیقه‌ایِ پرداخت گذشته یا زمان اسلات رد شده است؟ */
public function isPaymentWindowExpired(int $now): bool
{
if ($this->expiresAt !== null && $now > $this->expiresAt) {
return true;
}
return $now >= $this->slotStart;
}
public function canTransitionTo(string $newStatus): bool
{
return in_array($newStatus, self::ALLOWED_TRANSITIONS[$this->status] ?? [], true);
@@ -155,6 +155,14 @@ class PaymentController extends BaseController
return $this->redirectToReturn($return, 'notfound');
}
// نوبتِ منقضی (status=expired یا پنجرهٔ ۱۵ دقیقه‌ای گذشته) → غیرقابل‌پرداخت.
if ($appointment->getStatus() === Appointment::STATUS_EXPIRED
|| ($appointment->getStatus() === Appointment::STATUS_PENDING
&& $appointment->isPaymentWindowExpired(time()))) {
$this->expireAppointmentPayment($appointment);
return $this->redirectToReturn($return, 'expired');
}
// اعتبارسنجی سفارش: فقط نوبت قابل‌پرداخت.
if (!in_array($appointment->getStatus(), [Appointment::STATUS_PENDING, Appointment::STATUS_CONFIRMED], true)) {
return $this->redirectToReturn($return, 'invalid');
@@ -194,6 +202,21 @@ class PaymentController extends BaseController
return $payment;
}
/** نوبتِ منقضی را expired و پرداخت pending آن را canceled می‌کند (idempotent). */
private function expireAppointmentPayment(Appointment $appointment): void
{
if ($appointment->getStatus() === Appointment::STATUS_PENDING
&& $appointment->canTransitionTo(Appointment::STATUS_EXPIRED)) {
$appointment->transitionTo(Appointment::STATUS_EXPIRED);
$this->appointmentRepo->save($appointment);
}
$payment = $this->paymentRepo->findPendingByAppointment($appointment);
if ($payment !== null) {
$payment->setStatus(Payment::STATUS_CANCELED);
$this->paymentRepo->save($payment);
}
}
private function redirectToReturn(string $return, string $status): \Symfony\Component\HttpFoundation\Response
{
if ($return !== '' && $this->isAllowedFrontend($return)) {
@@ -224,6 +247,16 @@ class PaymentController extends BaseController
return $this->renderPaymentResult('notfound');
}
// پرداختِ نوبت: اگر نوبت منقضی شده باشد، پرداخت هم منقضی و غیرقابل‌پرداخت است.
$appointment = $payment->getAppointment();
if ($payment->getType() === Payment::TYPE_APPOINTMENT && $appointment !== null
&& ($appointment->getStatus() === Appointment::STATUS_EXPIRED
|| ($appointment->getStatus() === Appointment::STATUS_PENDING
&& $appointment->isPaymentWindowExpired(time())))) {
$this->expireAppointmentPayment($appointment);
return $this->renderPaymentResult('expired', $payment);
}
// فقط پرداخت در انتظار قابل انتقال به درگاه است (جلوگیری از پرداخت تکراری/replay).
if ($payment->getStatus() !== Payment::STATUS_PENDING) {
return $this->redirectToFrontend($payment, $payment->getStatus() === Payment::STATUS_SUCCESS);
@@ -558,6 +591,7 @@ class PaymentController extends BaseController
'gateway' => ['درگاه نامعتبر', 'درگاه پرداخت انتخابی نامعتبر یا غیرفعال است.'],
'invalid_return' => ['آدرس بازگشت نامعتبر', 'آدرس بازگشت مجاز نیست.'],
'forbidden' => ['دسترسی غیرمجاز', 'این درخواست از مبدأ مجاز ارسال نشده است.'],
'expired' => ['مهلت پرداخت به پایان رسید', 'مهلت ۱۵ دقیقه‌ایِ پرداخت این نوبت تمام شده است. لطفاً دوباره نوبت بگیرید.'],
];
[$title, $message] = $labels[$status] ?? ['خطا در پرداخت', 'خطایی در فرآیند پرداخت رخ داد.'];