Files
MathApp/payment/serializers.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

75 lines
3.7 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 rest_framework import serializers
class CreateOrderRequestSerializer(serializers.Serializer):
# این فراخوانی سرور-به-سرور از اپ Subscription‌ه، نه از کلاینت؛ به همین
# دلیل userId اینجا مستقیماً در بدنه ارسال می‌شه (نه از توکن استخراج).
userId = serializers.CharField()
amount = serializers.IntegerField()
currency = serializers.CharField(required=False, default='IRT')
subscriptionId = serializers.CharField(required=False, allow_null=True, allow_blank=True)
# provider='bazaar' وقتی کاربر از همون ابتدا پرداخت درون‌برنامه‌ای بازار
# رو انتخاب کرده؛ در این حالت amount باید دقیقاً برابر قیمتِ محصولِ
# تعریف‌شده در پنل بازار برای همون bazaarProductSku باشه (بازار خودش
# قیمت رو کنترل می‌کنه، نه ما).
provider = serializers.ChoiceField(choices=['gateway', 'bazaar'], required=False, default='gateway')
bazaarProductSku = serializers.CharField(required=False, allow_blank=True)
class BazaarVerifyRequestSerializer(serializers.Serializer):
orderId = serializers.CharField()
productSku = serializers.CharField(required=False, allow_blank=True)
purchaseToken = serializers.CharField()
class BazaarVerifyResponseSerializer(serializers.Serializer):
orderId = serializers.CharField()
status = serializers.CharField()
valid = serializers.BooleanField()
class CreateOrderResponseSerializer(serializers.Serializer):
orderId = serializers.CharField()
status = serializers.CharField()
# provider='bazaar' یعنی paymentUrl نداریم (کلاینت با SDK بازار خودش
# خرید می‌کنه، نه با ریدایرکت به یک URL).
paymentUrl = serializers.CharField(allow_null=True)
provider = serializers.CharField()
class WebhookRequestSerializer(serializers.Serializer):
# کال‌بک از سمت درگاه پرداخت میاد، نه از اپ
orderId = serializers.CharField()
gateway = serializers.CharField(required=False, default='zarinpal')
gatewayRefId = serializers.CharField(required=False, allow_blank=True)
# ⚠️ اصلاح امنیتی: قبلاً status یک CharField آزاد بود و هر مقداری
# (حتی مقادیر پشتیبانی‌نشده/اشتباه) رو مستقیم روی Order ذخیره می‌کرد.
# حالا فقط دو وضعیت نهاییِ معتبرِ کال‌بک درگاه پذیرفته می‌شه؛ 'pending'
# وضعیت اولیه‌ی داخلیه و نباید از بیرون توسط webhook قابل‌تنظیم باشه.
status = serializers.ChoiceField(choices=['paid', 'failed'])
class WebhookResponseSerializer(serializers.Serializer):
received = serializers.BooleanField()
class ZarinpalCallbackResponseSerializer(serializers.Serializer):
"""فقط وقتی settings.ZARINPAL_REDIRECT_URL خالی باشه برگردونده می‌شه؛
وگرنه کاربر به همون URL ریدایرکت می‌شه، نه به این JSON."""
orderId = serializers.CharField()
status = serializers.CharField()
class TransactionSerializer(serializers.Serializer):
id = serializers.CharField()
gateway = serializers.CharField()
status = serializers.CharField()
paidAt = serializers.DateTimeField(allow_null=True)
class OrderDetailResponseSerializer(serializers.Serializer):
orderId = serializers.CharField()
status = serializers.CharField()
amount = serializers.IntegerField()
transactions = TransactionSerializer(many=True)