You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
16 KiB
406 lines
16 KiB
from rest_framework import generics, status
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.response import Response
|
|
from rest_framework.views import APIView
|
|
from apps.course.models import Participant, Course
|
|
from apps.transaction.models import TransactionParticipant, TransactionReceipt
|
|
from apps.transaction.serializers import (
|
|
TransactionParticipantSerializer,
|
|
TransactionListSerializer,
|
|
UploadReceiptsSerializer,
|
|
TransactionReceiptSerializer
|
|
)
|
|
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,
|
|
doc_transaction_list,
|
|
doc_create_transaction
|
|
)
|
|
|
|
from utils.ip_helper import get_client_ip, get_country_code
|
|
|
|
|
|
|
|
class TransactionParticipantCreateView(generics.CreateAPIView):
|
|
queryset = TransactionParticipant.objects.all()
|
|
serializer_class = TransactionParticipantSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@swagger_auto_schema(
|
|
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=['Imam-Javad - Transaction']
|
|
)
|
|
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')
|
|
|
|
# 1. Retrieve Course
|
|
try:
|
|
course = Course.objects.get(slug=course_slug)
|
|
except Course.DoesNotExist:
|
|
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"})
|
|
|
|
if not user.has_role('student'):
|
|
user.add_role('student')
|
|
|
|
existing_participant = Participant.objects.filter(student=user, course=course).first()
|
|
if existing_participant:
|
|
participant = existing_participant
|
|
else:
|
|
participant = Participant.objects.create(student=user, course=course)
|
|
statis = TransactionParticipant.TransactionStatus.SUCCESS
|
|
|
|
# 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: HYBRID GEOLOCATION CHECK (Cloudflare + Local DB)
|
|
# =======================================================
|
|
|
|
payment_link = None
|
|
|
|
payment_method = TransactionParticipant.PaymentMethods.FREE
|
|
if statis == TransactionParticipant.TransactionStatus.PENDING:
|
|
|
|
# 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:
|
|
# payment_link = f"https://global-payment.com/pay/{transaction_participant.id}"
|
|
|
|
# =======================================================
|
|
|
|
return Response({
|
|
'message': 'Transaction Participant created successfully.',
|
|
'transaction_id': transaction_participant.id,
|
|
'payment_method':payment_method,
|
|
'payment_link': payment_link,
|
|
'participant_infos': serializer.data['participant_infos']
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
|
|
|
|
from utils.pagination import StandardResultsSetPagination
|
|
|
|
|
|
|
|
class TransactiontListView(generics.ListAPIView):
|
|
queryset = TransactionParticipant.objects.all()
|
|
serializer_class = TransactionListSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
operation_description=doc_transaction_list(),
|
|
tags=['Imam-Javad - Transaction']
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
queryset = queryset.filter(user=self.request.user, is_deleted=False)
|
|
return queryset
|
|
|
|
|
|
|
|
class SoftDeleteTransactionParticipantView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="Soft delete a transaction participant",
|
|
operation_description="Marks a transaction participant as deleted without removing it from the database",
|
|
tags=['Imam-Javad - Transaction'],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'id',
|
|
openapi.IN_PATH,
|
|
description="Transaction Participant ID",
|
|
type=openapi.TYPE_INTEGER,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Transaction participant successfully marked as deleted",
|
|
examples={
|
|
"application/json": {
|
|
"success": True,
|
|
"message": "Transaction participant successfully marked as deleted"
|
|
}
|
|
}
|
|
),
|
|
404: "Transaction participant not found",
|
|
403: "Permission denied"
|
|
}
|
|
)
|
|
def delete(self, request, pk):
|
|
try:
|
|
transaction = TransactionParticipant.objects.get(pk=pk)
|
|
if transaction.user == request.user:
|
|
transaction.is_deleted = True
|
|
transaction.save()
|
|
return Response({
|
|
"success": True,
|
|
"message": "Transaction participant successfully marked as deleted"
|
|
}, status=status.HTTP_200_OK)
|
|
else:
|
|
raise AppAPIException(
|
|
detail={'message': "You don't have permission to delete this transaction"},
|
|
status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
except TransactionParticipant.DoesNotExist:
|
|
raise AppAPIException({'message': "Transaction participant not found"})
|
|
|
|
|
|
class UploadTransactionReceiptsView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="Upload payment receipts for a transaction",
|
|
operation_description=doc_upload_transaction_receipts(),
|
|
tags=['Imam-Javad - Transaction'],
|
|
request_body=UploadReceiptsSerializer,
|
|
responses={
|
|
201: openapi.Response(
|
|
description="Receipts uploaded successfully",
|
|
examples={
|
|
"application/json": {
|
|
"success": True,
|
|
"message": "Receipts uploaded successfully",
|
|
"transaction_status": "waiting_approval",
|
|
"receipts": [
|
|
{
|
|
"id": 1,
|
|
"file": "http://example.com/media/receipts/1/receipt.jpg",
|
|
"description": "Payment receipt",
|
|
"uploaded_at": "2025-12-03T10:30:00Z"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
),
|
|
400: "Invalid data or transaction cannot accept receipts",
|
|
403: "Permission denied",
|
|
404: "Transaction not found"
|
|
}
|
|
)
|
|
def post(self, request, transaction_id):
|
|
try:
|
|
transaction = TransactionParticipant.objects.get(pk=transaction_id, is_deleted=False)
|
|
except TransactionParticipant.DoesNotExist:
|
|
raise AppAPIException({'message': "Transaction not found"})
|
|
|
|
# Check if user owns this transaction
|
|
if transaction.user != request.user:
|
|
raise AppAPIException(
|
|
detail={'message': "You don't have permission to upload receipts for this transaction"},
|
|
status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
# Check if transaction is in a state that can accept receipts
|
|
if transaction.status not in [
|
|
TransactionParticipant.TransactionStatus.PENDING,
|
|
TransactionParticipant.TransactionStatus.WAITING_APPROVAL
|
|
]:
|
|
raise AppAPIException(
|
|
detail={'message': f"Cannot upload receipts for transaction with status '{transaction.status}'"},
|
|
status_code=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Validate using serializer
|
|
serializer = UploadReceiptsSerializer(data=request.data, context={'request': request})
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
# Create receipt records
|
|
receipts = []
|
|
description = serializer.validated_data.get('description', '')
|
|
file_urls = serializer.validated_data.get('files', [])
|
|
|
|
for file_url in file_urls:
|
|
receipt = TransactionReceipt.objects.create(
|
|
transaction=transaction,
|
|
file=file_url,
|
|
description=description
|
|
)
|
|
receipts.append(receipt)
|
|
|
|
# Update transaction status to waiting_approval
|
|
transaction.status = TransactionParticipant.TransactionStatus.WAITING_APPROVAL
|
|
transaction.save()
|
|
|
|
# Serialize receipts for response
|
|
receipts_data = TransactionReceiptSerializer(receipts, many=True, context={'request': request}).data
|
|
|
|
return Response({
|
|
'success': True,
|
|
'message': 'Receipts uploaded successfully',
|
|
'transaction_status': transaction.status,
|
|
'receipts': receipts_data
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
class TransactionReceiptsListView(generics.ListAPIView):
|
|
serializer_class = TransactionReceiptSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
operation_summary="List receipts for a transaction",
|
|
operation_description=doc_list_transaction_receipts(),
|
|
tags=['Imam-Javad - Transaction'],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'transaction_id',
|
|
openapi.IN_PATH,
|
|
description="Transaction ID",
|
|
type=openapi.TYPE_INTEGER,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
200: TransactionReceiptSerializer(many=True),
|
|
403: "Permission denied",
|
|
404: "Transaction not found"
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
transaction_id = self.kwargs.get('transaction_id')
|
|
|
|
try:
|
|
transaction = TransactionParticipant.objects.get(pk=transaction_id, is_deleted=False)
|
|
except TransactionParticipant.DoesNotExist:
|
|
raise AppAPIException({'message': "Transaction not found"})
|
|
|
|
# Check if user owns this transaction
|
|
if transaction.user != self.request.user:
|
|
raise AppAPIException(
|
|
detail={'message': "You don't have permission to view receipts for this transaction"},
|
|
status_code=status.HTTP_403_FORBIDDEN
|
|
)
|
|
|
|
return TransactionReceipt.objects.filter(transaction=transaction)
|