Initial commit - clean project structure
This commit is contained in:
201
payment/views.py
Normal file
201
payment/views.py
Normal file
@@ -0,0 +1,201 @@
|
||||
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
|
||||
|
||||
with transaction.atomic():
|
||||
order = get_object_or_404(
|
||||
Order.objects.select_for_update(),
|
||||
pk=v['orderId'], user_id=request.user.id, provider='bazaar',
|
||||
)
|
||||
|
||||
result = bazaar.verify_purchase(
|
||||
product_sku=order.bazaar_product_sku or v['productSku'],
|
||||
purchase_token=v['purchaseToken'],
|
||||
)
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user