feat(billing): WIP Add Topup model, filters, serializers, and views for topup management

This commit is contained in:
2025-07-03 17:13:25 +05:00
parent bae0882879
commit c07d3c93d2
7 changed files with 172 additions and 7 deletions

View File

@ -1,5 +1,5 @@
import django_filters import django_filters
from .models import Payment from .models import Payment, Topup
class PaymentFilter(django_filters.FilterSet): class PaymentFilter(django_filters.FilterSet):
@ -16,3 +16,16 @@ class PaymentFilter(django_filters.FilterSet):
class Meta: class Meta:
model = Payment model = Payment
fields = "__all__" 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__"

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -51,6 +51,8 @@ class Topup(models.Model):
amount = models.FloatField() amount = models.FloatField()
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups") user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="topups")
paid = models.BooleanField(default=False) 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) created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)

View File

@ -1,19 +1,37 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Payment from .models import Payment, Topup
from devices.serializers import DeviceSerializer from devices.serializers import DeviceSerializer
from api.serializers import CustomReadOnlyUserSerializer
class PaymentSerializer(serializers.ModelSerializer): class PaymentSerializer(serializers.ModelSerializer):
devices = DeviceSerializer(many=True, read_only=True) devices = DeviceSerializer(many=True, read_only=True)
class Meta: class Meta: # type: ignore
model = Payment model = Payment
fields = "__all__" fields = "__all__"
class UpdatePaymentSerializer(serializers.ModelSerializer): class UpdatePaymentSerializer(serializers.ModelSerializer):
class Meta: class Meta: # type: ignore
model = Payment model = Payment
fields = [ fields = [
"number_of_months", "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"]

View File

@ -6,6 +6,8 @@ from .views import (
PaymentDetailAPIView, PaymentDetailAPIView,
UpdatePaymentAPIView, UpdatePaymentAPIView,
DeletePaymentView, DeletePaymentView,
ListCreateTopupView,
VerifyTopupPaymentAPIView,
) )
urlpatterns = [ urlpatterns = [
@ -24,4 +26,12 @@ urlpatterns = [
path( path(
"payment/<str:pk>/verify/", VerifyPaymentView.as_view(), name="verify-payment" "payment/<str:pk>/verify/", VerifyPaymentView.as_view(), name="verify-payment"
), ),
# Topups
path("topup/", ListCreateTopupView.as_view(), name="create-list-topups"),
# path("topup/<str:pk>/", TopupDetailAPIView.as_view(), name="retrieve-topup"),
path(
"topup/<str:pk>/verify/",
VerifyTopupPaymentAPIView.as_view(),
name="verify-topup-payment",
),
] ]

View File

@ -15,9 +15,9 @@ from api.tasks import add_new_devices_to_omada
from apibase.env import BASE_DIR, env from apibase.env import BASE_DIR, env
import logging import logging
from .models import Device, Payment from .models import Device, Payment, Topup
from .serializers import PaymentSerializer, UpdatePaymentSerializer from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer
from .filters import PaymentFilter from .filters import PaymentFilter, TopupFilter
env.read_env(os.path.join(BASE_DIR, ".env")) env.read_env(os.path.join(BASE_DIR, ".env"))
@ -259,3 +259,91 @@ class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView):
devices = instance.devices.all() devices = instance.devices.all()
devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False) devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False)
return super().delete(request, *args, **kwargs) 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,
)