mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-06-28 22:03:59 +00:00
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:
@ -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)
|
||||
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
20
billing/migrations/0004_alter_topup_id.py
Normal file
20
billing/migrations/0004_alter_topup_id.py
Normal 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
|
||||
),
|
||||
),
|
||||
]
|
@ -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
11
billing/serializers.py
Normal 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
8
billing/urls.py
Normal 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"),
|
||||
]
|
141
billing/views.py
141
billing/views.py
@ -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.")
|
||||
|
Reference in New Issue
Block a user