From ddb65ca985e61f020f5b2903194d9d449945e108 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 20:14:37 +0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(billing):=20Add=20delete=20functionali?= =?UTF-8?q?ty=20for=20topups=20and=20update=20creation=20logic=20with=20ex?= =?UTF-8?q?piration=20time=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- billing/tests.py | 7 ++++++ billing/urls.py | 6 +++++ billing/views.py | 65 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/billing/tests.py b/billing/tests.py index ce550dd..9964be4 100644 --- a/billing/tests.py +++ b/billing/tests.py @@ -198,3 +198,10 @@ class TopupTests(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.json()["amount"], 50.00) self.assertEqual(response.json()["user"]["id"], getattr(self.real_user, "id")) + + def test_delete_topup(self): + topup = Topup.objects.create(amount=50.00, user=self.real_user) + url = reverse("delete-topup", kwargs={"pk": topup.pk}) + response = self.client.delete(url, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(Topup.objects.count(), 0) diff --git a/billing/urls.py b/billing/urls.py index 143ae4e..94d8bf4 100644 --- a/billing/urls.py +++ b/billing/urls.py @@ -9,6 +9,7 @@ from .views import ( ListCreateTopupView, VerifyTopupPaymentAPIView, TopupDetailAPIView, + DeleteTopupView, ) urlpatterns = [ @@ -35,4 +36,9 @@ urlpatterns = [ VerifyTopupPaymentAPIView.as_view(), name="verify-topup-payment", ), + path( + "topup//delete/", + DeleteTopupView.as_view(), + name="delete-topup", + ), ] diff --git a/billing/views.py b/billing/views.py index 063f9cd..7c39458 100644 --- a/billing/views.py +++ b/billing/views.py @@ -18,6 +18,8 @@ import logging from .models import Device, Payment, Topup from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer from .filters import PaymentFilter, TopupFilter +from dataclasses import dataclass, asdict +from typing import Optional env.read_env(os.path.join(BASE_DIR, ".env")) @@ -271,13 +273,15 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView def create(self, request, *args, **kwargs): data = request.data user = request.user + current_time = timezone.now() + expires_at = current_time + timedelta(minutes=10) # Topup expires in 10 minutes 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) + topup = Topup.objects.create(amount=amount, user=user, expires_at=expires_at) serializer = TopupSerializer(topup) return Response(serializer.data, status=status.HTTP_201_CREATED) @@ -300,12 +304,26 @@ class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView): return queryset.filter(user=self.request.user) +@dataclass +class Transaction: + ref: str + sourceBank: str + trxDate: str + + +@dataclass +class PaymentVerificationResponse: + message: str + success: bool + transaction: Optional[Transaction] = None + + class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): queryset = Topup.objects.all() serializer_class = TopupSerializer lookup_field = "pk" - def verify_transfer_topup(self, data, topup): + def verify_transfer_topup(self, data, topup) -> PaymentVerificationResponse: if not PAYMENT_BASE_URL: raise ValueError( "PAYMENT_BASE_URL is not set. Please set it in your environment variables." @@ -320,18 +338,32 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi response.raise_for_status() except requests.exceptions.HTTPError as e: logger.error(f"HTTPError: {e}") - return False # Or handle the error as appropriate + return PaymentVerificationResponse( + message="Payment verification failed.", success=False, transaction=None + ) mib_resp = response.json() print(mib_resp) if not response.json().get("success"): - return mib_resp["success"] + return PaymentVerificationResponse( + message=mib_resp["message"], + success=mib_resp["success"], + transaction=None, + ) else: topup.paid = True - # topup.paid_at = timezone.now() # Assuming Topup model has paid_at field + topup.paid_at = timezone.now() topup.mib_reference = mib_resp["transaction"]["ref"] or "" topup.paid_at = timezone.now() topup.save() - return True + return PaymentVerificationResponse( + message=mib_resp["message"], + success=mib_resp["success"], + transaction=Transaction( + ref=topup.mib_reference, + sourceBank=mib_resp["transaction"]["sourceBank"], + trxDate=mib_resp["transaction"]["trxDate"], + ), + ) def update(self, request, *args, **kwargs): topup_instance = self.get_object() @@ -355,16 +387,27 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi 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: + print("DATA", data) + topup_verification_response = self.verify_transfer_topup(data, topup_instance) + print("TOPUP VERIFICATION RESPONSE", topup_verification_response) + if topup_verification_response.success: return Response( - {"message": "Topup payment verified successfully."}, + { + "status": topup_verification_response.success, + "message": topup_verification_response.message, + "transaction": asdict(topup_verification_response.transaction) + if topup_verification_response.transaction + else None, + }, status=status.HTTP_200_OK, ) else: return Response( - {"message": "Topup payment verification failed."}, + { + "status": topup_verification_response.success, + "message": topup_verification_response.message + or "Topup payment verification failed.", + }, status=status.HTTP_400_BAD_REQUEST, ) From 70f8efb19a273ee085f1c2e163d101a417f3add1 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 20:14:48 +0500 Subject: [PATCH 2/3] =?UTF-8?q?refactor(signals):=20Simplify=20topup=20per?= =?UTF-8?q?missions=20assignment=20by=20including=20all=20permissions=20?= =?UTF-8?q?=F0=9F=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/signals.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/api/signals.py b/api/signals.py index bd41c73..142ff6a 100644 --- a/api/signals.py +++ b/api/signals.py @@ -18,9 +18,7 @@ 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_") + topup_permissions = Permission.objects.filter(content_type__model="topup") for permission in topup_permissions: instance.user_permissions.add(permission) From e8e6a09b24732810f00f8411d5db39a5d4b72562 Mon Sep 17 00:00:00 2001 From: i701 Date: Fri, 4 Jul 2025 20:14:57 +0500 Subject: [PATCH 3/3] =?UTF-8?q?feat(billing):=20Add=20'expires=5Fat'=20fie?= =?UTF-8?q?ld=20to=20TopupSerializer=20for=20better=20expiration=20trackin?= =?UTF-8?q?g=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 8b613a8..a03bc76 100644 --- a/billing/serializers.py +++ b/billing/serializers.py @@ -46,6 +46,7 @@ class TopupSerializer(serializers.ModelSerializer): "paid", "mib_reference", "is_expired", + "expires_at", "created_at", "updated_at", ]