Payment event ledger (PaymentEvent) replicated to the backup server, plus CSV export script on 247

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 14:18:47 +03:30
parent 947f946909
commit 8d8665edf3
10 changed files with 199 additions and 3 deletions

View File

@@ -15,7 +15,7 @@ from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle
from rest_framework.views import APIView
from . import bazaar, zarinpal
from . import bazaar, events, zarinpal
from .models import Order
from .permissions import HasInternalServiceKey
from .serializers import (
@@ -103,12 +103,18 @@ class PaymentRedirectView(APIView):
if order.status != 'pending' or order.gateway_authority:
break
requested_amount = order.amount
result = zarinpal.request_payment(order)
try:
result = zarinpal.request_payment(order)
except Exception as exc:
events.record('gateway_request_failed', order=order, error=type(exc).__name__)
raise
Order.objects.filter(pk=order.pk, gateway_authority='', amount=requested_amount).update(
gateway_authority=result.authority,
)
order.refresh_from_db(fields=['gateway_authority', 'amount', 'status'])
events.record('gateway_redirect', order=order, status=order.status,
authority_set=bool(order.gateway_authority))
if order.status != 'pending':
# سفارش از قبل نتیجه گرفته (paid/failed، یا کدِ ۱۰۰٪ همین الان)؛ چیزی برای پرداختِ تازه نیست.
# کاربر به اپ برمی‌گردد با همان نتیجه، نه به ریشه‌ی دامنه‌ی API.
@@ -159,7 +165,10 @@ class ZarinpalCallbackView(APIView):
if order is None:
# کال‌بکِ بی‌سفارش: یا Authorityِ جعلی یا سفارشی که از دیتابیس رفته؛ در دیتابیس ردی ندارد.
logger.warning('zarinpal callback: no order for authority=%s status=%s', authority[:64], status_param[:10])
events.record('callback_unknown_authority', provider='gateway',
authority=authority[:64], status_param=status_param[:10])
raise Http404
events.record('callback_received', order=order, authority=authority[:64], status_param=status_param[:10])
if status_param != 'OK':
new_status, gateway_ref_id = 'failed', ''
else:
@@ -168,6 +177,7 @@ class ZarinpalCallbackView(APIView):
)
new_status = 'paid' if result.is_valid else 'failed'
gateway_ref_id = result.ref_id
events.record('verify_result', order=order, valid=bool(result.is_valid), ref_id=gateway_ref_id)
with transaction.atomic():
locked_order = get_object_or_404(Order.objects.select_for_update(), pk=order.pk)
@@ -306,6 +316,7 @@ class BazaarVerifyPurchaseView(APIView):
# apply_order_payment_result سفارش را از نو می‌خواند و همان را عوض می‌کند؛ این نمونه هنوز
# وضعیتِ پیش از پرداخت را دارد. بدونِ این، پاسخ «pending» بود درحالی‌که سفارش «paid» شده بود.
order.refresh_from_db(fields=['status'])
events.record('bazaar_verify', order=order, valid=bool(result.is_valid), status=order.status)
data = {'orderId': str(order.id), 'status': order.status, 'valid': result.is_valid}
return Response(BazaarVerifyResponseSerializer(data).data)
@@ -366,6 +377,7 @@ class BazaarStartPurchaseView(APIView):
# علامتِ «خرید شروع شد» تا کدِ تخفیفِ بعدی نیاید؛ قیمت همان قیمتِ کامل است.
order.bazaar_price_rial = _to_rial(order.amount, order.currency)
order.save(update_fields=['bazaar_price_rial'])
events.record('bazaar_start', order=order, amount=order.amount, discounted=token is not None)
data = {
'orderId': str(order.id),
'sku': order.bazaar_product_sku,