fix(payments): make checkout attempts idempotent and reconcilable

This commit is contained in:
2026-09-30 21:14:16 +03:30
parent 77567178ad
commit 1daa755543
9 changed files with 862 additions and 173 deletions

73
apps/payments/tasks.py Normal file
View File

@@ -0,0 +1,73 @@
"""Bounded retries for payment verification after an inconclusive callback."""
import logging
from celery import shared_task
from django.shortcuts import get_object_or_404
from apps.payments.models import Payment
from apps.payments.services import PaymentVerificationUncertain, request_payment_verification
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=5, name="apps.payments.reconcile_payment")
def reconcile_payment(self, payment_id: int):
payment = get_object_or_404(
Payment.objects.select_related("event", "user"),
pk=payment_id,
)
if payment.status in (Payment.OrderStatusChoices.PAID, Payment.OrderStatusChoices.FAILED):
return payment.get_status_display().lower()
if not payment.authority:
logger.error(
"Payment %s remains pending without a gateway authority; manual review is required",
payment.id,
)
return "pending_manual_review"
# Imported at task runtime to avoid a view/task import cycle.
from apps.payments.api.views import (
_mark_payment_paid,
_mark_payment_pending,
_mark_payment_unsuccessful,
_notify_payment_status,
)
try:
code, data = request_payment_verification(payment)
except PaymentVerificationUncertain as exc:
_mark_payment_pending(payment.id)
if self.request.retries >= self.max_retries:
logger.error(
"Payment %s verification remains uncertain after bounded retries; manual review is required",
payment.id,
exc_info=exc,
)
return "pending_manual_review"
countdown = min(60 * (2 ** self.request.retries), 15 * 60)
raise self.retry(exc=exc, countdown=countdown)
if code in (100, 101):
payment, newly_verified = _mark_payment_paid(payment.id, data)
if newly_verified:
_notify_payment_status(
payment,
title=f"پرداخت رویداد «{payment.event.title}» تأیید شد",
message="پرداخت شما با موفقیت ثبت شد و ثبت‌نام رویداد تکمیل شده است.",
level="success",
)
return "paid"
payment, result, transitioned = _mark_payment_unsuccessful(
payment.id,
Payment.OrderStatusChoices.FAILED,
)
if transitioned:
_notify_payment_status(
payment,
title=f"پرداخت رویداد «{payment.event.title}» ناموفق بود",
message="تراکنش شما توسط درگاه تأیید نشد. در صورت نیاز دوباره پرداخت را انجام دهید.",
level="error",
)
return result