connect Zarinpal as the real payment gateway

payment.services.call_payment_gateway was a stub returning a fake
payment.gateway/pay/{id} URL. It's replaced with a real Zarinpal
integration (payment/zarinpal.py, Payment API v4):

- create_order/CreateOrderView still never call Zarinpal directly —
  paymentUrl points at our own GET /api/payment/orders/{id}/pay
  (PaymentRedirectView), which requests the real Zarinpal authority on
  first visit and redirects to StartPay. This keeps order creation from
  depending on Zarinpal's uptime/latency, and means none of the existing
  purchase/idempotency tests needed to change.
- GET /api/payment/zarinpal/callback (ZarinpalCallbackView) is where
  Zarinpal redirects the user back to; it always re-verifies
  server-to-server via zarinpal.verify_payment before marking an order
  paid, so a forged Status=OK in the query string alone does nothing.
- zarinpal.py fails closed (503 / invalid) when ZARINPAL_MERCHANT_ID
  isn't configured, matching payment/bazaar.py's existing convention —
  never a fake successful payment.

Adds Order.gateway_authority, PUBLIC_BASE_URL/ZARINPAL_* settings, and
a payment_pay throttle scope.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 13:27:13 +03:30
parent a275eac7fd
commit b5476e1c5d
11 changed files with 630 additions and 14 deletions

View File

@@ -4,6 +4,7 @@
جنگویی‌ه (نه میکروسرویس واقعی)، «فراخوانی سرور-به-سرور» با یک import مستقیم
پیاده‌سازی شده، نه یک HTTP call به خودمون.
"""
from django.conf import settings
from django.db import IntegrityError
from django.db import transaction as db_transaction
from django.utils import timezone
@@ -38,11 +39,15 @@ def _ensure_idempotent_match(order: Order, *, user_id: int, amount: int, currenc
def call_payment_gateway(order: Order) -> str:
"""
استاب فراخوانی درگاه پرداخت واقعی؛ فعلاً یک paymentUrl ساختگی برمی‌گرداند
تا کل جریان (ساخت سفارش → پرداخت → webhook) بدون کلید API واقعی قابل تست باشد.
برای اتصال واقعی، این تابع با فراخوانی HTTP به API درگاه جایگزین شود.
paymentUrlی که به کلاینت برمی‌گردد: آدرسِ داخلیِ خودمان
(PaymentRedirectView)، نه مستقیماً StartPay زرین‌پال. فراخوانیِ واقعیِ
PaymentRequest به زرین‌پال (یک HTTP call خارجی) عمداً همین‌جا انجام
نمی‌شود — create_order از subscription صدا زده می‌شود و نباید موفقیتِ
ساختِ سفارش به دسترس‌بودن/سرعتِ زرین‌پال گره بخورد؛ PaymentRequest اولین
باری که کاربر واقعاً این URL را باز کند زده می‌شود (نگاه کنید به
payment/zarinpal.py و PaymentRedirectView در views.py).
"""
return f'https://payment.gateway/pay/{order.id}'
return f'{settings.PUBLIC_BASE_URL}/api/payment/orders/{order.id}/pay'
def create_order(*, user_id: int, amount: int, currency: str = 'IRT',