diff --git a/apps/course/serializers/course.py b/apps/course/serializers/course.py index cba2e5f..d381c0a 100644 --- a/apps/course/serializers/course.py +++ b/apps/course/serializers/course.py @@ -71,7 +71,7 @@ class CourseDetailSerializer(serializers.ModelSerializer): lessons_count = serializers.SerializerMethodField() last_lesson_id = serializers.SerializerMethodField() room_id = serializers.SerializerMethodField() - + user_transaction_status = serializers.SerializerMethodField() class Meta: model = Course fields = [ @@ -103,6 +103,7 @@ class CourseDetailSerializer(serializers.ModelSerializer): 'features', 'last_lesson_id', 'room_id', + 'user_transaction_status' ] def get_room_id(self, obj): @@ -111,6 +112,18 @@ class CourseDetailSerializer(serializers.ModelSerializer): return room_message.id return None + def get_user_transaction_status(self, obj): + from apps.transaction.models import TransactionParticipant + if student := self._get_authenticated_user(): + latest_transaction = TransactionParticipant.objects.filter( + user=student, + course=obj, + is_deleted=False + ).order_by('-created_at').first() + if latest_transaction: + return latest_transaction.status + return None + def get_last_lesson_id(self, obj): request = self.context.get('request') if request and request.user.is_authenticated: diff --git a/apps/transaction/admin.py b/apps/transaction/admin.py index e20d6c1..10e5ca8 100644 --- a/apps/transaction/admin.py +++ b/apps/transaction/admin.py @@ -22,7 +22,7 @@ class ParticipantInfoInline(StackedInline): @admin.register(TransactionParticipant) class TransactionParticipantAdmin(ModelAdmin): list_display = ('user', 'course', 'payment_status', 'price_display', 'created_at', 'updated_at') - list_filter = ('is_paid', 'course', 'created_at') + list_filter = ('status', 'course', 'created_at') search_fields = ('user__email', 'course__title') readonly_fields = [ 'created_at', 'updated_at'] inlines = [ParticipantInfoInline] @@ -32,7 +32,7 @@ class TransactionParticipantAdmin(ModelAdmin): fieldsets = ( (None, { - 'fields': ('user', 'course', 'is_paid', 'price') + 'fields': ('user', 'course', 'status', 'price') }), (_('Timestamps'), { 'fields': ('created_at', 'updated_at'), @@ -40,19 +40,20 @@ class TransactionParticipantAdmin(ModelAdmin): }), ) - @display(description=_("Payment Status"), ordering="is_paid") + @display(description=_("Payment Status"), ordering="status") def payment_status(self, obj): - if obj.is_paid: + if obj.status == 'success': return format_html('Paid') - return format_html('Unpaid') + elif obj.status == 'failed': + return format_html('Failed') + return format_html('Pending') @display(description=_("Price"), ordering="price") def price_display(self, obj): return format_html('${}', obj.price) def get_queryset(self, request): - queryset = super().get_queryset(request) - # Add any custom queryset modifications here if needed - return queryset - + # Filter out deleted transactions + return super().get_queryset(request).filter(is_deleted=False) + project_admin_site.register(TransactionParticipant, TransactionParticipantAdmin) diff --git a/apps/transaction/migrations/0002_remove_transactionparticipant_is_paid_and_more.py b/apps/transaction/migrations/0002_remove_transactionparticipant_is_paid_and_more.py new file mode 100644 index 0000000..cd0329e --- /dev/null +++ b/apps/transaction/migrations/0002_remove_transactionparticipant_is_paid_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.8 on 2025-04-07 13:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('transaction', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='transactionparticipant', + name='is_paid', + ), + migrations.AddField( + model_name='transactionparticipant', + name='is_deleted', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='transactionparticipant', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('success', 'Success'), ('failed', 'Failed')], default='pending', max_length=20, verbose_name='Transaction Status'), + ), + ] diff --git a/apps/transaction/models.py b/apps/transaction/models.py index 958ba77..f500e54 100644 --- a/apps/transaction/models.py +++ b/apps/transaction/models.py @@ -12,15 +12,26 @@ from utils.validators import validate_possible_number class TransactionParticipant(models.Model): + + + class TransactionStatus(models.TextChoices): + PENDING = 'pending', _('Pending') + SUCCESS = 'success', _('Success') + FAILED = 'failed', _('Failed') + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions') course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='course_transactions') - is_paid = models.BooleanField(default=False, verbose_name='Payment Status', help_text='Indicates whether the payment has been completed or not') + # 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')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at")) + is_deleted = models.BooleanField(default=False) - + def __str__(self): + return f"{self.user.email} - {self.course.title} ({self.status})" + + class ParticipantInfo(models.Model): @@ -42,6 +53,9 @@ class ParticipantInfo(models.Model): ) birthdate = models.DateField(verbose_name=_('birthdate'), null=True, blank=True) + def __str__(self): + return f"{self.fullname} (Transaction: {self.transaction_participant.id}) - {self.email}" + diff --git a/apps/transaction/serializers.py b/apps/transaction/serializers.py index 01b6c0a..f211340 100644 --- a/apps/transaction/serializers.py +++ b/apps/transaction/serializers.py @@ -42,7 +42,7 @@ class TransactionListSerializer(serializers.ModelSerializer): class Meta: model = TransactionParticipant - fields = ['course', 'is_paid', 'price', 'created_at', 'updated_at'] + fields = ['course', 'status', 'price', 'created_at', 'updated_at'] def get_course(self, obj): return CourseDetailSerializer(obj.course, context=self.context).data diff --git a/apps/transaction/urls.py b/apps/transaction/urls.py index 01f8804..babecbb 100644 --- a/apps/transaction/urls.py +++ b/apps/transaction/urls.py @@ -8,6 +8,7 @@ from . import views urlpatterns = [ path('/join/', views.TransactionParticipantCreateView.as_view(), name='transaction-participant-create'), path('list/', views.TransactiontListView.as_view(), name='transaction-list'), + path('/delete/', views.SoftDeleteTransactionParticipantView.as_view(), name='soft-delete-transaction-participant'), ] diff --git a/apps/transaction/views.py b/apps/transaction/views.py index 4801f09..ec53557 100644 --- a/apps/transaction/views.py +++ b/apps/transaction/views.py @@ -1,12 +1,14 @@ 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 from apps.transaction.serializers import TransactionParticipantSerializer, TransactionListSerializer from utils.exceptions import AppAPIException from apps.account.models import User +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi @@ -25,12 +27,10 @@ class TransactionParticipantCreateView(generics.CreateAPIView): raise AppAPIException({'message': "Course not found"}) # Handle course not found participant_infos = request.data.get('participant_infos', []) - print(f'1---> {participant_infos}') - print(f'2---> {len(participant_infos)}') serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - + statis = TransactionParticipant.TransactionStatus.PENDING if len(participant_infos) == 1 and (course.final_price == 0 or course.is_free): participant = participant_infos[0] if participant.get('email') != user.email: @@ -43,15 +43,11 @@ class TransactionParticipantCreateView(generics.CreateAPIView): student=user, course=course ) - return Response({ - 'message': 'Transaction Participant created successfully.', - 'participant_id': participant.id, - 'participant_infos': serializer.data['participant_infos'] - }, status=status.HTTP_201_CREATED) + statis = TransactionParticipant.TransactionStatus.SUCCESS - transaction_participant = serializer.save(user=user, course=course, price=course.final_price) + transaction_participant = serializer.save(user=user, course=course, price=course.final_price, status=statis) print(f'---> {type(transaction_participant)}/ {transaction_participant}') return Response({ 'message': 'Transaction Participant created successfully.', @@ -74,4 +70,51 @@ class TransactiontListView(generics.ListAPIView): def get_queryset(self): queryset = super().get_queryset() queryset = queryset.filter(user=self.request.user) - return queryset \ No newline at end of file + 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", + 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: + return AppAPIException({'message': "You don't have permission to delete this transaction"}, status_code=status.HTTP_403_FORBIDDEN) + + except TransactionParticipant.DoesNotExist: + return AppAPIException({'message': "Transaction participant not found"})