From 081366f87f794207bda9184541143bb26ff1c71e Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 17:37:04 +0500 Subject: [PATCH 1/5] =?UTF-8?q?feat(models):=20add=20status=20field=20to?= =?UTF-8?q?=20Topup=20model=20with=20choices=20for=20Pending,=20Paid,=20an?= =?UTF-8?q?d=20Cancelled=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/admin.py | 1 + billing/migrations/0011_topup_status.py | 25 +++++++++++++++++++++++++ billing/models.py | 9 +++++++++ 3 files changed, 35 insertions(+) create mode 100644 billing/migrations/0011_topup_status.py 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/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) From 2da9dcf14130aedd07520cccd2586bcfd47ec99d Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 17:37:17 +0500 Subject: [PATCH 2/5] =?UTF-8?q?feat(commands):=20add=20random=20status=20a?= =?UTF-8?q?ssignment=20for=20seeded=20topups=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/management/commands/seed_topups.py | 1 + 1 file changed, 1 insertion(+) 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, From 7003e4bcbabc8c9621e66d097e17958b017101cf Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 17:37:35 +0500 Subject: [PATCH 3/5] =?UTF-8?q?feat(views):=20implement=20topup=20cancella?= =?UTF-8?q?tion=20and=20update=20status=20to=20CANCELLED=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/urls.py | 8 ++++---- billing/views.py | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) 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..0f307fd 100644 --- a/billing/views.py +++ b/billing/views.py @@ -397,6 +397,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 +420,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 +452,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) From 3cc29cade6233c4552d3504084fa31f95a3a8b76 Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 17:37:50 +0500 Subject: [PATCH 4/5] =?UTF-8?q?feat(serializers):=20include=20status=20fie?= =?UTF-8?q?ld=20in=20TopupSerializer=20for=20better=20data=20representatio?= =?UTF-8?q?n=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/serializers.py | 1 + 1 file changed, 1 insertion(+) 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", From cd7555e35f02fdc5c74b37c6502ba41752000744 Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 17:42:06 +0500 Subject: [PATCH 5/5] =?UTF-8?q?fix(views):=20update=20paid=5Fat=20field=20?= =?UTF-8?q?in=20VerifyTopupPaymentAPIView=20to=20use=20transaction=20date?= =?UTF-8?q?=20from=20MIB=20response=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/billing/views.py b/billing/views.py index 0f307fd..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"],