from django.db import transaction from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from . import bazaar from .models import Order, Transaction from .permissions import HasInternalServiceKey, HasWebhookSecret from .serializers import ( BazaarVerifyRequestSerializer, BazaarVerifyResponseSerializer, CreateOrderRequestSerializer, CreateOrderResponseSerializer, OrderDetailResponseSerializer, WebhookRequestSerializer, WebhookResponseSerializer, ) from .services import apply_order_payment_result, create_order class CreateOrderView(APIView): """ POST /api/payment/orders — ساخت سفارش (فراخوانی‌شده از اپ Subscription) طبق سند API، این یک فراخوانی سرور-به-سرور از اپ Subscription‌ه، نه از کلاینت. در عمل subscription/views.py مستقیماً تابع create_order رو import و صدا می‌زنه، پس این endpoint برای جریان عادی از بیرون صدا زده نمی‌شه؛ برای همین به‌جای AllowAny یک shared-secret داخلی (HasInternalServiceKey، هدر X-Internal-Key) لازمه، وگرنه هر کلاینتی می‌تونست با یک userId دلخواه سفارش بسازه. """ permission_classes = [HasInternalServiceKey] # ⚠️ افزوده شده: هدرهای موردنیاز (X-Internal-Key, Idempotency-Key) قبلاً # مستند نشده بودن و از Swagger UI اصلاً قابل ست‌کردن/تست‌کردن نبودن. @extend_schema( request=CreateOrderRequestSerializer, responses=CreateOrderResponseSerializer, parameters=[ OpenApiParameter( 'X-Internal-Key', OpenApiTypes.STR, OpenApiParameter.HEADER, required=True, description='کلید مخفیِ فراخوانی داخلی/سرور-به-سرور (settings.INTERNAL_SERVICE_KEY).', ), OpenApiParameter( 'Idempotency-Key', OpenApiTypes.STR, OpenApiParameter.HEADER, required=False, description=( 'کلید یکتای idempotency برای جلوگیری از ساخت سفارش تکراری هنگام retry. ' 'در صورت عدم ارسال، یک کلید تصادفی ساخته می‌شود.' ), ), ], tags=['Payment (payment)'], ) def post(self, request): serializer = CreateOrderRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) v = serializer.validated_data order, payment_url = create_order( user_id=int(v['userId']), amount=v['amount'], currency=v.get('currency') or 'IRT', subscription_id=int(v['subscriptionId']) if v.get('subscriptionId') else None, idempotency_key=request.headers.get('Idempotency-Key'), provider=v.get('provider') or 'gateway', bazaar_product_sku=v.get('bazaarProductSku') or '', ) data = { 'orderId': str(order.id), 'status': order.status, 'paymentUrl': payment_url, 'provider': order.provider, } return Response(CreateOrderResponseSerializer(data).data) class PaymentWebhookView(APIView): """ POST /api/payment/webhook — کال‌بک درگاه پرداخت (از سمت درگاه میاد، نه کلاینت) محافظت‌شده با HasWebhookSecret (هدر X-Webhook-Secret) تا هر کلاینتی نتونه با یک orderId معتبر و status=paid، بدون پرداخت واقعی، اشتراک فعال کنه. همچنین idempotent شده: اگه سفارش از قبل paid باشه، دوباره Transaction/notify تکراری نمی‌سازه (مثلاً وقتی درگاه callback رو دوباره می‌فرسته). """ permission_classes = [HasWebhookSecret] # ⚠️ افزوده شده: مستندسازی هدر X-Webhook-Secret که قبلاً در Swagger UI # قابل تنظیم/تست نبود. @extend_schema( request=WebhookRequestSerializer, responses=WebhookResponseSerializer, parameters=[ OpenApiParameter( 'X-Webhook-Secret', OpenApiTypes.STR, OpenApiParameter.HEADER, required=True, description='کلید مخفیِ وب‌هوک درگاه پرداخت (settings.PAYMENT_WEBHOOK_SECRET).', ), ], tags=['Payment (payment)'], ) def post(self, request): serializer = WebhookRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) v = serializer.validated_data gateway_ref_id = v.get('gatewayRefId', '') with transaction.atomic(): order = get_object_or_404(Order.objects.select_for_update(), pk=v['orderId']) # ⚠️ اصلاح امنیتی/صحت داده (قبلی): هر بار فراخوانی webhook (حتی # تکراری از سمت درگاه) یک Transaction جدید می‌ساخت. حالا این # idempotency + اثرات جانبی (فعال‌سازی اشتراک/notify/statuslog) # داخل apply_order_payment_result مشترکه (هم اینجا هم بازار). apply_order_payment_result( order=order, gateway=v.get('gateway') or 'zarinpal', gateway_ref_id=gateway_ref_id, new_status=v['status'], event='gateway_webhook', ) return Response(WebhookResponseSerializer({'received': True}).data) class BazaarVerifyPurchaseView(APIView): """ POST /api/payment/bazaar/verify — تایید پرداخت درون‌برنامه‌ای بازار برخلاف درگاه معمولی که با webhook خودِ درگاه تایید می‌شه، در پرداخت درون‌برنامه‌ای بازار هیچ webhookی نیست: کلاینت اندروید بعد از خرید از SDK بازار (IInAppBillingService)، یک purchaseToken می‌گیره و باید همون رو برای این endpoint بفرسته تا بک‌اند با فراخوانی سرور-به-سرور به API رسمی بازار (bazaar.verify_purchase)، صحتش رو تایید کنه — مشابه دقیقاً همون چیزی که سند رسمی بازار توصیه می‌کنه: «هیچ‌وقت فقط به گفته‌ی کلاینت اعتماد نکنید، حتماً سمت سرور verify کنید». """ permission_classes = [IsAuthenticated] @extend_schema( request=BazaarVerifyRequestSerializer, responses=BazaarVerifyResponseSerializer, tags=['Payment (payment)'], ) def post(self, request): serializer = BazaarVerifyRequestSerializer(data=request.data) serializer.is_valid(raise_exception=True) v = serializer.validated_data # ⚛️🔒 اصلاح: فراخوانی HTTP به API بازار (تا ~۲۰ ثانیه، دو درخواست # پشت‌سرهم برای refresh token + verify) قبلاً *داخل* transaction.atomic # و زیر select_for_update روی Order انجام می‌شد؛ یعنی ردیف Order (و # هر تراکنش دیگه‌ای که بخواد همون Order یا Subscription مرتبط رو لمس # کنه) تا برگشتِ پاسخ بازار قفل می‌موند. حالا verify_purchase قبل از # باز شدن atomic/lock صدا زده می‌شه؛ لاک فقط برای اعمال نتیجه (چند # میلی‌ثانیه) گرفته می‌شه. order_lookup = get_object_or_404( Order, pk=v['orderId'], user_id=request.user.id, provider='bazaar', ) result = bazaar.verify_purchase( product_sku=order_lookup.bazaar_product_sku or v['productSku'], purchase_token=v['purchaseToken'], ) with transaction.atomic(): order = get_object_or_404( Order.objects.select_for_update(), pk=v['orderId'], user_id=request.user.id, provider='bazaar', ) new_status = 'paid' if result.is_valid else 'failed' order.bazaar_purchase_token = v['purchaseToken'] order.save(update_fields=['bazaar_purchase_token']) apply_order_payment_result( order=order, gateway='bazaar', gateway_ref_id=v['purchaseToken'], new_status=new_status, event='bazaar_verify', ) data = {'orderId': str(order.id), 'status': order.status, 'valid': result.is_valid} return Response(BazaarVerifyResponseSerializer(data).data) class OrderDetailView(APIView): """GET /api/payment/orders/{id} — وضعیت سفارش""" permission_classes = [IsAuthenticated] @extend_schema(responses=OrderDetailResponseSerializer, tags=['Payment (payment)']) def get(self, request, order_id): order = get_object_or_404(Order, pk=order_id, user_id=request.user.id) data = { 'orderId': str(order.id), 'status': order.status, 'amount': order.amount, 'transactions': [ { 'id': str(t.id), 'gateway': t.gateway, 'status': t.status, 'paidAt': t.paid_at, } for t in order.transactions.all() ], } return Response(OrderDetailResponseSerializer(data).data)