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"})