Merge pull request #6 from i701/feat/topups
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 4m19s

feature/topups
This commit is contained in:
Abdulla Aidhaan
2025-07-04 20:16:22 +05:00
committed by GitHub
5 changed files with 69 additions and 14 deletions

View File

@ -18,9 +18,7 @@ def assign_device_permissions(sender, instance, created, **kwargs):
atoll_read_permission = Permission.objects.get(codename="view_atoll") atoll_read_permission = Permission.objects.get(codename="view_atoll")
island_read_permission = Permission.objects.get(codename="view_island") island_read_permission = Permission.objects.get(codename="view_island")
payment_permissions = Permission.objects.filter(content_type__model="payment") payment_permissions = Permission.objects.filter(content_type__model="payment")
topup_permissions = Permission.objects.filter( topup_permissions = Permission.objects.filter(content_type__model="topup")
content_type__model="topup"
).exclude(codename__startswith="delete_")
for permission in topup_permissions: for permission in topup_permissions:
instance.user_permissions.add(permission) instance.user_permissions.add(permission)

View File

@ -46,6 +46,7 @@ class TopupSerializer(serializers.ModelSerializer):
"paid", "paid",
"mib_reference", "mib_reference",
"is_expired", "is_expired",
"expires_at",
"created_at", "created_at",
"updated_at", "updated_at",
] ]

View File

@ -198,3 +198,10 @@ class TopupTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.json()["amount"], 50.00) self.assertEqual(response.json()["amount"], 50.00)
self.assertEqual(response.json()["user"]["id"], getattr(self.real_user, "id")) 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)

View File

@ -9,6 +9,7 @@ from .views import (
ListCreateTopupView, ListCreateTopupView,
VerifyTopupPaymentAPIView, VerifyTopupPaymentAPIView,
TopupDetailAPIView, TopupDetailAPIView,
DeleteTopupView,
) )
urlpatterns = [ urlpatterns = [
@ -35,4 +36,9 @@ urlpatterns = [
VerifyTopupPaymentAPIView.as_view(), VerifyTopupPaymentAPIView.as_view(),
name="verify-topup-payment", name="verify-topup-payment",
), ),
path(
"topup/<str:pk>/delete/",
DeleteTopupView.as_view(),
name="delete-topup",
),
] ]

View File

@ -18,6 +18,8 @@ import logging
from .models import Device, Payment, Topup from .models import Device, Payment, Topup
from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer
from .filters import PaymentFilter, TopupFilter from .filters import PaymentFilter, TopupFilter
from dataclasses import dataclass, asdict
from typing import Optional
env.read_env(os.path.join(BASE_DIR, ".env")) env.read_env(os.path.join(BASE_DIR, ".env"))
@ -271,13 +273,15 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
data = request.data data = request.data
user = request.user user = request.user
current_time = timezone.now()
expires_at = current_time + timedelta(minutes=10) # Topup expires in 10 minutes
amount = data.get("amount") amount = data.get("amount")
if not amount: if not amount:
return Response( return Response(
{"message": "amount is required."}, {"message": "amount is required."},
status=status.HTTP_400_BAD_REQUEST, 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) serializer = TopupSerializer(topup)
return Response(serializer.data, status=status.HTTP_201_CREATED) 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) 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): class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
queryset = Topup.objects.all() queryset = Topup.objects.all()
serializer_class = TopupSerializer serializer_class = TopupSerializer
lookup_field = "pk" lookup_field = "pk"
def verify_transfer_topup(self, data, topup): def verify_transfer_topup(self, data, topup) -> PaymentVerificationResponse:
if not PAYMENT_BASE_URL: if not PAYMENT_BASE_URL:
raise ValueError( raise ValueError(
"PAYMENT_BASE_URL is not set. Please set it in your environment variables." "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() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
logger.error(f"HTTPError: {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() mib_resp = response.json()
print(mib_resp) print(mib_resp)
if not response.json().get("success"): if not response.json().get("success"):
return mib_resp["success"] return PaymentVerificationResponse(
message=mib_resp["message"],
success=mib_resp["success"],
transaction=None,
)
else: else:
topup.paid = True 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.mib_reference = mib_resp["transaction"]["ref"] or ""
topup.paid_at = timezone.now() topup.paid_at = timezone.now()
topup.save() 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): def update(self, request, *args, **kwargs):
topup_instance = self.get_object() topup_instance = self.get_object()
@ -355,16 +387,27 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi
topup_instance.created_at + timedelta(minutes=5) topup_instance.created_at + timedelta(minutes=5)
).strftime("%Y-%m-%d %H:%M"), ).strftime("%Y-%m-%d %H:%M"),
} }
print("payment payload in view ->", data) print("DATA", data)
topup_status = self.verify_transfer_topup(data, topup_instance) topup_verification_response = self.verify_transfer_topup(data, topup_instance)
if topup_status: print("TOPUP VERIFICATION RESPONSE", topup_verification_response)
if topup_verification_response.success:
return Response( 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, status=status.HTTP_200_OK,
) )
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )