diff --git a/billing/admin.py b/billing/admin.py index 19fd08d..8241083 100644 --- a/billing/admin.py +++ b/billing/admin.py @@ -25,6 +25,7 @@ class TopupAdmin(admin.ModelAdmin): "amount", "paid", "paid_at", + "status", "created_at", "is_expired", "expires_at", diff --git a/billing/management/commands/seed_topups.py b/billing/management/commands/seed_topups.py index 04261eb..ca28337 100644 --- a/billing/management/commands/seed_topups.py +++ b/billing/management/commands/seed_topups.py @@ -51,6 +51,7 @@ class Command(BaseCommand): min_value=100.00, max_value=5000.00, ), + status=random.choice(["PENDING", "PAID", "CANCELLED"]), user=random_user, updated_at=timezone.now(), expires_at=expires_at_date, diff --git a/billing/migrations/0011_topup_status.py b/billing/migrations/0011_topup_status.py new file mode 100644 index 0000000..8c7a1f3 --- /dev/null +++ b/billing/migrations/0011_topup_status.py @@ -0,0 +1,25 @@ +# Generated by Django 5.2 on 2025-07-05 12:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0010_add_expiry_notification_sent_to_topup"), + ] + + operations = [ + migrations.AddField( + model_name="topup", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("PAID", "Paid"), + ("CANCELLED", "Cancelled"), + ], + default="PENDING", + max_length=20, + ), + ), + ] diff --git a/billing/models.py b/billing/models.py index e73cbe0..814365a 100644 --- a/billing/models.py +++ b/billing/models.py @@ -52,6 +52,15 @@ class Topup(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups") paid = models.BooleanField(default=False) paid_at = models.DateTimeField(null=True, blank=True) + status = models.CharField( + max_length=20, + choices=[ + ("PENDING", "Pending"), + ("PAID", "Paid"), + ("CANCELLED", "Cancelled"), + ], + default="PENDING", + ) mib_reference = models.CharField(default="", null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) expiry_notification_sent = models.BooleanField(default=False) diff --git a/billing/serializers.py b/billing/serializers.py index a03bc76..a21616f 100644 --- a/billing/serializers.py +++ b/billing/serializers.py @@ -44,6 +44,7 @@ class TopupSerializer(serializers.ModelSerializer): "amount", "user", "paid", + "status", "mib_reference", "is_expired", "expires_at", diff --git a/billing/urls.py b/billing/urls.py index 94d8bf4..76591cd 100644 --- a/billing/urls.py +++ b/billing/urls.py @@ -9,7 +9,7 @@ from .views import ( ListCreateTopupView, VerifyTopupPaymentAPIView, TopupDetailAPIView, - DeleteTopupView, + CancelTopupView, ) urlpatterns = [ @@ -37,8 +37,8 @@ urlpatterns = [ name="verify-topup-payment", ), path( - "topup//delete/", - DeleteTopupView.as_view(), - name="delete-topup", + "topup//cancel/", + CancelTopupView.as_view(), + name="cancel-topup", ), ] diff --git a/billing/views.py b/billing/views.py index f2922a9..cf8227d 100644 --- a/billing/views.py +++ b/billing/views.py @@ -355,9 +355,8 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi ) else: topup.paid = True - topup.paid_at = timezone.now() topup.mib_reference = mib_resp["transaction"]["ref"] or "" - topup.paid_at = timezone.now() + topup.paid_at = mib_resp["transaction"]["trxDate"] topup.save() return PaymentVerificationResponse( message=mib_resp["message"], @@ -397,6 +396,8 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi if topup_verification_response.success: user.wallet_balance += topup_instance.amount # type: ignore user.save() + topup_instance.status = "PAID" + topup_instance.save() return Response( { "status": topup_verification_response.success, @@ -418,17 +419,22 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi ) -class DeleteTopupView(StaffEditorPermissionMixin, generics.DestroyAPIView): - queryset = Topup.objects.all() +class CancelTopupView(StaffEditorPermissionMixin, generics.UpdateAPIView): + queryset = Topup.objects.all().select_related("user") serializer_class = TopupSerializer lookup_field = "pk" - def delete(self, request, *args, **kwargs): + def update(self, request, *args, **kwargs): instance = self.get_object() user = request.user + if instance.status == "CANCELLED": + return Response( + {"message": "Topup has already been cancelled."}, + status=status.HTTP_400_BAD_REQUEST, + ) if instance.is_expired: return Response( - {"message": "Expired topups cannot be deleted."}, + {"message": "Expired topups cannot be cancelled."}, status=status.HTTP_400_BAD_REQUEST, ) if ( @@ -445,4 +451,6 @@ class DeleteTopupView(StaffEditorPermissionMixin, generics.DestroyAPIView): {"message": "Paid topups cannot be deleted."}, status=status.HTTP_400_BAD_REQUEST, ) - return super().delete(request, *args, **kwargs) + instance.status = "CANCELLED" + instance.save() + return super().update(request, *args, **kwargs)