From c07d3c93d25f52c593632aeccd0f947ba5dd4857 Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 17:13:25 +0500 Subject: [PATCH 1/8] =?UTF-8?q?feat(billing):=20WIP=20Add=20Topup=20model,?= =?UTF-8?q?=20filters,=20serializers,=20and=20views=20for=20topup=20manage?= =?UTF-8?q?ment=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/filters.py | 15 ++- .../migrations/0006_topup_mib_reference.py | 17 ++++ billing/migrations/0007_topup_paid_at.py | 17 ++++ billing/models.py | 2 + billing/serializers.py | 24 ++++- billing/urls.py | 10 ++ billing/views.py | 94 ++++++++++++++++++- 7 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 billing/migrations/0006_topup_mib_reference.py create mode 100644 billing/migrations/0007_topup_paid_at.py diff --git a/billing/filters.py b/billing/filters.py index 07cc71a..c316e4f 100644 --- a/billing/filters.py +++ b/billing/filters.py @@ -1,5 +1,5 @@ import django_filters -from .models import Payment +from .models import Payment, Topup class PaymentFilter(django_filters.FilterSet): @@ -16,3 +16,16 @@ class PaymentFilter(django_filters.FilterSet): class Meta: model = Payment fields = "__all__" + + +class TopupFilter(django_filters.FilterSet): + amount = django_filters.RangeFilter(field_name="amount") + paid = django_filters.BooleanFilter(field_name="paid") + user = django_filters.CharFilter( + field_name="user__username", lookup_expr="icontains" + ) + created_at = django_filters.DateFromToRangeFilter() + + class Meta: + model = Topup # Assuming Topup is a subclass of Payment + fields = "__all__" diff --git a/billing/migrations/0006_topup_mib_reference.py b/billing/migrations/0006_topup_mib_reference.py new file mode 100644 index 0000000..50d8d54 --- /dev/null +++ b/billing/migrations/0006_topup_mib_reference.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2 on 2025-07-03 11:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0005_alter_payment_options_payment_mib_reference"), + ] + + operations = [ + migrations.AddField( + model_name="topup", + name="mib_reference", + field=models.CharField(blank=True, default="", null=True), + ), + ] diff --git a/billing/migrations/0007_topup_paid_at.py b/billing/migrations/0007_topup_paid_at.py new file mode 100644 index 0000000..ed39269 --- /dev/null +++ b/billing/migrations/0007_topup_paid_at.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2 on 2025-07-03 12:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0006_topup_mib_reference"), + ] + + operations = [ + migrations.AddField( + model_name="topup", + name="paid_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/billing/models.py b/billing/models.py index d47662a..08041e7 100644 --- a/billing/models.py +++ b/billing/models.py @@ -51,6 +51,8 @@ class Topup(models.Model): amount = models.FloatField() user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups") paid = models.BooleanField(default=False) + paid_at = models.DateTimeField(null=True, blank=True) + mib_reference = models.CharField(default="", null=True, blank=True) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) diff --git a/billing/serializers.py b/billing/serializers.py index 4f8559c..c859eb1 100644 --- a/billing/serializers.py +++ b/billing/serializers.py @@ -1,19 +1,37 @@ from rest_framework import serializers -from .models import Payment +from .models import Payment, Topup from devices.serializers import DeviceSerializer +from api.serializers import CustomReadOnlyUserSerializer class PaymentSerializer(serializers.ModelSerializer): devices = DeviceSerializer(many=True, read_only=True) - class Meta: + class Meta: # type: ignore model = Payment fields = "__all__" class UpdatePaymentSerializer(serializers.ModelSerializer): - class Meta: + class Meta: # type: ignore model = Payment fields = [ "number_of_months", ] + + +class TopupSerializer(serializers.ModelSerializer): + user = CustomReadOnlyUserSerializer(read_only=True) + + class Meta: # type: ignore + model = Topup + fields = [ + "id", + "amount", + "user", + "paid", + "mib_reference", + "created_at", + "updated_at", + ] + read_only_fields = ["id", "created_at", "updated_at"] diff --git a/billing/urls.py b/billing/urls.py index d6238eb..6d57ce4 100644 --- a/billing/urls.py +++ b/billing/urls.py @@ -6,6 +6,8 @@ from .views import ( PaymentDetailAPIView, UpdatePaymentAPIView, DeletePaymentView, + ListCreateTopupView, + VerifyTopupPaymentAPIView, ) urlpatterns = [ @@ -24,4 +26,12 @@ urlpatterns = [ path( "payment//verify/", VerifyPaymentView.as_view(), name="verify-payment" ), + # Topups + path("topup/", ListCreateTopupView.as_view(), name="create-list-topups"), + # path("topup//", TopupDetailAPIView.as_view(), name="retrieve-topup"), + path( + "topup//verify/", + VerifyTopupPaymentAPIView.as_view(), + name="verify-topup-payment", + ), ] diff --git a/billing/views.py b/billing/views.py index 584925c..620a4b5 100644 --- a/billing/views.py +++ b/billing/views.py @@ -15,9 +15,9 @@ from api.tasks import add_new_devices_to_omada from apibase.env import BASE_DIR, env import logging -from .models import Device, Payment -from .serializers import PaymentSerializer, UpdatePaymentSerializer -from .filters import PaymentFilter +from .models import Device, Payment, Topup +from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer +from .filters import PaymentFilter, TopupFilter env.read_env(os.path.join(BASE_DIR, ".env")) @@ -259,3 +259,91 @@ class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView): devices = instance.devices.all() devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False) return super().delete(request, *args, **kwargs) + + +class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView): + queryset = Topup.objects.all() + serializer_class = TopupSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = "__all__" + filterset_class = TopupFilter + + def create(self, request, *args, **kwargs): + data = request.data + user = request.user + amount = data.get("amount") + if not amount: + return Response( + {"message": "amount is required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + topup = Topup.objects.create(amount=amount, user=user) + serializer = TopupSerializer(topup) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + +class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): + queryset = Topup.objects.all() + serializer_class = TopupSerializer + lookup_field = "pk" + + def verify_transfer_topup(self, data, topup): + if not PAYMENT_BASE_URL: + raise ValueError( + "PAYMENT_BASE_URL is not set. Please set it in your environment variables." + ) + logger.info(data) + response = requests.post( + f"{PAYMENT_BASE_URL}/verify-payment", + json=data, + headers={"Content-Type": "application/json"}, + ) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + logger.error(f"HTTPError: {e}") + return False # Or handle the error as appropriate + mib_resp = response.json() + print(mib_resp) + if not response.json().get("success"): + return mib_resp["success"] + else: + topup.paid = True + # topup.paid_at = timezone.now() # Assuming Topup model has paid_at field + topup.mib_reference = mib_resp["transaction"]["ref"] or "" + topup.save() + return True + + def update(self, request, *args, **kwargs): + topup_instance = self.get_object() + user = request.user + + if topup_instance.paid: + return Response( + {"message": "Payment has already been verified."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if topup_instance.user != user and not user.is_superuser: + return Response( + {"message": "You are not allowed to pay for this topup."}, + status=status.HTTP_403_FORBIDDEN, + ) + data = { + "benefName": f"{user.first_name} {user.last_name}", # type: ignore + "accountNo": user.acc_no, # type: ignore + "absAmount": topup_instance.amount, + "time": localtime(timezone.now() + timedelta(minutes=5)).strftime( + "%Y-%m-%d %H:%M" + ), + } + topup_status = self.verify_transfer_topup(data, topup_instance) + if topup_status: + return Response( + {"message": "Topup payment verified successfully."}, + status=status.HTTP_200_OK, + ) + else: + return Response( + {"message": "Topup payment verification failed."}, + status=status.HTTP_400_BAD_REQUEST, + ) From 367ccf0f888eeb63ec8178b46a66060b6d39c9f9 Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 17:13:41 +0500 Subject: [PATCH 2/8] =?UTF-8?q?feat(justfile):=20Add=20migrate=20and=20mak?= =?UTF-8?q?e-migrations=20commands=20for=20database=20management=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- justfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/justfile b/justfile index b7ca1d6..bc25798 100644 --- a/justfile +++ b/justfile @@ -4,3 +4,9 @@ default: dev: python manage.py runserver python manage.py procrastinate worker + +migrate: + python manage.py migrate + +make-migrations: + python manage.py makemigrations \ No newline at end of file From 978a4a27d00861fb1bb52aed292e298401727aae Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:02:37 +0500 Subject: [PATCH 3/8] =?UTF-8?q?feat(billing):=20Add=20topup=20permissions?= =?UTF-8?q?=20assignment=20for=20new=20users=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/signals.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/signals.py b/api/signals.py index b206a34..bd41c73 100644 --- a/api/signals.py +++ b/api/signals.py @@ -18,7 +18,12 @@ def assign_device_permissions(sender, instance, created, **kwargs): atoll_read_permission = Permission.objects.get(codename="view_atoll") island_read_permission = Permission.objects.get(codename="view_island") payment_permissions = Permission.objects.filter(content_type__model="payment") + topup_permissions = Permission.objects.filter( + content_type__model="topup" + ).exclude(codename__startswith="delete_") + for permission in topup_permissions: + instance.user_permissions.add(permission) for permission in device_permissions: instance.user_permissions.add(permission) instance.user_permissions.add(atoll_read_permission, island_read_permission) From 2122e8dfee1b7d4b9eb87ed8ceaad25426e0e402 Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:03:15 +0500 Subject: [PATCH 4/8] =?UTF-8?q?feat(billing):=20Enhance=20Topup=20admin=20?= =?UTF-8?q?interface=20and=20add=20default=20ordering=20for=20Topup=20mode?= =?UTF-8?q?l=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/admin.py | 20 +++++++++++++++++++- billing/models.py | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/billing/admin.py b/billing/admin.py index db6311c..becf3cb 100644 --- a/billing/admin.py +++ b/billing/admin.py @@ -18,6 +18,24 @@ class PaymentAdmin(admin.ModelAdmin): ) +class TopupAdmin(admin.ModelAdmin): + list_display = ( + "id", + "user", + "amount", + "paid", + "paid_at", + "created_at", + "updated_at", + ) + search_fields = ( + "user__first_name", + "user__last_name", + "user__id_card", + "user__mobile", + ) + + admin.site.register(Payment, PaymentAdmin) admin.site.register(BillFormula) -admin.site.register(Topup) +admin.site.register(Topup, TopupAdmin) diff --git a/billing/models.py b/billing/models.py index 08041e7..8017793 100644 --- a/billing/models.py +++ b/billing/models.py @@ -58,3 +58,6 @@ class Topup(models.Model): def __str__(self): return f"Topup for {self.user}" + + class Meta: + ordering = ["-created_at"] From f67a3762addd81d75af88feedd607d5220b839c8 Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:03:55 +0500 Subject: [PATCH 5/8] =?UTF-8?q?test(billing):=20Add=20comprehensive=20test?= =?UTF-8?q?s=20for=20Topup=20model=20and=20payment=20verification=20?= =?UTF-8?q?=F0=9F=A7=AA=E2=9C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/tests.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++- justfile | 12 ++- 2 files changed, 200 insertions(+), 3 deletions(-) diff --git a/billing/tests.py b/billing/tests.py index 7ce503c..7be55fe 100644 --- a/billing/tests.py +++ b/billing/tests.py @@ -1,3 +1,192 @@ from django.test import TestCase +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model +from .models import Topup +from .serializers import TopupSerializer +from django.utils import timezone +from datetime import timedelta +from decouple import config +from unittest import mock -# Create your tests here. +User = get_user_model() + +REAL_USER_FIRST_NAME = config("REAL_USER_FIRST_NAME", default="josh") +REAL_USER_LAST_NAME = config("REAL_USER_LAST_NAME", default="mosh") + + +class TopupTests(TestCase): + def setUp(self): + self.client = APIClient() + self.real_user = User.objects.create_user( + username="testuser", + password="testpassword", + first_name=REAL_USER_FIRST_NAME, + last_name=REAL_USER_LAST_NAME, + acc_no="7770000010629", + is_admin=True, + ) + self.user = User.objects.create_user( + username="plskillme", + password="modewasgayithink", + first_name="mode", + last_name="hussain", + acc_no="1122334455", + ) + self.admin_user = User.objects.create_superuser( + username="adminuser", + password="adminpassword", + email="admin@example.com", + first_name="Admin", + last_name="User", + acc_no="987654321", + ) + self.client.force_authenticate(user=self.real_user) + + def test_create_topup(self): + url = reverse("create-list-topups") + data = {"amount": 100.00} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Topup.objects.count(), 1) + topup = Topup.objects.first() + self.assertEqual(getattr(topup, "amount"), 100.00) + self.assertEqual(getattr(topup, "user"), self.real_user) + + def test_create_topup_no_amount(self): + url = reverse("create-list-topups") + data = {} + response = self.client.post(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_list_topups(self): + Topup.objects.create(amount=50.00, user=self.real_user) + Topup.objects.create(amount=75.00, user=self.real_user) + url = reverse("create-list-topups") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 2) + + def test_list_topups_admin(self): + self.client = APIClient() + self.client.force_authenticate(user=self.admin_user) + Topup.objects.create(amount=50.00, user=self.real_user) + admin_user = User.objects.create_user( + username="anotheruser", + password="testpassword", + first_name="Another", + last_name="User", + acc_no="1122334455", + is_admin=True, + ) + Topup.objects.create(amount=75.00, user=admin_user) + url = reverse("create-list-topups") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 2) # Admin sees all + + def test_list_topups_filtered_by_user(self): + other_user = User.objects.create_user( + username="otheruser", + password="testpassword", + first_name="Other", + last_name="User", + acc_no="5544332211", + ) + Topup.objects.create(amount=50.00, user=self.user) + Topup.objects.create(amount=75.00, user=other_user) + url = reverse("create-list-topups") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 2) + + @mock.patch("billing.views.localtime") + def test_verify_topup_payment(self, mock_localtime): + fixed_time = timezone.datetime( + 2025, 7, 3, 19, 36, tzinfo=timezone.get_current_timezone() + ) + mock_localtime.return_value = fixed_time + + topup = Topup.objects.create(amount=1.5, user=self.real_user) + url = reverse("verify-topup-payment", kwargs={"pk": topup.pk}) + self.client = APIClient() + + self.client.force_authenticate(user=self.real_user) + data = { + "benefName": f"{REAL_USER_FIRST_NAME} {REAL_USER_LAST_NAME}", + "accountNo": "7770000010629", + "absAmount": 1.5, + "time": fixed_time.strftime("%Y-%m-%d %H:%M"), # Use the same fixed time + } + response = self.client.patch(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + topup.refresh_from_db() + self.assertEqual(topup.paid, True) + + def test_verify_topup_payment_already_verified(self): + topup = Topup.objects.create(amount=100.00, user=self.real_user, paid=True) + url = reverse("verify-topup-payment", kwargs={"pk": topup.pk}) + data = {} + response = self.client.patch(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_verify_topup_payment_unauthorized(self): + other_user = User.objects.create_user( + username="otheruser", + password="testpassword", + first_name="Other", + last_name="User", + acc_no="5544332211", + ) + topup = Topup.objects.create(amount=100.00, user=other_user) + url = reverse("verify-topup-payment", kwargs={"pk": topup.pk}) + data = {} + response = self.client.patch(url, data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_topup_serializer(self): + topup = Topup.objects.create(amount=120.00, user=self.real_user) + serializer = TopupSerializer(topup) + self.assertEqual(serializer.data["amount"], 120.00) + self.assertEqual(serializer.data["user"]["id"], getattr(self.real_user, "id")) + + def test_topup_filter_amount(self): + Topup.objects.create(amount=50.00, user=self.real_user) + Topup.objects.create(amount=100.00, user=self.real_user) + url = reverse("create-list-topups") + "?amount_min=75" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 1) + self.assertEqual(response.json()["data"][0]["amount"], 100.00) + + def test_topup_filter_user_search(self): + Topup.objects.create(amount=50.00, user=self.real_user) + other_user = User.objects.create_user( + username="otheruser", + password="testpassword", + first_name="Other", + last_name="User", + id_card="12345", + mobile="1234567890", + acc_no="5544332211", + ) + Topup.objects.create(amount=75.00, user=other_user) + url = reverse("create-list-topups") + "?user=Other" + response = self.client.get(url, format="json") + print(response.json()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 1) + self.assertEqual(response.json()["data"][0]["amount"], 75.00) + + def test_topup_filter_created_at(self): + now = timezone.now() + Topup.objects.create( + amount=50.00, user=self.real_user, created_at=now - timedelta(days=2) + ) + Topup.objects.create(amount=100.00, user=self.real_user, created_at=now) + url = reverse("create-list-topups") + f"?created_at_after={now.date()}" + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()["data"]), 1) + self.assertEqual(response.json()["data"][0]["amount"], 100.00) diff --git a/justfile b/justfile index bc25798..025916d 100644 --- a/justfile +++ b/justfile @@ -7,6 +7,14 @@ dev: migrate: python manage.py migrate - make-migrations: - python manage.py makemigrations \ No newline at end of file + python manage.py makemigrations + +seed-topups: + python manage.py seed_topups --number=50 +seed-payments: + python manage.py seed_payments --number=50 + +# TESTS +test-billing: + python manage.py test billing \ No newline at end of file From 4a944c176b5673df97de8377128d290288035944 Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:04:14 +0500 Subject: [PATCH 6/8] =?UTF-8?q?feat(billing):=20Add=20management=20command?= =?UTF-8?q?=20to=20seed=20topup=20models=20with=20dummy=20data=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 | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 billing/management/commands/seed_topups.py diff --git a/billing/management/commands/seed_topups.py b/billing/management/commands/seed_topups.py new file mode 100644 index 0000000..145b1c6 --- /dev/null +++ b/billing/management/commands/seed_topups.py @@ -0,0 +1,63 @@ +# billing/management/commands/seed_billing.py + +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 + + +class Command(BaseCommand): + help = "Seeds topup models with dummy data." + + def add_arguments(self, parser): + parser.add_argument( + "--number", + type=int, + default=10, + help="The number of topups to create.", + ) + + def handle(self, *args, **options): + number = options["number"] + fake = Faker() + + users = User.objects.all() + if not users.exists(): + self.stdout.write( + self.style.ERROR( + "No users found. Please seed users first (e.g., python manage.py seed_users)." + ) + ) + return + + self.stdout.write(self.style.NOTICE(f"Seeding {number} topups...")) + + 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) + + Topup.objects.create( + amount=fake.pydecimal( + left_digits=4, + right_digits=2, + positive=True, + min_value=100.00, + max_value=5000.00, + ), + paid=paid_status, + user=random_user, + paid_at=paid_at_date, + updated_at=timezone.now(), + ) + + self.stdout.write(self.style.SUCCESS(f"Successfully seeded {number} topups.")) From e4a01597aa88e6e21f8cf6087af403fd863a14dc Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:04:42 +0500 Subject: [PATCH 7/8] =?UTF-8?q?refactor(billing):=20Refactor=20TopupFilter?= =?UTF-8?q?=20to=20enhance=20user=20search=20functionality=20and=20update?= =?UTF-8?q?=20fields=20=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/filters.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/billing/filters.py b/billing/filters.py index c316e4f..97af140 100644 --- a/billing/filters.py +++ b/billing/filters.py @@ -1,5 +1,6 @@ import django_filters from .models import Payment, Topup +from django.db.models import Q class PaymentFilter(django_filters.FilterSet): @@ -21,11 +22,25 @@ class PaymentFilter(django_filters.FilterSet): class TopupFilter(django_filters.FilterSet): amount = django_filters.RangeFilter(field_name="amount") paid = django_filters.BooleanFilter(field_name="paid") - user = django_filters.CharFilter( - field_name="user__username", lookup_expr="icontains" - ) - created_at = django_filters.DateFromToRangeFilter() + user = django_filters.CharFilter(method="filter_user_search") + created_at = django_filters.DateFromToRangeFilter(field_name="created_at") + + def filter_user_search(self, queryset, name, value): + """ + Search across multiple user fields: first_name, last_name, id_card, mobile + """ + return queryset.filter( + Q(user__first_name__icontains=value) + | Q(user__last_name__icontains=value) + | Q(user__id_card__icontains=value) + | Q(user__mobile__icontains=value) + ) class Meta: model = Topup # Assuming Topup is a subclass of Payment - fields = "__all__" + fields = [ + "amount", + "paid", + "user", + "created_at", + ] From cec2045e5fd3b1fa9841c2550abb57dff4055bdb Mon Sep 17 00:00:00 2001 From: i701 Date: Thu, 3 Jul 2025 21:04:59 +0500 Subject: [PATCH 8/8] =?UTF-8?q?refactor(billing):=20Enhance=20TopupSeriali?= =?UTF-8?q?zer=20to=20include=20detailed=20user=20information=20and=20upda?= =?UTF-8?q?te=20ListCreateTopupView=20queryset=20filtering=20=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/serializers.py | 14 ++++++++++++-- billing/views.py | 14 +++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/billing/serializers.py b/billing/serializers.py index c859eb1..44dcc5c 100644 --- a/billing/serializers.py +++ b/billing/serializers.py @@ -1,7 +1,6 @@ from rest_framework import serializers from .models import Payment, Topup from devices.serializers import DeviceSerializer -from api.serializers import CustomReadOnlyUserSerializer class PaymentSerializer(serializers.ModelSerializer): @@ -21,7 +20,18 @@ class UpdatePaymentSerializer(serializers.ModelSerializer): class TopupSerializer(serializers.ModelSerializer): - user = CustomReadOnlyUserSerializer(read_only=True) + user = serializers.SerializerMethodField() + + def get_user(self, obj): + user = obj.user + if user: + return { + "id": user.id, + "name": user.first_name + " " + user.last_name, + "id_card": user.id_card, + "mobile": user.mobile, + } + return None class Meta: # type: ignore model = Topup diff --git a/billing/views.py b/billing/views.py index 620a4b5..9b4c4ae 100644 --- a/billing/views.py +++ b/billing/views.py @@ -281,6 +281,12 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView serializer = TopupSerializer(topup) return Response(serializer.data, status=status.HTTP_201_CREATED) + def get_queryset(self): + queryset = super().get_queryset() + if getattr(self.request.user, "is_admin") or self.request.user.is_superuser: + return queryset + return queryset.filter(user=self.request.user) + class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): queryset = Topup.objects.all() @@ -311,6 +317,7 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi topup.paid = True # topup.paid_at = timezone.now() # Assuming Topup model has paid_at field topup.mib_reference = mib_resp["transaction"]["ref"] or "" + topup.paid_at = timezone.now() topup.save() return True @@ -332,10 +339,11 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi "benefName": f"{user.first_name} {user.last_name}", # type: ignore "accountNo": user.acc_no, # type: ignore "absAmount": topup_instance.amount, - "time": localtime(timezone.now() + timedelta(minutes=5)).strftime( - "%Y-%m-%d %H:%M" - ), + "time": localtime( + topup_instance.created_at + timedelta(minutes=5) + ).strftime("%Y-%m-%d %H:%M"), } + print("payment payload in view ->", data) topup_status = self.verify_transfer_topup(data, topup_instance) if topup_status: return Response(