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 # پلنی که *این* سفارش برایش پرداخت می‌شود. پیش‌تر پلن فقط روی Subscription # بود و با شروعِ یک خریدِ دیگر عوض می‌شد: پرداختِ سفارشِ ماهانه بعد از # شروعِ خریدِ سالانه، اشتراکِ سالانه می‌داد (بازتولید شد، ۲۰۲۶-۰۹-۲۵). plan_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به subscription.Plan 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}"