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

@@ -24,6 +24,12 @@ class Order(models.Model):
# بازار می‌گیره و برای verify به بک‌اند می‌فرسته.
bazaar_product_sku = models.CharField(max_length=100, blank=True)
bazaar_purchase_token = models.CharField(max_length=300, blank=True)
# فقط وقتی provider == 'gateway' (زرین‌پال) پر می‌شه: Authorityِ برگشتی از
# PaymentRequest، وقتی کاربر واقعاً لینکِ پرداخت رو باز کنه (نه موقعِ
# ساختِ سفارش؛ نگاه کنید به PaymentRedirectView). همون چیزیه که زرین‌پال
# موقعِ callback در query string (Authority=...) برمی‌گردونه، پس برای
# پیداکردنِ Order از روی callback لازمه.
gateway_authority = models.CharField(max_length=64, blank=True, db_index=True)
idempotency_key = models.CharField(max_length=100, unique=True)
created_at = models.DateTimeField(auto_now_add=True)