""" منطق مشترک اپ Payment که هم از views.py همین اپ و هم به‌صورت سرور-به-سرور از اپ‌های دیگه (مثل subscription) صدا زده می‌شه. چون این پروژه یک مونولیت جنگویی‌ه (نه میکروسرویس واقعی)، «فراخوانی سرور-به-سرور» با یک import مستقیم پیاده‌سازی شده، نه یک HTTP call به خودمون. """ from django.db import IntegrityError from django.db import transaction as db_transaction from django.utils.crypto import get_random_string from rest_framework.exceptions import APIException from .models import Order class IdempotencyKeyConflict(APIException): """ ⚠️ اصلاح امنیتی: قبلاً create_order فقط با idempotency_key سفارش رو get_or_create می‌کرد، بدون بررسی اینکه صاحب سفارش موجود همون کاربرِ درخواست‌دهنده‌ست یا پارامترها (amount/currency/subscription) یکی‌ان. این یعنی یک کاربر می‌تونست با حدس زدن/استفاده‌ی مجدد از یک Idempotency-Key، سفارشِ کاربر دیگه‌ای رو برگردونه/استفاده کنه، یا با همون کلید ولی amount متفاوت، رفتار غیرمنتظره بگیره. حالا این حالت‌ها 409 Conflict برمی‌گردونن. """ status_code = 409 default_detail = 'این Idempotency-Key قبلاً برای درخواست دیگری استفاده شده است.' default_code = 'idempotency_key_conflict' def _ensure_idempotent_match(order: Order, *, user_id: int, amount: int, currency: str, subscription_id: int | None) -> None: if order.user_id != user_id: raise IdempotencyKeyConflict( 'این Idempotency-Key متعلق به کاربر دیگری است و نمی‌تواند استفاده شود.' ) if (order.amount != amount or order.currency != currency or order.subscription_id != subscription_id): raise IdempotencyKeyConflict( 'این Idempotency-Key قبلاً با پارامترهای متفاوتی استفاده شده است.' ) def call_payment_gateway(order: Order) -> str: """ استاب فراخوانی درگاه پرداخت واقعی (zarinpal و غیره) — دقیقاً مثل الگوی generate_tutor_answer در ai_tutor/views.py. فعلاً فقط یک paymentUrl قطعی و ساختگی برمی‌گردونه تا کل جریان (ساخت سفارش → پرداخت → webhook) بدون نیاز به کلید API واقعی درگاه، از طریق Swagger قابل تست باشه. برای اتصال واقعی، این تابع را با فراخوانی HTTP به API درگاه (مثلاً zarinpal.com/pg/rest/WebGate/PaymentRequest.json) جایگزین کنید. """ return f'https://payment.gateway/pay/{order.id}' def create_order(*, user_id: int, amount: int, currency: str = 'IRT', subscription_id: int | None = None, idempotency_key: str | None = None): """ ساخت سفارش با پشتیبانی از Idempotency-Key (بخش ۴.۳ سند API): اگه سفارشی با همین کلید قبلاً ساخته شده باشه، همون رکورد برگردونده می‌شه، نه یک رکورد تکراری (مثلاً وقتی کلاینت درخواست رو به‌خاطر timeout دوباره می‌فرسته) — اما فقط وقتی سفارشِ موجود واقعاً متعلق به همین کاربر و همین پارامترهاست (وگرنه IdempotencyKeyConflict می‌گیره؛ نگاه کنید به توضیح بالای این فایل). """ if not idempotency_key: idempotency_key = get_random_string(32) existing = Order.objects.filter(idempotency_key=idempotency_key).first() if existing is not None: _ensure_idempotent_match( existing, user_id=user_id, amount=amount, currency=currency, subscription_id=subscription_id, ) payment_url = call_payment_gateway(existing) return existing, payment_url try: with db_transaction.atomic(): order = Order.objects.create( user_id=user_id, subscription_id=subscription_id, amount=amount, currency=currency, status='pending', idempotency_key=idempotency_key, ) except IntegrityError: # race condition: درخواست هم‌زمان دیگه‌ای با همین کلید زودتر ساخت. order = Order.objects.filter(idempotency_key=idempotency_key).first() if order is None: raise _ensure_idempotent_match( order, user_id=user_id, amount=amount, currency=currency, subscription_id=subscription_id, ) payment_url = call_payment_gateway(order) return order, payment_url