diff --git a/apps/transaction/doc.py b/apps/transaction/doc.py index e93f2b2..1eacff9 100644 --- a/apps/transaction/doc.py +++ b/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//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//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//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//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" + ) + } +) \ No newline at end of file diff --git a/apps/transaction/migrations/0004_transactionparticipant_payment_method.py b/apps/transaction/migrations/0004_transactionparticipant_payment_method.py new file mode 100644 index 0000000..0785a7c --- /dev/null +++ b/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", + ), + ), + ] diff --git a/apps/transaction/migrations/0005_alter_transactionparticipant_payment_method.py b/apps/transaction/migrations/0005_alter_transactionparticipant_payment_method.py new file mode 100644 index 0000000..3b1090e --- /dev/null +++ b/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", + ), + ), + ] diff --git a/apps/transaction/models.py b/apps/transaction/models.py index a017719..b852d90 100644 --- a/apps/transaction/models.py +++ b/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')) diff --git a/apps/transaction/views.py b/apps/transaction/views.py index 8b7d7f6..06f377a 100644 --- a/apps/transaction/views.py +++ b/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)