Files
MathApp/payment/models.py
Zareian b5476e1c5d 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>
2026-09-06 13:27:13 +03:30

68 lines
3.9 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.db import models
class Order(models.Model):
"""
تنها اپی که واقعاً پول جابه‌جا می‌کنه. عمداً از Subscription جدا شده
چون منطق و ریسکش فرق داره (درگاه، retry، idempotency).
"""
STATUS_CHOICES = [('pending', 'در انتظار'), ('paid', 'پرداخت‌شده'), ('failed', 'ناموفق')]
# 'gateway' و 'bazaar' منطق و صحت‌سنجیِ کاملاً متفاوتی دارن (webhook در
# برابرِ verifyِ سرور-به-سرور با access_token)؛ برای همین provider جداست
# از Transaction.gateway (که فقط برای گزارش‌گیریِ درگاهِ واقعیه).
PROVIDER_CHOICES = [('gateway', 'درگاه پرداخت'), ('bazaar', 'پرداخت درون‌برنامه‌ای بازار')]
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
subscription_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به subscription.Subscription
amount = models.PositiveIntegerField()
currency = models.CharField(max_length=10, default='IRT')
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
provider = models.CharField(max_length=10, choices=PROVIDER_CHOICES, default='gateway')
# فقط وقتی provider == 'bazaar' پر می‌شن: SKU محصول درون‌برنامه‌ای تعریف‌شده
# در پنل بازار، و purchase token‌ای که کلاینت اندروید بعد از خرید از SDK
# بازار می‌گیره و برای 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)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"order-{self.pk}-user{self.user_id}"
class Transaction(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='transactions')
gateway = models.CharField(max_length=30, default='zarinpal')
gateway_ref_id = models.CharField(max_length=100, blank=True)
status = models.CharField(max_length=30)
paid_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
constraints = [
# یکتاییِ سراسریِ (gateway, gateway_ref_id) برای refهای غیرخالی:
# یک ref مشخص (مثلاً Authorityِ زرین‌پال) فقط می‌تونه متعلقِ یک
# Order باشه — هم جلوی duplicate رو می‌گیره هم جلوی reuse/replay
# برای Orderِ دیگه (نگاه کنید به apply_order_payment_result).
# gateway_ref_id__gt='' یعنی refِ خالی مشمول نیست.
models.UniqueConstraint(
fields=['gateway', 'gateway_ref_id'],
condition=models.Q(gateway_ref_id__gt=''),
name='uniq_transaction_gateway_ref_global',
),
]
def __str__(self):
return f"txn-{self.pk}-order{self.order_id}"