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, + )