mirror of
https://github.com/i701/sarlink-portal-api.git
synced 2025-02-20 19:32:00 +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:
parent
4d0eb86478
commit
f6f77bb0e5
@ -15,6 +15,7 @@ class UserAdmin(BaseUserAdmin):
|
||||
"is_staff",
|
||||
"mobile",
|
||||
"address",
|
||||
"wallet_balance",
|
||||
"acc_no",
|
||||
"id_card",
|
||||
"dob",
|
||||
@ -35,6 +36,7 @@ class UserAdmin(BaseUserAdmin):
|
||||
"email",
|
||||
"mobile",
|
||||
"address",
|
||||
"wallet_balance",
|
||||
"acc_no",
|
||||
"id_card",
|
||||
"dob",
|
||||
|
22
api/migrations/0005_alter_atoll_name_alter_island_name.py
Normal file
22
api/migrations/0005_alter_atoll_name_alter_island_name.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.2 on 2025-01-20 14:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0004_alter_atoll_id_alter_island_id"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="atoll",
|
||||
name="name",
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="island",
|
||||
name="name",
|
||||
field=models.CharField(max_length=255, unique=True),
|
||||
),
|
||||
]
|
@ -7,6 +7,7 @@ from django.db import models
|
||||
from .managers import CustomUserManager
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
email = models.EmailField(unique=True, blank=True, null=True)
|
||||
address = models.CharField(max_length=255, blank=True)
|
||||
@ -20,8 +21,13 @@ class User(AbstractUser):
|
||||
policy_accepted = models.BooleanField(default=False)
|
||||
wallet_balance = models.FloatField(default=0.0)
|
||||
ninja_user_id = models.CharField(max_length=255, blank=True)
|
||||
atoll = models.ForeignKey('Atoll', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
|
||||
island = models.ForeignKey('Island', on_delete=models.SET_NULL, null=True, blank=True, related_name='users')
|
||||
atoll = models.ForeignKey(
|
||||
"Atoll", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
||||
)
|
||||
island = models.ForeignKey(
|
||||
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
||||
)
|
||||
|
||||
def get_all_fields(self, instance):
|
||||
return [field.name for field in instance.get_fields()]
|
||||
|
||||
@ -29,16 +35,17 @@ class User(AbstractUser):
|
||||
|
||||
|
||||
class Atoll(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Island(models.Model):
|
||||
atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name='islands')
|
||||
name = models.CharField(max_length=255)
|
||||
atoll = models.ForeignKey(Atoll, on_delete=models.CASCADE, related_name="islands")
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from knox.models import AuthToken
|
||||
from django.contrib.auth import authenticate
|
||||
from api.models import User
|
||||
from api.models import User, Atoll, Island
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
@ -46,7 +46,6 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
|
||||
"username",
|
||||
"mobile",
|
||||
"address",
|
||||
|
||||
)
|
||||
|
||||
|
||||
@ -92,3 +91,15 @@ class KnoxTokenSerializer(serializers.ModelSerializer):
|
||||
class Meta: # type: ignore
|
||||
model = AuthToken
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AtollSerializer(serializers.ModelSerializer):
|
||||
class Meta: # type: ignore
|
||||
model = Atoll
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class IslandSerializer(serializers.ModelSerializer):
|
||||
class Meta: # type: ignore
|
||||
model = Island
|
||||
fields = "__all__"
|
||||
|
@ -7,17 +7,23 @@ from django.db.models.signals import post_save
|
||||
from api.models import User
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def assign_device_permissions(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
# Assign all permissions for devices and read permission for atoll and island
|
||||
device_permissions = Permission.objects.filter(content_type__model='device')
|
||||
atoll_read_permission = Permission.objects.get(codename='view_atoll')
|
||||
island_read_permission = Permission.objects.get(codename='view_island')
|
||||
device_permissions = Permission.objects.filter(content_type__model="device")
|
||||
atoll_read_permission = Permission.objects.get(codename="view_atoll")
|
||||
island_read_permission = Permission.objects.get(codename="view_island")
|
||||
payment_permissions = Permission.objects.filter(
|
||||
content_type__model="payment"
|
||||
).exclude(codename="delete_payment")
|
||||
|
||||
for permission in device_permissions:
|
||||
instance.user_permissions.add(permission)
|
||||
instance.user_permissions.add(atoll_read_permission, island_read_permission)
|
||||
for permission in payment_permissions:
|
||||
instance.user_permissions.add(permission)
|
||||
|
||||
|
||||
@receiver(reset_password_token_created)
|
||||
|
17
api/urls.py
17
api/urls.py
@ -11,6 +11,10 @@ from .views import (
|
||||
UserDetailAPIView,
|
||||
healthcheck,
|
||||
test_email,
|
||||
ListCreateAtollView,
|
||||
RetrieveUpdateDestroyAtollView,
|
||||
ListCreateIslandView,
|
||||
RetrieveUpdateDestroyIslandView,
|
||||
)
|
||||
|
||||
|
||||
@ -26,5 +30,16 @@ urlpatterns = [
|
||||
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
||||
path("healthcheck/", healthcheck, name="healthcheck"),
|
||||
path("test/", test_email, name="testemail"),
|
||||
|
||||
path("atolls/", ListCreateAtollView.as_view(), name="atolls"),
|
||||
path(
|
||||
"atolls/<int:pk>/",
|
||||
RetrieveUpdateDestroyAtollView.as_view(),
|
||||
name="atoll-detail",
|
||||
),
|
||||
path("islands/", ListCreateIslandView.as_view(), name="islands"),
|
||||
path(
|
||||
"islands/<int:pk>/",
|
||||
RetrieveUpdateDestroyIslandView.as_view(),
|
||||
name="island-detail",
|
||||
),
|
||||
]
|
||||
|
76
api/views.py
76
api/views.py
@ -11,6 +11,7 @@ from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from api.serializers import AtollSerializer, IslandSerializer
|
||||
|
||||
# knox imports
|
||||
from knox.views import LoginView as KnoxLoginView
|
||||
@ -85,13 +86,19 @@ class CreateUserView(generics.CreateAPIView):
|
||||
return Response({"message": "Policy acceptance is required."}, status=400)
|
||||
|
||||
if not re.match(r"^[A-Z]{1,2}[0-9]{6,7}$", id_card):
|
||||
return Response({"message": "Please enter a valid ID card number."}, status=400)
|
||||
return Response(
|
||||
{"message": "Please enter a valid ID card number."}, status=400
|
||||
)
|
||||
|
||||
if not re.match(r"^[7|9][0-9]{6}$", mobile):
|
||||
return Response({"message": "Please enter a valid mobile number."}, status=400)
|
||||
return Response(
|
||||
{"message": "Please enter a valid mobile number."}, status=400
|
||||
)
|
||||
|
||||
if not re.match(r"^(7\d{12}|9\d{16})$", acc_no):
|
||||
return Response({"message": "Please enter a valid account number."}, status=400)
|
||||
return Response(
|
||||
{"message": "Please enter a valid account number."}, status=400
|
||||
)
|
||||
|
||||
# Fetch Atoll and Island instances
|
||||
try:
|
||||
@ -109,7 +116,7 @@ class CreateUserView(generics.CreateAPIView):
|
||||
username=username,
|
||||
password=password,
|
||||
address=address,
|
||||
mobile=str("+960") + str(mobile),
|
||||
mobile=mobile,
|
||||
acc_no=acc_no,
|
||||
id_card=id_card,
|
||||
dob=dob,
|
||||
@ -219,3 +226,64 @@ def test_email(request):
|
||||
fail_silently=False,
|
||||
)
|
||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ListCreateAtollView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||
serializer_class = AtollSerializer
|
||||
queryset = Atoll.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name = serializer.validated_data.get("name")
|
||||
print(name)
|
||||
if Atoll.objects.filter(name=name).exists():
|
||||
return Response({"message": "Atoll name already exists."}, status=400)
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveUpdateDestroyAtollView(
|
||||
StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView
|
||||
):
|
||||
serializer_class = AtollSerializer
|
||||
queryset = Atoll.objects.all()
|
||||
lookup_field = "pk"
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name = serializer.validated_data.get("name")
|
||||
if name and Atoll.objects.filter(name=name).exclude(pk=instance.pk).exists():
|
||||
return Response({"message": "Atoll name already exists."}, status=400)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ListCreateIslandView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||
serializer_class = IslandSerializer
|
||||
queryset = Island.objects.all()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name = serializer.validated_data.get("name")
|
||||
if Island.objects.filter(name=name).exists():
|
||||
return Response({"message": "Island name already exists."}, status=400)
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetrieveUpdateDestroyIslandView(
|
||||
StaffEditorPermissionMixin, generics.RetrieveUpdateDestroyAPIView
|
||||
):
|
||||
serializer_class = IslandSerializer
|
||||
queryset = Island.objects.all()
|
||||
lookup_field = "pk"
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
name = serializer.validated_data.get("name")
|
||||
if name and Island.objects.filter(name=name).exclude(pk=instance.pk).exists():
|
||||
return Response({"message": "Island name already exists."}, status=400)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
@ -36,7 +36,8 @@ urlpatterns = [
|
||||
path("api/auth/", include("api.urls")),
|
||||
# Devices
|
||||
path("api/devices/", include("devices.urls")),
|
||||
|
||||
# Billing
|
||||
path("api/billing/", include("billing.urls")),
|
||||
]
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
@ -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.")
|
||||
|
@ -2,7 +2,11 @@ from rest_framework import generics, status
|
||||
from rest_framework.response import Response
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from .models import Device
|
||||
from .serializers import CreateDeviceSerializer, DeviceSerializer, ReadOnlyDeviceSerializer
|
||||
from .serializers import (
|
||||
CreateDeviceSerializer,
|
||||
DeviceSerializer,
|
||||
ReadOnlyDeviceSerializer,
|
||||
)
|
||||
from api.mixins import StaffEditorPermissionMixin
|
||||
from .filters import DeviceFilter
|
||||
import re
|
||||
@ -27,16 +31,17 @@ class DeviceListCreateAPIView(
|
||||
def create(self, request, *args, **kwargs):
|
||||
mac = request.data.get("mac", None)
|
||||
if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac):
|
||||
return Response({"error": "Invalid mac address"}, status=400)
|
||||
return Response({"message": "Invalid mac address."}, status=400)
|
||||
if Device.objects.filter(mac=mac).exists():
|
||||
return Response({"error": "Device with this mac already exists"}, status=400)
|
||||
return Response(
|
||||
{"message": "Device with this mac address already exists."}, status=400
|
||||
)
|
||||
return super().create(request, *args, **kwargs)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
|
||||
class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||
queryset = Device.objects.select_related("user").all()
|
||||
serializer_class = ReadOnlyDeviceSerializer
|
||||
@ -52,6 +57,9 @@ class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
# Pass 'partial=True' to allow partial updates
|
||||
partial = kwargs.pop("partial", True)
|
||||
instance = self.get_object()
|
||||
mac = request.data.get("mac", None)
|
||||
if not re.match(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$", mac):
|
||||
return Response({"message": "Invalid mac address"}, status=400)
|
||||
serializer = self.get_serializer(instance, data=request.data, partial=partial)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
@ -73,4 +81,3 @@ class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||
{"message": f"Device '{device_name}' deleted."},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Your login token">
|
||||
<meta name="keywords" content="login, token, SARLink Portal">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Your Login Token</title>
|
||||
@ -62,6 +66,7 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
@ -85,4 +90,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
@ -1,10 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Your verification token">
|
||||
<meta name="keywords" content="verification, token, sarlink">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Your Verification Token</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Use this verification code: {{ callback_token }}</h2>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
|
||||
# import os
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import PermissionDenied
|
||||
@ -7,10 +8,12 @@ from django.template import loader
|
||||
from django.utils import timezone
|
||||
from djangopasswordlessknox.models import CallbackToken
|
||||
from djangopasswordlessknox.settings import api_settings
|
||||
|
||||
# from twilio.rest import Client
|
||||
from decouple import config
|
||||
import requests
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
|
||||
@ -27,9 +30,13 @@ def authenticate_by_token(callback_token):
|
||||
return token.user
|
||||
|
||||
except CallbackToken.DoesNotExist:
|
||||
logger.debug("djangopasswordlessknox: Challenged with a callback token that doesn't exist.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: Challenged with a callback token that doesn't exist."
|
||||
)
|
||||
except User.DoesNotExist:
|
||||
logger.debug("djangopasswordlessknox: Authenticated user somehow doesn't exist.")
|
||||
logger.debug(
|
||||
"djangopasswordlessknox: Authenticated user somehow doesn't exist."
|
||||
)
|
||||
except PermissionDenied:
|
||||
logger.debug("djangopasswordlessknox: Permission denied while authenticating.")
|
||||
|
||||
@ -41,15 +48,19 @@ def create_callback_token_for_user(user, token_type):
|
||||
token = None
|
||||
token_type = token_type.upper()
|
||||
|
||||
if token_type == 'EMAIL':
|
||||
token = CallbackToken.objects.create(user=user,
|
||||
to_alias_type=token_type,
|
||||
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))
|
||||
if token_type == "EMAIL":
|
||||
token = CallbackToken.objects.create(
|
||||
user=user,
|
||||
to_alias_type=token_type,
|
||||
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME),
|
||||
)
|
||||
|
||||
elif token_type == 'MOBILE':
|
||||
token = CallbackToken.objects.create(user=user,
|
||||
to_alias_type=token_type,
|
||||
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME))
|
||||
elif token_type == "MOBILE":
|
||||
token = CallbackToken.objects.create(
|
||||
user=user,
|
||||
to_alias_type=token_type,
|
||||
to_alias=getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME),
|
||||
)
|
||||
|
||||
if token is not None:
|
||||
return token
|
||||
@ -83,12 +94,20 @@ def verify_user_alias(user, token):
|
||||
"""
|
||||
Marks a user's contact point as verified depending on accepted token type.
|
||||
"""
|
||||
if token.to_alias_type == 'EMAIL':
|
||||
if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME):
|
||||
setattr(user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True)
|
||||
elif token.to_alias_type == 'MOBILE':
|
||||
if token.to_alias == getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME):
|
||||
setattr(user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True)
|
||||
if token.to_alias_type == "EMAIL":
|
||||
if token.to_alias == getattr(
|
||||
user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME
|
||||
):
|
||||
setattr(
|
||||
user, api_settings.PASSWORDLESS_USER_EMAIL_VERIFIED_FIELD_NAME, True
|
||||
)
|
||||
elif token.to_alias_type == "MOBILE":
|
||||
if token.to_alias == getattr(
|
||||
user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME
|
||||
):
|
||||
setattr(
|
||||
user, api_settings.PASSWORDLESS_USER_MOBILE_VERIFIED_FIELD_NAME, True
|
||||
)
|
||||
else:
|
||||
return False
|
||||
user.save()
|
||||
@ -116,32 +135,47 @@ def send_email_with_callback_token(user, email_token, **kwargs):
|
||||
# Make sure we have a sending address before sending.
|
||||
|
||||
# Get email subject and message
|
||||
email_subject = kwargs.get('email_subject',
|
||||
api_settings.PASSWORDLESS_EMAIL_SUBJECT)
|
||||
email_plaintext = kwargs.get('email_plaintext',
|
||||
api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE)
|
||||
email_html = kwargs.get('email_html',
|
||||
api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME)
|
||||
email_subject = kwargs.get(
|
||||
"email_subject", api_settings.PASSWORDLESS_EMAIL_SUBJECT
|
||||
)
|
||||
email_plaintext = kwargs.get(
|
||||
"email_plaintext", api_settings.PASSWORDLESS_EMAIL_PLAINTEXT_MESSAGE
|
||||
)
|
||||
email_html = kwargs.get(
|
||||
"email_html", api_settings.PASSWORDLESS_EMAIL_TOKEN_HTML_TEMPLATE_NAME
|
||||
)
|
||||
# Inject context if user specifies.
|
||||
context = inject_template_context({'callback_token': email_token.key, })
|
||||
html_message = loader.render_to_string(email_html, context,)
|
||||
context = inject_template_context(
|
||||
{
|
||||
"callback_token": email_token.key,
|
||||
}
|
||||
)
|
||||
html_message = loader.render_to_string(
|
||||
email_html,
|
||||
context,
|
||||
)
|
||||
send_mail(
|
||||
email_subject,
|
||||
email_plaintext % email_token.key,
|
||||
api_settings.PASSWORDLESS_EMAIL_NOREPLY_ADDRESS,
|
||||
[getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)],
|
||||
fail_silently=False,
|
||||
html_message=html_message,)
|
||||
html_message=html_message,
|
||||
)
|
||||
|
||||
else:
|
||||
logger.debug("Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS.")
|
||||
logger.debug(
|
||||
"Failed to send token email. Missing PASSWORDLESS_EMAIL_NOREPLY_ADDRESS."
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug("Failed to send token email to user: %d."
|
||||
"Possibly no email on user object. Email entered was %s" %
|
||||
(user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME)))
|
||||
logger.debug(
|
||||
"Failed to send token email to user: %d."
|
||||
"Possibly no email on user object. Email entered was %s"
|
||||
% (user.id, getattr(user, api_settings.PASSWORDLESS_USER_EMAIL_FIELD_NAME))
|
||||
)
|
||||
logger.debug(e)
|
||||
return False
|
||||
|
||||
@ -152,60 +186,62 @@ def send_sms_with_callback_token(user, mobile_token, **kwargs):
|
||||
|
||||
Passes silently without sending in test environment.
|
||||
"""
|
||||
base_string = kwargs.get('mobile_message', api_settings.PASSWORDLESS_MOBILE_MESSAGE)
|
||||
base_string = kwargs.get("mobile_message", api_settings.PASSWORDLESS_MOBILE_MESSAGE)
|
||||
|
||||
try:
|
||||
if api_settings.PASSWORDLESS_MOBILE_NOREPLY_NUMBER:
|
||||
print("Sending SMS")
|
||||
# We need a sending number to send properly
|
||||
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
|
||||
# we assume success to prevent spamming SMS during testing.
|
||||
return True
|
||||
to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
|
||||
if to_number.__class__.__name__ == 'PhoneNumber':
|
||||
to_number = to_number.__str__()
|
||||
print("Sending SMS")
|
||||
# We need a sending number to send properly
|
||||
if api_settings.PASSWORDLESS_TEST_SUPPRESSION is True:
|
||||
# we assume success to prevent spamming SMS during testing.
|
||||
return True
|
||||
to_number = getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
|
||||
if to_number.__class__.__name__ == "PhoneNumber":
|
||||
to_number = to_number.__str__()
|
||||
|
||||
# user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists()
|
||||
# if not user_withh_mobile_exists:
|
||||
# print("User with mobile number does not exist.")
|
||||
# logger.debug("User with mobile number does not exist.")
|
||||
# return False
|
||||
# user_withh_mobile_exists = User.objects.filter(mobile=to_number).exists()
|
||||
# if not user_withh_mobile_exists:
|
||||
# print("User with mobile number does not exist.")
|
||||
# logger.debug("User with mobile number does not exist.")
|
||||
# return False
|
||||
|
||||
|
||||
api_url = config("SMS_API_URL")
|
||||
api_key = config("SMS_API_KEY")
|
||||
if not api_url or not api_key:
|
||||
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
|
||||
return False
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
data = {
|
||||
"number": to_number,
|
||||
"message": base_string % mobile_token.key,
|
||||
"check_delivery": False
|
||||
}
|
||||
|
||||
response = requests.post(api_url, headers=headers, data=json.dumps(data))
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Failed to send SMS. Status code: {response.status_code}")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Failed to send token sms. Missing PASSWORDLESS_MOBILE_NOREPLY_NUMBER.")
|
||||
api_url = config("SMS_API_URL")
|
||||
api_key = config("SMS_API_KEY")
|
||||
if not api_url or not api_key:
|
||||
logger.debug("Failed to send SMS. Missing SMS_API_URL or SMS_API_KEY.")
|
||||
return False
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
data = {
|
||||
"number": to_number,
|
||||
"message": base_string % mobile_token.key,
|
||||
"check_delivery": False,
|
||||
}
|
||||
|
||||
response = requests.post(api_url, headers=headers, data=json.dumps(data))
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"Failed to send SMS. Status code: {response.status_code}")
|
||||
return False
|
||||
|
||||
except ImportError:
|
||||
logger.debug("Couldn't import Twilio client. Is twilio installed?")
|
||||
return False
|
||||
except KeyError:
|
||||
logger.debug("Couldn't send SMS."
|
||||
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?")
|
||||
logger.debug(
|
||||
"Couldn't send SMS."
|
||||
"Did you set your Twilio account tokens and specify a PASSWORDLESS_MOBILE_NOREPLY_NUMBER?"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug("Failed to send token SMS to user: {}. "
|
||||
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
|
||||
"Number entered was {}".format(user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)))
|
||||
logger.debug(
|
||||
"Failed to send token SMS to user: {}. "
|
||||
"Possibly no mobile number on user object or the twilio package isn't set up yet. "
|
||||
"Number entered was {}".format(
|
||||
user.id, getattr(user, api_settings.PASSWORDLESS_USER_MOBILE_FIELD_NAME)
|
||||
)
|
||||
)
|
||||
logger.debug(e)
|
||||
return False
|
||||
|
Loading…
x
Reference in New Issue
Block a user