5 changed files with 381 additions and 41 deletions
-
208apps/transaction/doc.py
-
25apps/transaction/migrations/0004_transactionparticipant_payment_method.py
-
26apps/transaction/migrations/0005_alter_transactionparticipant_payment_method.py
-
8apps/transaction/models.py
-
155apps/transaction/views.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": "[email protected]", |
|||
"phone_number": "+989123456789", |
|||
"gender": "female", |
|||
"birthdate": "1998-03-20" |
|||
"fullname": "John Doe", |
|||
"email": "[email protected]", |
|||
"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": "[email protected]", |
|||
"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" |
|||
) |
|||
} |
|||
) |
|||
@ -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", |
|||
), |
|||
), |
|||
] |
|||
@ -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", |
|||
), |
|||
), |
|||
] |
|||
@ -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": "[email protected]", |
|||
"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) |
|||
|
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue