Add wallet balance to User model and implement Atoll/Island management

- Added `wallet_balance` field to the User model.
- Updated UserAdmin to include `wallet_balance` in the admin interface.
- Created serializers and views for Atoll and Island management.
- Implemented endpoints for listing, creating, and updating Atolls and Islands.
- Enhanced payment processing with UUIDs for Payment and Topup models.
- Added migration files for new fields and constraints.
- Improved error handling and validation in various views.
- Updated email templates for better responsiveness and SEO.
This commit is contained in:
2025-01-20 20:59:16 +05:00
parent 4d0eb86478
commit f6f77bb0e5
19 changed files with 513 additions and 108 deletions

View File

@ -1,3 +1,13 @@
from django.contrib import admin
from .models import Payment, BillFormula, Topup
# Register your models here.
class PaymentAdmin(admin.ModelAdmin):
list_display = ("id", "user", "amount", "paid", "paid_at", "method")
admin.site.register(Payment, PaymentAdmin)
admin.site.register(BillFormula)
admin.site.register(Topup)

View File

@ -0,0 +1,27 @@
# Generated by Django 5.1.2 on 2025-01-20 14:58
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0002_billformula_payment_topup_delete_device"),
]
operations = [
migrations.AlterField(
model_name="billformula",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="payment",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.1.2 on 2025-01-20 15:00
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0003_alter_billformula_id_alter_payment_id"),
]
operations = [
migrations.AlterField(
model_name="topup",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
]

View File

@ -1,35 +1,37 @@
from django.db import models
from django.utils import timezone
from api.models import User
import uuid
# Create your models here.
from devices.models import Device
# Create your models here.
class Payment(models.Model):
PAYMENT_TYPES = [
('WALLET', 'Wallet'),
('TRANSFER', 'Transfer'),
("WALLET", "Wallet"),
("TRANSFER", "Transfer"),
]
id = models.CharField(primary_key=True, max_length=255)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
number_of_months = models.IntegerField()
amount = models.FloatField()
paid = models.BooleanField(default=False)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='payments')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payments")
paid_at = models.DateTimeField(null=True, blank=True)
method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default='TRANSFER')
method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default="TRANSFER")
expires_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)
devices = models.ManyToManyField(Device, related_name='payments')
devices = models.ManyToManyField(Device, related_name="payments")
def __str__(self):
return f"Payment by {self.user}"
class BillFormula(models.Model):
id = models.CharField(primary_key=True, max_length=255)
formula = models.CharField(max_length=255)
base_amount = models.FloatField()
discount_percentage = models.FloatField()
@ -39,10 +41,11 @@ class BillFormula(models.Model):
def __str__(self):
return self.formula
class Topup(models.Model):
id = models.CharField(primary_key=True, max_length=255)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
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)
created_at = models.DateTimeField(default=timezone.now)
updated_at = models.DateTimeField(auto_now=True)

11
billing/serializers.py Normal file
View File

@ -0,0 +1,11 @@
from rest_framework import serializers
from .models import Payment
from devices.serializers import DeviceSerializer
class PaymentSerializer(serializers.ModelSerializer):
devices = DeviceSerializer(many=True, read_only=True)
class Meta:
model = Payment
fields = "__all__"

8
billing/urls.py Normal file
View File

@ -0,0 +1,8 @@
# billing/urls.py
from django.urls import path
from .views import CreatePaymentView, VerifyPaymentView
urlpatterns = [
path("create-payment/", CreatePaymentView.as_view(), name="create-payment"),
path("verify-payment/", VerifyPaymentView.as_view(), name="verify-payment"),
]

View File

@ -1,2 +1,141 @@
# Create your views here.
# billing/views.py
from .models import Payment, Device
from datetime import datetime, timedelta
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from decouple import config
from .serializers import PaymentSerializer
from rest_framework.permissions import AllowAny
from api.mixins import StaffEditorPermissionMixin
from rest_framework import generics
class InsufficientFundsError(Exception):
pass
class CreatePaymentView(StaffEditorPermissionMixin, generics.CreateAPIView):
serializer_class = PaymentSerializer
queryset = Payment.objects.all()
def create(self, request):
data = request.data
user = request.user
amount = data.get("amount")
number_of_months = data.get("number_of_months")
device_ids = data.get("device_ids", [])
print(amount, number_of_months, device_ids)
if not amount or not number_of_months:
return Response(
{"message": "amount and number_of_months are required."},
status=status.HTTP_400_BAD_REQUEST,
)
if not device_ids:
return Response(
{"message": "device_ids are required."},
status=status.HTTP_400_BAD_REQUEST,
)
# Create payment
payment = Payment.objects.create(
amount=amount,
number_of_months=number_of_months,
paid=data.get("paid", False),
user=user,
)
# Connect devices to payment
devices = Device.objects.filter(id__in=device_ids, user=user)
payment.devices.set(devices)
serializer = PaymentSerializer(payment)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
serializer_class = PaymentSerializer
queryset = Payment.objects.all()
def update(self, request, *args, **kwargs):
data = request.data
user = request.user
print(user)
method = data.get("method")
payment_id = data.get("payment_id")
abs_amount = data.get("abs_amount")
if not method:
return Response(
{"message": "method is required. 'WALLET' or 'TRANSFER'"},
status=status.HTTP_400_BAD_REQUEST,
)
if not payment_id:
return Response(
{"message": "payment_id is required."},
status=status.HTTP_400_BAD_REQUEST,
)
if not abs_amount:
return Response(
{"message": "abs_amount is required."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
payment = Payment.objects.get(id=payment_id)
devices = payment.devices.all()
if data["type"] == "WALLET":
print("processing WALLET payment")
self.process_wallet_payment(user, payment, float(data["abs_amount"]))
elif data["type"] == "TRANSFER":
self.verify_external_payment(data, payment)
# Update devices
expiry_date = datetime.now() + timedelta(days=30 * payment.number_of_months)
devices.update(is_active=True, expiry_date=expiry_date)
return Response({"message": "Payment verified successfully."})
except Payment.DoesNotExist:
return Response(
{"message": "Payment not found."}, status=status.HTTP_404_NOT_FOUND
)
except InsufficientFundsError:
return Response(
{"message": "Insufficient funds in wallet."},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
return Response(
{"message": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def process_wallet_payment(self, user, payment, amount):
print("processing wallet payment")
print(user, amount)
if user.wallet_balance < amount:
return Response(
{"message": "Insufficient funds in wallet."},
status=status.HTTP_400_BAD_REQUEST,
)
payment.paid = True
payment.paid_at = datetime.now()
payment.method = "WALLET"
payment.save()
user.wallet_balance -= amount
user.save()
def verify_external_payment(self, data, payment):
response = requests.post(
f"{config('PAYMENT_VERIFY_BASE_URL')}/verify-payment",
json=data,
headers={"Content-Type": "application/json"},
)
response.raise_for_status()
print(response.json())
if not response.json().get("success"):
raise Exception("Payment verification failed.")