""" روش‌های پرداخت، هرکدام یک کلاس با یک رابط (الگوی Strategy)، و `provider_for`/`provider_for_method` که از روی `Order.provider` یا `paymentMethod`ِ API کلاسِ درست را برمی‌گردانند (Factory). ۲۰۲۶-۱۰-۱۲. پیش از این، تفاوتِ زرین‌پال و بازار با `if provider == 'bazaar'` در چند فایل پخش بود (ساختِ سفارش، کدِ تخفیف، بازسنجی، صفحه‌ی خرید)؛ افزودنِ روشِ سوم یعنی پیدا کردنِ همه‌ی آن‌ها. حالا روشِ تازه یک زیرکلاس است و یک خط در `PROVIDERS`. تأییدِ خود پرداخت (کال‌بکِ زرین‌پال، verifyِ بازار) عمداً بیرون مانده: دو مسیرِ HTTPِ کاملاً جدا با ورودی‌های متفاوت‌اند و در یک رابط فقط شکلِ مشترکِ ساختگی پیدا می‌کردند. روشِ ناشناخته خطا می‌دهد (KeyError/ValidationError)، نه پیش‌فرضِ زرین‌پال: سفارشی که روشش را نمی‌شناسیم نباید بی‌صدا به درگاهِ دیگری برود. """ from rest_framework.exceptions import ValidationError class PaymentProvider: #: مقدارِ `Order.provider` key: str #: مقدارِ `paymentMethod` در `POST /api/subscription/purchase` method: str def check_plan(self, plan) -> None: """اگر این پلن با این روش فروختنی نیست، ValidationError.""" def sku_for(self, plan) -> str: """شناسه‌ی محصول که روی سفارش می‌نشیند (فقط بازار).""" return '' def payment_url(self, order) -> str | None: """آدرسی که کاربر برای پرداخت به آن می‌رود؛ None یعنی پرداخت در اپ است، نه در مرورگر.""" raise NotImplementedError def purchase_extras(self, order, plan) -> dict: """فیلدهای اضافه‌ی پاسخِ خرید برای این روش.""" return {} def discount_blocker(self, order) -> str | None: """زیرِ قفلِ سفارش صدا زده می‌شود. متنِ خطا اگر کدِ تخفیف دیگر روی این سفارش نمی‌نشیند.""" return None def reconcile_user(self, user_id: int) -> int: """پرداخت‌هایی که تأییدشان نرسیده یا تمدیدشان دیده نشده را از خودِ درگاه می‌پرسد. خروجی: شمارِ تازه‌ها.""" return 0 class ZarinpalProvider(PaymentProvider): key = 'gateway' method = 'zarinpal' def payment_url(self, order) -> str: from .services import call_payment_gateway return call_payment_gateway(order) def discount_blocker(self, order) -> str | None: # بعد از رفتن به درگاه، authorityِ زرین‌پال با مبلغِ قبلی گرفته شده و دوباره استفاده می‌شود؛ تخفیف در این # لحظه یعنی کاربر مبلغِ قبلی را می‌پردازد ولی verify با مبلغِ جدید رد می‌شود. if order.gateway_authority: return 'کدِ تخفیف را پیش از رفتن به درگاهِ پرداخت وارد کنید.' return None def reconcile_user(self, user_id: int) -> int: from django.utils import timezone from .models import Order from .services import reconcile_pending_order since = timezone.now() - timezone.timedelta(days=2) return sum(reconcile_pending_order(order) for order in Order.objects.filter( user_id=user_id, status='pending', provider=self.key, created_at__gte=since, ).exclude(gateway_authority='')[:3]) class BazaarProvider(PaymentProvider): key = 'bazaar' method = 'bazaar' def check_plan(self, plan) -> None: # SKU فقط از پلن، هرگز از کلاینت. بی‌SKU یعنی این مورد در پیشخانِ بازار تعریف نشده. if not plan.bazaar_sku: raise ValidationError({'planId': 'این مورد هنوز در کافه‌بازار تعریف نشده است.'}) def sku_for(self, plan) -> str: return plan.bazaar_sku def payment_url(self, order) -> None: # اپِ اندروید با Poolakey می‌خرد و توکن را به /api/payment/bazaar/verify می‌فرستد. return None def purchase_extras(self, order, plan) -> dict: return {'bazaarSku': order.bazaar_product_sku} def discount_blocker(self, order) -> str | None: from . import bazaar # بی‌«تخفیف پویا» بازار قیمتِ ثابتِ پیشخان را می‌گیرد؛ تخفیف آن‌وقت فقط کد را می‌سوزاند. if not bazaar.dynamic_price_configured(): return 'کدِ تخفیف روی خرید از کافه‌بازار اعمال نمی‌شود.' # قیمت در JWT امضا شده و خرید در بازار شروع شده است (BazaarStartPurchaseView). if order.bazaar_price_rial is not None: return 'کدِ تخفیف را پیش از رفتن به صفحه‌ی خریدِ بازار وارد کنید.' return None def reconcile_user(self, user_id: int) -> int: from .services import sync_bazaar_renewals return sync_bazaar_renewals(user_id) PROVIDERS = (ZarinpalProvider(), BazaarProvider()) _BY_KEY = {p.key: p for p in PROVIDERS} _BY_METHOD = {p.method: p for p in PROVIDERS} def provider_for(key: str) -> PaymentProvider: """از `Order.provider`. مقدارِ ناشناخته KeyError است (داده‌ی خراب، نه ورودیِ کاربر).""" return _BY_KEY[key] def provider_for_method(method: str) -> PaymentProvider: """از `paymentMethod`ِ درخواست. ناشناخته ۴۰۰.""" try: return _BY_METHOD[method] except KeyError: raise ValidationError({'paymentMethod': f'روشِ پرداختِ ناشناخته: {method}'}) from None