From d4b26074e69038e8e7419c32d4e97ace28f3b442 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 16:31:47 +0500 Subject: [PATCH 1/4] =?UTF-8?q?feat(billing):=20Add=20is=5Fexpired=20field?= =?UTF-8?q?=20to=20TopupAdmin=20and=20TopupSerializer=20for=20better=20top?= =?UTF-8?q?up=20management=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/admin.py | 7 +++++++ billing/serializers.py | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/billing/admin.py b/billing/admin.py index becf3cb..19fd08d 100644 --- a/billing/admin.py +++ b/billing/admin.py @@ -26,8 +26,11 @@ class TopupAdmin(admin.ModelAdmin): "paid", "paid_at", "created_at", + "is_expired", + "expires_at", "updated_at", ) + search_fields = ( "user__first_name", "user__last_name", @@ -35,6 +38,10 @@ class TopupAdmin(admin.ModelAdmin): "user__mobile", ) + @admin.display(boolean=True, description="Expired") + def is_expired(self, obj): + return obj.is_expired + admin.site.register(Payment, PaymentAdmin) admin.site.register(BillFormula) diff --git a/billing/serializers.py b/billing/serializers.py index 44dcc5c..8b613a8 100644 --- a/billing/serializers.py +++ b/billing/serializers.py @@ -21,6 +21,7 @@ class UpdatePaymentSerializer(serializers.ModelSerializer): class TopupSerializer(serializers.ModelSerializer): user = serializers.SerializerMethodField() + is_expired = serializers.SerializerMethodField() def get_user(self, obj): user = obj.user @@ -33,6 +34,9 @@ class TopupSerializer(serializers.ModelSerializer): } return None + def get_is_expired(self, obj): + return obj.is_expired + class Meta: # type: ignore model = Topup fields = [ @@ -41,6 +45,7 @@ class TopupSerializer(serializers.ModelSerializer): "user", "paid", "mib_reference", + "is_expired", "created_at", "updated_at", ] From 6568504f5bd207d5b55a92724b69fd71b0d95c1e Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 16:32:21 +0500 Subject: [PATCH 2/4] =?UTF-8?q?migration(billing):=20Add=20expires=5Fat=20?= =?UTF-8?q?field=20and=20is=5Fexpired=20property=20to=20Topup=20model=20fo?= =?UTF-8?q?r=20expiration=20management=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._options_topup_expired_topup_expires_at.py | 26 +++++++++++++++++++ .../migrations/0009_remove_topup_expired.py | 16 ++++++++++++ billing/models.py | 7 +++++ 3 files changed, 49 insertions(+) create mode 100644 billing/migrations/0008_alter_topup_options_topup_expired_topup_expires_at.py create mode 100644 billing/migrations/0009_remove_topup_expired.py diff --git a/billing/migrations/0008_alter_topup_options_topup_expired_topup_expires_at.py b/billing/migrations/0008_alter_topup_options_topup_expired_topup_expires_at.py new file mode 100644 index 0000000..46cd966 --- /dev/null +++ b/billing/migrations/0008_alter_topup_options_topup_expired_topup_expires_at.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2 on 2025-07-04 06:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0007_topup_paid_at"), + ] + + operations = [ + migrations.AlterModelOptions( + name="topup", + options={"ordering": ["-created_at"]}, + ), + migrations.AddField( + model_name="topup", + name="expired", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="topup", + name="expires_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/billing/migrations/0009_remove_topup_expired.py b/billing/migrations/0009_remove_topup_expired.py new file mode 100644 index 0000000..71c5576 --- /dev/null +++ b/billing/migrations/0009_remove_topup_expired.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2 on 2025-07-04 11:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0008_alter_topup_options_topup_expired_topup_expires_at"), + ] + + operations = [ + migrations.RemoveField( + model_name="topup", + name="expired", + ), + ] diff --git a/billing/models.py b/billing/models.py index 8017793..8fee3d6 100644 --- a/billing/models.py +++ b/billing/models.py @@ -53,9 +53,16 @@ class Topup(models.Model): paid = models.BooleanField(default=False) paid_at = models.DateTimeField(null=True, blank=True) mib_reference = models.CharField(default="", null=True, blank=True) + expires_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) + @property + def is_expired(self): + if self.expires_at is None: + return False + return timezone.now() > self.expires_at + def __str__(self): return f"Topup for {self.user}" From 5db71edc2cd67e4abbd1c1e1cc52d140611fed5c Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 16:32:34 +0500 Subject: [PATCH 3/4] =?UTF-8?q?feat(billing):=20Implement=20DeleteTopupVie?= =?UTF-8?q?w=20with=20expiration=20and=20authorization=20checks=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/views.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/billing/views.py b/billing/views.py index 2ccb5e7..063f9cd 100644 --- a/billing/views.py +++ b/billing/views.py @@ -367,3 +367,33 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi {"message": "Topup payment verification failed."}, status=status.HTTP_400_BAD_REQUEST, ) + + +class DeleteTopupView(StaffEditorPermissionMixin, generics.DestroyAPIView): + queryset = Topup.objects.all() + serializer_class = TopupSerializer + lookup_field = "pk" + + def delete(self, request, *args, **kwargs): + instance = self.get_object() + user = request.user + if instance.is_expired: + return Response( + {"message": "Expired topups cannot be deleted."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if ( + instance.user != user + and getattr(user, "is_admin") + and not user.is_superuser + ): + return Response( + {"message": "You are not authorized to delete this topup."}, + status=status.HTTP_403_FORBIDDEN, + ) + if instance.paid: + return Response( + {"message": "Paid topups cannot be deleted."}, + status=status.HTTP_400_BAD_REQUEST, + ) + return super().delete(request, *args, **kwargs) From 212ea2541f2c8c09fe71275f11e072413c263fa0 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 16:32:45 +0500 Subject: [PATCH 4/4] =?UTF-8?q?refactor(billing):=20Simplify=20topup=20see?= =?UTF-8?q?ding=20by=20removing=20payment=20status=20and=20date=20logic=20?= =?UTF-8?q?=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/management/commands/seed_topups.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/billing/management/commands/seed_topups.py b/billing/management/commands/seed_topups.py index 145b1c6..04261eb 100644 --- a/billing/management/commands/seed_topups.py +++ b/billing/management/commands/seed_topups.py @@ -4,9 +4,6 @@ import random from django.core.management.base import BaseCommand from django.utils import timezone from faker import Faker -from billing.models import ( - Payment, -) from billing.models import Topup from api.models import User @@ -40,12 +37,12 @@ class Command(BaseCommand): for _ in range(number): random_user = random.choice(users) - paid_status = fake.boolean(chance_of_getting_true=80) - paid_at_date = fake.date_time_this_year() if paid_status else None - - if paid_at_date and timezone.is_naive(paid_at_date): - paid_at_date = timezone.make_aware(paid_at_date) - + expires_at_date = timezone.now() + timezone.timedelta( + minutes=10, + ) + print( + f"Creating topup for user {getattr(random_user, 'id', None)} expires at: {expires_at_date}" + ) Topup.objects.create( amount=fake.pydecimal( left_digits=4, @@ -54,10 +51,9 @@ class Command(BaseCommand): min_value=100.00, max_value=5000.00, ), - paid=paid_status, user=random_user, - paid_at=paid_at_date, updated_at=timezone.now(), + expires_at=expires_at_date, ) self.stdout.write(self.style.SUCCESS(f"Successfully seeded {number} topups."))