Initial commit
This commit is contained in:
0
payment/__init__.py
Normal file
0
payment/__init__.py
Normal file
21
payment/admin.py
Normal file
21
payment/admin.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Order, Transaction
|
||||
|
||||
|
||||
class TransactionInline(admin.TabularInline):
|
||||
model = Transaction
|
||||
extra = 0
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'user_id', 'subscription_id', 'amount', 'currency', 'status', 'created_at')
|
||||
list_filter = ('status', 'currency')
|
||||
inlines = [TransactionInline]
|
||||
|
||||
|
||||
@admin.register(Transaction)
|
||||
class TransactionAdmin(admin.ModelAdmin):
|
||||
list_display = ('id', 'order', 'gateway', 'status', 'paid_at')
|
||||
list_filter = ('gateway', 'status')
|
||||
6
payment/apps.py
Normal file
6
payment/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PaymentConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'payment'
|
||||
46
payment/migrations/0001_initial.py
Normal file
46
payment/migrations/0001_initial.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 5.2.17 on 2026-08-10 12:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user_id', models.IntegerField(db_index=True)),
|
||||
('subscription_id', models.IntegerField(blank=True, null=True)),
|
||||
('amount', models.PositiveIntegerField()),
|
||||
('currency', models.CharField(default='IRT', max_length=10)),
|
||||
('status', models.CharField(choices=[('pending', 'در انتظار'), ('paid', 'پرداختشده'), ('failed', 'ناموفق')], default='pending', max_length=10)),
|
||||
('idempotency_key', models.CharField(max_length=100, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('gateway', models.CharField(default='zarinpal', max_length=30)),
|
||||
('gateway_ref_id', models.CharField(blank=True, max_length=100)),
|
||||
('status', models.CharField(max_length=30)),
|
||||
('paid_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='payment.order')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
payment/migrations/__init__.py
Normal file
0
payment/migrations/__init__.py
Normal file
38
payment/models.py
Normal file
38
payment/models.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
"""
|
||||
تنها اپی که واقعاً پول جابهجا میکنه. عمداً از Subscription جدا شده
|
||||
چون منطق و ریسکش فرق داره (درگاه، retry، idempotency).
|
||||
"""
|
||||
STATUS_CHOICES = [('pending', 'در انتظار'), ('paid', 'پرداختشده'), ('failed', 'ناموفق')]
|
||||
|
||||
user_id = models.IntegerField(db_index=True) # ارجاع سبک به accounts.User
|
||||
subscription_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به subscription.Subscription
|
||||
amount = models.PositiveIntegerField()
|
||||
currency = models.CharField(max_length=10, default='IRT')
|
||||
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
|
||||
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']
|
||||
|
||||
def __str__(self):
|
||||
return f"txn-{self.pk}-order{self.order_id}"
|
||||
31
payment/permissions.py
Normal file
31
payment/permissions.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.conf import settings
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
||||
class HasInternalServiceKey(BasePermission):
|
||||
"""
|
||||
برای endpointهایی مثل POST /api/payment/orders که طبق سند فقط قراره
|
||||
سرور-به-سرور (از اپ Subscription) صدا زده بشن، نه از یک کلاینت دلخواه.
|
||||
درخواست باید هدر X-Internal-Key رو برابر settings.INTERNAL_SERVICE_KEY
|
||||
بفرسته؛ در غیر این صورت 403 برمیگرده.
|
||||
"""
|
||||
|
||||
message = 'این endpoint فقط برای فراخوانی داخلی/سرور-به-سرور است.'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
provided = request.headers.get('X-Internal-Key', '')
|
||||
return bool(provided) and provided == settings.INTERNAL_SERVICE_KEY
|
||||
|
||||
|
||||
class HasWebhookSecret(BasePermission):
|
||||
"""
|
||||
برای POST /api/payment/webhook: باید از سمت درگاه پرداخت بیاد، نه یک
|
||||
کلاینت دلخواه. تا زمان اتصال درگاه واقعی (که خودش امضای HMAC داره)،
|
||||
یک shared-secret ساده از طریق هدر X-Webhook-Secret بررسی میشه.
|
||||
"""
|
||||
|
||||
message = 'امضای وبهوک نامعتبر است.'
|
||||
|
||||
def has_permission(self, request, view):
|
||||
provided = request.headers.get('X-Webhook-Secret', '')
|
||||
return bool(provided) and provided == settings.PAYMENT_WEBHOOK_SECRET
|
||||
46
payment/serializers.py
Normal file
46
payment/serializers.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class CreateOrderRequestSerializer(serializers.Serializer):
|
||||
# این فراخوانی سرور-به-سرور از اپ Subscriptionه، نه از کلاینت؛ به همین
|
||||
# دلیل userId اینجا مستقیماً در بدنه ارسال میشه (نه از توکن استخراج).
|
||||
userId = serializers.CharField()
|
||||
amount = serializers.IntegerField()
|
||||
currency = serializers.CharField(required=False, default='IRT')
|
||||
subscriptionId = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
|
||||
class CreateOrderResponseSerializer(serializers.Serializer):
|
||||
orderId = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
paymentUrl = serializers.CharField()
|
||||
|
||||
|
||||
class WebhookRequestSerializer(serializers.Serializer):
|
||||
# کالبک از سمت درگاه پرداخت میاد، نه از اپ
|
||||
orderId = serializers.CharField()
|
||||
gateway = serializers.CharField(required=False, default='zarinpal')
|
||||
gatewayRefId = serializers.CharField(required=False, allow_blank=True)
|
||||
# ⚠️ اصلاح امنیتی: قبلاً status یک CharField آزاد بود و هر مقداری
|
||||
# (حتی مقادیر پشتیبانینشده/اشتباه) رو مستقیم روی Order ذخیره میکرد.
|
||||
# حالا فقط دو وضعیت نهاییِ معتبرِ کالبک درگاه پذیرفته میشه؛ 'pending'
|
||||
# وضعیت اولیهی داخلیه و نباید از بیرون توسط webhook قابلتنظیم باشه.
|
||||
status = serializers.ChoiceField(choices=['paid', 'failed'])
|
||||
|
||||
|
||||
class WebhookResponseSerializer(serializers.Serializer):
|
||||
received = serializers.BooleanField()
|
||||
|
||||
|
||||
class TransactionSerializer(serializers.Serializer):
|
||||
id = serializers.CharField()
|
||||
gateway = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
paidAt = serializers.DateTimeField(allow_null=True)
|
||||
|
||||
|
||||
class OrderDetailResponseSerializer(serializers.Serializer):
|
||||
orderId = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
amount = serializers.IntegerField()
|
||||
transactions = TransactionSerializer(many=True)
|
||||
98
payment/services.py
Normal file
98
payment/services.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
منطق مشترک اپ 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
|
||||
3
payment/tests.py
Normal file
3
payment/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
9
payment/urls.py
Normal file
9
payment/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import CreateOrderView, OrderDetailView, PaymentWebhookView
|
||||
|
||||
urlpatterns = [
|
||||
path('payment/orders', CreateOrderView.as_view(), name='payment-create-order'),
|
||||
path('payment/webhook', PaymentWebhookView.as_view(), name='payment-webhook'),
|
||||
path('payment/orders/<int:order_id>', OrderDetailView.as_view(), name='payment-order-detail'),
|
||||
]
|
||||
174
payment/views.py
Normal file
174
payment/views.py
Normal file
@@ -0,0 +1,174 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user