mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-07-07 12:16:30 +00:00
feat(billing): WIP Add Topup model, filters, serializers, and views for topup management ✨
This commit is contained in:
@ -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__"
|
||||||
|
17
billing/migrations/0006_topup_mib_reference.py
Normal file
17
billing/migrations/0006_topup_mib_reference.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
17
billing/migrations/0007_topup_paid_at.py
Normal file
17
billing/migrations/0007_topup_paid_at.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
|
||||||
|
@ -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"]
|
||||||
|
@ -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",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
Reference in New Issue
Block a user