Files
MathApp/payment/views.py
2026-08-11 16:43:12 +03:30

175 lines
8.2 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 django.db import transaction
from django.shortcuts import get_object_or_404
from django.utils import timezone
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 .models import Order, Transaction
from .permissions import HasInternalServiceKey, HasWebhookSecret
from .serializers import (
CreateOrderRequestSerializer,
CreateOrderResponseSerializer,
OrderDetailResponseSerializer,
WebhookRequestSerializer,
WebhookResponseSerializer,
)
from .services import 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'),
)
data = {'orderId': str(order.id), 'status': order.status, 'paymentUrl': payment_url}
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'])
already_paid = order.status == 'paid'
# ⚠️ اصلاح امنیتی/صحت داده: قبلاً هر بار فراخوانی webhook (حتی
# اگه درگاه به‌خاطر عدم دریافت ۲۰۰ یا هر دلیل دیگه‌ای همون
# کال‌بک رو دوباره می‌فرستاد) یک رکورد Transaction جدید می‌ساخت.
# حالا اگه رکوردی با همین order/status/gatewayRefId از قبل
# موجود باشه، تکراری در نظر گرفته می‌شه و نه Transaction جدید
# ساخته می‌شه و نه اثرات جانبی (فعال‌سازی اشتراک/notify) دوباره
# اجرا می‌شن.
is_duplicate = Transaction.objects.filter(
order=order, status=v['status'], gateway_ref_id=gateway_ref_id,
).exists()
if not is_duplicate:
order.status = v['status']
order.save(update_fields=['status'])
Transaction.objects.create(
order=order,
gateway=v.get('gateway') or 'zarinpal',
gateway_ref_id=gateway_ref_id,
status=v['status'],
paid_at=timezone.now() if v['status'] == 'paid' else None,
)
if v['status'] == 'paid' and not already_paid and not is_duplicate:
if order.subscription_id:
# فعال‌سازی اشتراک بعد از پرداخت موفق. ارتباط با اپ
# subscription هم مثل create_order یک import مستقیم به
# تابع سرویسه (رفرنس سبک با id، نه FK واقعی و نه HTTP
# call به خودمون).
from subscription.services import activate_subscription
activate_subscription(order.subscription_id)
# رویداد order_paid طبق سند notifications باید ساطع بشه؛
# قبلاً این فراخوانی جا افتاده بود (فقط اشتراک فعال می‌شد).
from notifications.services import notify
notify(
user_id=order.user_id,
type='order_paid',
title='پرداخت با موفقیت انجام شد',
related_id=order.id,
)
return Response(WebhookResponseSerializer({'received': True}).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)