Browse Source

better transaction response with payment methods added.

master
Mohsen Taba 5 months ago
parent
commit
8d548423ad
  1. 208
      apps/transaction/doc.py
  2. 25
      apps/transaction/migrations/0004_transactionparticipant_payment_method.py
  3. 26
      apps/transaction/migrations/0005_alter_transactionparticipant_payment_method.py
  4. 8
      apps/transaction/models.py
  5. 155
      apps/transaction/views.py

208
apps/transaction/doc.py

@ -1,3 +1,7 @@
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from rest_framework import status
def doc_upload_transaction_receipts():
return """
# 🐈 Scenario
@ -428,11 +432,19 @@ POST /api/transactions/<slug>/join/
2. **دوره پولی**:
- تراکنش با وضعیت 'pending' ایجاد میشود
- کاربر باید رسید پرداخت خود را آپلود کند
- پس از آپلود رسید، وضعیت به 'waiting_approval' تغییر میکند
- پس از تایید توسط ادمین، وضعیت به 'success' تغییر میکند
- سیستم بر اساس موقعیت جغرافیایی کاربر، روش پرداخت مناسب را تعیین میکند
3. **روش پرداخت (Payment Method)**:
- **Payment_Gateway**: برای کاربران غیر روسی - پرداخت از طریق درگاه پرداخت آنلاین
- **Receipt**: برای کاربران روسی - آپلود رسید پرداخت از طریق واتساپ
3. **احراز هویت**:
4. **تشخیص موقعیت جغرافیایی**:
- ابتدا از هدر Cloudflare (`CF-IPCountry`) استفاده میشود
- در صورت عدم وجود، از پایگاه داده GeoIP محلی استفاده میشود
- کاربران روسی روش پرداخت Receipt دریافت میکنند
- کاربران سایر کشورها روش پرداخت Payment_Gateway دریافت میکنند
5. **احراز هویت**:
- باید توکن احراز هویت را در هدر درخواست ارسال کنید
---
@ -445,6 +457,29 @@ POST /api/transactions/<slug>/join/
| `400` | دادههای نامعتبر |
| `404` | دوره یافت نشد |
### ساختار پاسخ:
| کلید | نوع داده | توضیحات |
|---------------------|-----------|----------------------------------------------------------|
| `message` | String | پیام موفقیتآمیز |
| `transaction_id` | Integer | شناسه تراکنش ایجاد شده |
| `payment_method` | String | روش پرداخت (Payment_Gateway یا Receipt) |
| `payment_link` | String | لینک پرداخت (فقط برای Payment_Gateway) |
| `participant_infos` | Array | لیست اطلاعات شرکتکنندگان |
---
## 💳 روش‌های پرداخت:
### Payment_Gateway (درگاه پرداخت):
- **کاربران**: غیر روسی
- **اقدام کاربر**: کلیک روی `payment_link` و پرداخت آنلاین
- **فرآیند**: پرداخت مستقیم از طریق درگاه پرداخت
### Receipt (رسید پرداخت):
- **کاربران**: روسی
- **اقدام کاربر**: آپلود رسید پرداخت از طریق واتساپ
- **فرآیند**: آپلود رسید بررسی توسط ادمین تایید پرداخت
---
## 📄 نمونه درخواست (JSON Body):
@ -465,12 +500,14 @@ POST /api/transactions/<slug>/join/
---
## 📄 نمونه پاسخ موفقیت‌آمیز (دوره رایگان):
## 📄 نمونه پاسخ (دوره رایگان):
```json
{
"message": "Transaction Participant created successfully.",
"transaction_id": 123,
"payment_method": Free,
"payment_link": null,
"participant_infos": [
{
"fullname": "علی رضایی",
@ -485,25 +522,49 @@ POST /api/transactions/<slug>/join/
---
## 📄 نمونه پاسخ موفقیت‌آمیز (دوره پولی):
## 📄 نمونه پاسخ (دوره پولی - Payment_Gateway):
```json
{
"message": "Transaction Participant created successfully.",
"transaction_id": 124,
"transaction_id": 374,
"payment_method": "Payment_Gateway",
"payment_link": "https://russia-payment.com/pay/374",
"participant_infos": [
{
"fullname": "سارا احمدی",
"email": "sara@example.com",
"phone_number": "+989123456789",
"gender": "female",
"birthdate": "1998-03-20"
"fullname": "John Doe",
"email": "john@example.com",
"phone_number": "+1234567890",
"gender": "male",
"birthdate": "1990-01-01"
}
]
}
```
---
## 📄 نمونه پاسخ (دوره پولی - Receipt):
```json
{
"message": "Transaction Participant created successfully.",
"transaction_id": 375,
"payment_method": "receipt",
"payment_link": null,
"participant_infos": [
{
"fullname": "Иван Иванов",
"email": "ivan@example.ru",
"phone_number": "+71234567890",
"gender": "male",
"birthdate": "1992-05-15"
}
]
}
```
توجه: برای دوره پولی، شما باید با استفاده از `transaction_id` بازگشتی، رسید پرداخت خود را از طریق API آپلود رسید آپلود کنید.
**نکته**: برای روش پرداخت Receipt، کاربر باید رسید پرداخت خود را از طریق واتساپ آپلود کند.
---
@ -527,3 +588,124 @@ curl -X POST \\
}'
```
"""
hadis_list_swagger = swagger_auto_schema(
operation_description="""
Retrieve a paginated list of Hadis (traditions) for a specific category.
**Key Features:**
- Returns hadis entries filtered by category ID
- Supports pagination for large datasets
- Translations are automatically provided based on the Accept-Language header
- Each hadis includes its category information, title, narrator, Arabic text, and translation
**Usage:**
- Use this endpoint to browse hadis within a specific category
- The response includes pagination links (next/previous) for navigation
- Set the Accept-Language header to get translations in your preferred language (en, fa, ar, ur)
- Only active (status=True) hadis are returned
**Response Structure:**
- `count`: Total number of hadis in the category
- `next`: URL for the next page (null if on last page)
- `previous`: URL for the previous page (null if on first page)
- `results`: Array of hadis objects with full details
""",
operation_summary="List Hadis by Category",
tags=['Hadis'],
manual_parameters=[
openapi.Parameter(
'category_slug',
openapi.IN_PATH,
description="Unique identifier of the Hadis category. Must be a valid category ID that exists in the system.",
type=openapi.TYPE_STRING,
required=True,
example='-330'
),
openapi.Parameter(
'page',
openapi.IN_QUERY,
description="Page number for pagination. Starts from 1. If not provided, returns the first page.",
type=openapi.TYPE_INTEGER,
required=False,
example=1
),
openapi.Parameter(
'Accept-Language',
openapi.IN_HEADER,
description="Language code for translations. Supported codes: 'en' (English), 'fa' (Persian), 'ar' (Arabic), 'ur' (Urdu). Defaults to 'en' if not specified.",
type=openapi.TYPE_STRING,
required=False,
default='en',
enum=['en', 'fa', 'ar', 'ur']
)
],
responses={
status.HTTP_200_OK: openapi.Response(
description="Successfully retrieved paginated list of hadis for the specified category",
examples={
"application/json": {
"count": 150,
"next": "http://example.com/api/hadis/category/1/?page=2",
"previous": None,
"results": [
{
"id": 1,
"number": 1,
"title": "The Opening",
"title_narrator": "From Abu Hurairah",
"text": "إنما الأعمال بالنيات وإنما لكل امرئ ما نوى",
"translation": "Actions are but by intention, and every man shall have only what he intended",
"category": {
"id": 1,
"title": "Book of Faith",
"slug": "book-of-faith",
"source_type": "hadith",
"sect_type": "sunni"
},
"status": {
"id": 130,
"title": "Прерванный",
"color": "orange"
},
"share_link": "http://example.com/hadis/1"
},
{
"id": 2,
"number": 2,
"title": "The Second Hadith",
"title_narrator": "From Umar ibn al-Khattab",
"text": "بينما نحن عند رسول الله صلى الله عليه وسلم ذات يوم",
"translation": "While we were sitting with the Messenger of Allah (peace be upon him) one day",
"category": {
"id": 1,
"title": "Book of Faith",
"slug": "book-of-faith",
"source_type": "hadith",
"sect_type": "sunni"
},
"status": {
"id": 130,
"title": "Прерванный",
"color": "orange"
},
"share_link": "http://example.com/hadis/2"
}
]
}
}
),
status.HTTP_404_NOT_FOUND: openapi.Response(
description="The specified category ID does not exist or the category has no active hadis",
examples={
"application/json": {
"detail": "Not found."
}
}
),
status.HTTP_500_INTERNAL_SERVER_ERROR: openapi.Response(
description="Internal server error occurred while processing the request"
)
}
)

25
apps/transaction/migrations/0004_transactionparticipant_payment_method.py

@ -0,0 +1,25 @@
# Generated by Django 4.2.27 on 2025-12-28 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("transaction", "0003_alter_transactionparticipant_status_and_more"),
]
operations = [
migrations.AddField(
model_name="transactionparticipant",
name="payment_method",
field=models.CharField(
choices=[
("receipt", "Receipt"),
("Payment_Gateway", "Payment Gateway"),
],
default="Payment_Gateway",
max_length=20,
verbose_name="Transaction Payment Method",
),
),
]

26
apps/transaction/migrations/0005_alter_transactionparticipant_payment_method.py

@ -0,0 +1,26 @@
# Generated by Django 4.2.27 on 2025-12-28 12:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("transaction", "0004_transactionparticipant_payment_method"),
]
operations = [
migrations.AlterField(
model_name="transactionparticipant",
name="payment_method",
field=models.CharField(
choices=[
("receipt", "Receipt"),
("free", "Free"),
("Payment_Gateway", "Payment Gateway"),
],
default="Payment_Gateway",
max_length=20,
verbose_name="Transaction Payment Method",
),
),
]

8
apps/transaction/models.py

@ -25,8 +25,14 @@ class TransactionParticipant(models.Model):
SUCCESS = 'success', _('Success')
FAILED = 'failed', _('Failed')
class PaymentMethods(models.TextChoices):
RECEIPT = 'receipt', _('Receipt')
FREE = 'free', _('Free')
PAYMENT_GATEWAY = 'Payment_Gateway', _('Payment Gateway')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions')
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_transactions')
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_transactions')
payment_method=models.CharField(max_length=20, choices=PaymentMethods.choices, default=PaymentMethods.PAYMENT_GATEWAY, verbose_name=_('Transaction Payment Method'))
# is_paid = models.BooleanField(default=False, verbose_name='Payment Status', help_text='Indicates whether the payment has been completed or not')
price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='Transaction Price')
status = models.CharField(max_length=20, choices=TransactionStatus.choices, default=TransactionStatus.PENDING, verbose_name=_('Transaction Status'))

155
apps/transaction/views.py

@ -14,6 +14,7 @@ from utils.exceptions import AppAPIException
from apps.account.models import User
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from apps.transaction.models import TransactionParticipant
from apps.transaction.doc import (
doc_upload_transaction_receipts,
doc_list_transaction_receipts,
@ -31,31 +32,111 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description=doc_create_transaction()
operation_description=doc_create_transaction(),
responses={
status.HTTP_201_CREATED: openapi.Response(
description="Transaction participant created successfully",
examples={
"application/json": {
"message": "Transaction Participant created successfully.",
"transaction_id": 374,
"payment_method": "Payment_Gateway",
"payment_link": "https://russia-payment.com/pay/374",
"participant_infos": [
{
"fullname": "string",
"email": "admin@gmail.com",
"phone_number": "string",
"gender": "male",
"birthdate": "2025-12-28"
}
]
}
},
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING, description="Success message"),
'transaction_id': openapi.Schema(type=openapi.TYPE_INTEGER, description="Unique transaction identifier"),
'payment_method': openapi.Schema(
type=openapi.TYPE_STRING,
enum=['Payment_Gateway', 'receipt'],
description="Payment method: 'Payment_Gateway' for online payment, 'receipt' for WhatsApp upload"
),
'payment_link': openapi.Schema(
type=openapi.TYPE_STRING,
nullable=True,
description="Payment gateway URL (only present when payment_method is 'Payment_Gateway')"
),
'participant_infos': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'fullname': openapi.Schema(type=openapi.TYPE_STRING),
'email': openapi.Schema(type=openapi.TYPE_STRING),
'phone_number': openapi.Schema(type=openapi.TYPE_STRING),
'gender': openapi.Schema(type=openapi.TYPE_STRING, enum=['male', 'female']),
'birthdate': openapi.Schema(type=openapi.TYPE_STRING, format='date'),
}
),
description="List of participant information"
),
},
required=['message', 'transaction_id', 'participant_infos']
)
),
status.HTTP_400_BAD_REQUEST: openapi.Response(
description="Invalid data provided",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'detail': openapi.Schema(type=openapi.TYPE_STRING),
}
)
),
status.HTTP_404_NOT_FOUND: openapi.Response(
description="Course not found",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'message': openapi.Schema(type=openapi.TYPE_STRING),
}
)
),
},
tags=['transaction']
)
def create(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
# Simply call the create method
return self.create(request, *args, **kwargs)
def create(self, request, *args, **kwargs):
user = request.user
course_slug = self.kwargs.get('slug') # Get the slug from the URL
course_slug = self.kwargs.get('slug')
# 1. Retrieve Course
try:
course = Course.objects.get(slug=course_slug) # Retrieve the Course object
course = Course.objects.get(slug=course_slug)
except Course.DoesNotExist:
raise AppAPIException({'message': "Course not found"}) # Handle course not found
raise AppAPIException({'message': "Course not found"})
participant_infos = request.data.get('participant_infos', [])
# 2. Validate and Initialize
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
statis = TransactionParticipant.TransactionStatus.PENDING
# 3. Handle Free/Self-Enrollment Logic
if len(participant_infos) == 1 and (course.final_price == 0 or course.is_free):
participant = participant_infos[0]
if participant.get('email') != user.email:
raise AppAPIException({'message': "The email must be for the requesting user"})
# به جای تغییر user_type، فقط نقش student را اضافه می‌کنیم
if not user.has_role('student'):
user.add_role('student')
# فیلتر کردن برای چک کردن وجود participant
existing_participant = Participant.objects.filter(student=user, course=course).first()
if existing_participant:
participant = existing_participant
@ -63,37 +144,57 @@ class TransactionParticipantCreateView(generics.CreateAPIView):
participant = Participant.objects.create(student=user, course=course)
statis = TransactionParticipant.TransactionStatus.SUCCESS
transaction_participant = serializer.save(user=user, course=course, price=course.final_price, status=statis)
print(f'---> {type(transaction_participant)}/ {transaction_participant}')
# 4. Save Transaction
transaction_participant = serializer.save(
user=user,
course=course,
price=course.final_price,
status=statis
)
print(f'---> {type(transaction_participant)}/ {transaction_participant}')
# =======================================================
# NEW LOGIC: GEOLOCATION CHECK
# =======================================================
# NEW LOGIC: HYBRID GEOLOCATION CHECK (Cloudflare + Local DB)
# =======================================================
payment_link = None
payment_link = None # Default link
# Only check IP if transaction is PENDING (needs payment)
payment_method = TransactionParticipant.PaymentMethods.FREE
if statis == TransactionParticipant.TransactionStatus.PENDING:
client_ip = get_client_ip(request)
# "188.93.104.1"
#get_client_ip(request)
country_code = get_country_code(client_ip)
if country_code == 'RU':
# Generate Russia-specific link (e.g., YooMoney, Mir card, etc.)
# Step A: Fast Path - Check Cloudflare Header
# Cloudflare sends the 2-letter code (e.g., 'RU', 'US') in this header
country_code = request.META.get('HTTP_CF_IPCOUNTRY')
# Step B: Slow Path - Fallback to Local DB
# If header is missing (e.g., Localhost, direct connection, or CF failed)
if not country_code:
try:
client_ip =get_client_ip(request)
# "188.93.104.1"
# get_client_ip(request)
# Assuming your helper handles errors gracefully and returns None
country_code = get_country_code(client_ip)
except Exception as e:
print(f"GeoIP Lookup Failed: {e}")
country_code = None
payment_method = TransactionParticipant.PaymentMethods.RECEIPT
# Step C: Apply Logic
if country_code != 'RU':
payment_method = TransactionParticipant.PaymentMethods.PAYMENT_GATEWAY
payment_link = f"https://russia-payment.com/pay/{transaction_participant.id}"
# Uncomment if you want a global fallback link
# else:
# # Standard Global Link (e.g., PayPal, Stripe)
# payment_link = f"https://global-payment.com/pay/{transaction_participant.id}"
# =======================================================
# =======================================================
return Response({
'message': 'Transaction Participant created successfully.',
'transaction_id': transaction_participant.id,
'payment_link': payment_link, # <--- Return the dynamic link
'payment_method':payment_method,
'payment_link': payment_link,
'participant_infos': serializer.data['participant_infos']
}, status=status.HTTP_201_CREATED)

Loading…
Cancel
Save