Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c56140011f | ||
|
|
4714b6ec15 | ||
|
|
6ae56774d1 | ||
|
|
bfc3fd1b89 | ||
|
|
9721585f8a | ||
|
|
b0936cd489 | ||
|
|
fc1aba3239 | ||
|
|
85485ae351 | ||
|
|
64bba25fb9 | ||
|
|
fbc8a17e6a | ||
|
|
36160c2665 | ||
|
|
f6afb3b658 | ||
|
|
9c082aedf2 | ||
|
|
2bc594da9c | ||
|
|
19321da0be | ||
|
|
e3c2d4450f | ||
|
|
ee54386fd5 | ||
|
|
b52cd9285a | ||
|
|
d0c809489c
|
||
|
|
80fc27fd74 | ||
|
|
cdef5ed27c | ||
|
|
3e7a74950e | ||
|
|
72e0cd1fba | ||
|
|
a4b6f44348 | ||
|
|
4aae0064ca | ||
|
|
a46f2635ad
|
||
|
|
118ad52c71 | ||
|
|
8d9a2ed2e0 | ||
|
|
3200d8e41c | ||
|
|
1554829b9a | ||
|
|
f8c91e8f14 | ||
|
|
f84f03fd5b | ||
|
|
fd603daaaf | ||
|
|
9e4449d0d6 | ||
|
|
087782e351 | ||
|
|
446ca6653e | ||
|
|
f8c0725558 | ||
|
|
ea57598e8d | ||
|
|
4cc6e91a66 |
@@ -9,6 +9,10 @@ server {
|
|||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/www/html/staticfiles/;
|
alias /var/www/html/staticfiles/;
|
||||||
}
|
}
|
||||||
|
location /media/ {
|
||||||
|
alias /var/www/html/media/;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Forward requests to Gunicorn
|
# Forward requests to Gunicorn
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
@@ -163,3 +163,4 @@ cython_debug/
|
|||||||
#staticfiles
|
#staticfiles
|
||||||
staticfiles/
|
staticfiles/
|
||||||
postgres_data/
|
postgres_data/
|
||||||
|
media/
|
||||||
@@ -47,6 +47,7 @@ class UserAdmin(BaseUserAdmin):
|
|||||||
"island",
|
"island",
|
||||||
"terms_accepted",
|
"terms_accepted",
|
||||||
"policy_accepted",
|
"policy_accepted",
|
||||||
|
"agreement",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
+69
@@ -0,0 +1,69 @@
|
|||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from telegram import Bot
|
||||||
|
from telegram.constants import ParseMode
|
||||||
|
from decouple import config
|
||||||
|
import re
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
telegram_loop = None
|
||||||
|
|
||||||
|
BOT_TOKEN = config("TG_BOT_TOKEN", default="killme", cast=str)
|
||||||
|
CHAT_ID = config("TG_CHAT_ID", default="drake", cast=str)
|
||||||
|
|
||||||
|
if not BOT_TOKEN or not isinstance(BOT_TOKEN, str):
|
||||||
|
raise ValueError(
|
||||||
|
"TG_BOT_TOKEN environment variable must be set and must be a string."
|
||||||
|
)
|
||||||
|
if not CHAT_ID:
|
||||||
|
raise ValueError(
|
||||||
|
"TG_CHAT_ID environment variable must be set and must be a string."
|
||||||
|
)
|
||||||
|
|
||||||
|
bot = Bot(token=BOT_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
def telegram_worker():
|
||||||
|
"""
|
||||||
|
Run the event loop for Telegram in a separate daemon thread.
|
||||||
|
"""
|
||||||
|
global telegram_loop
|
||||||
|
telegram_loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(telegram_loop)
|
||||||
|
try:
|
||||||
|
logger.info("Telegram loop started.")
|
||||||
|
telegram_loop.run_forever()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Telegram worker crashed! {e}", exc_info=True)
|
||||||
|
finally:
|
||||||
|
telegram_loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Start the Telegram worker thread when the module is loaded
|
||||||
|
telegram_thread = threading.Thread(target=telegram_worker, daemon=True)
|
||||||
|
telegram_thread.start()
|
||||||
|
|
||||||
|
# Wait until telegram_loop is ready
|
||||||
|
timeout = 5
|
||||||
|
for _ in range(timeout * 10): # up to 5 seconds
|
||||||
|
if telegram_loop is not None:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
logger.error("Telegram loop failed to initialize in time.")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_telegram_alert(markdown_message: str):
|
||||||
|
logger.info("[TELEGRAM] Preparing to send alert...")
|
||||||
|
await bot.send_message(
|
||||||
|
chat_id=str(CHAT_ID),
|
||||||
|
text=markdown_message,
|
||||||
|
parse_mode=ParseMode.MARKDOWN_V2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def escape_markdown_v2(text: str) -> str:
|
||||||
|
escape_chars = r"_~`>#+-=|{}.!\\"
|
||||||
|
return re.sub(f"([{re.escape(escape_chars)}])", r"\\\1", text)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from rest_framework.response import Response
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import date
|
||||||
|
from django.utils import timezone
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
|
||||||
|
MOBILE_PATTERN = r"^[7|9][0-9]{6}$"
|
||||||
|
ACCOUNT_NUMBER_PATTERN = r"^(7\d{12}|9\d{16})$"
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorMessages:
|
||||||
|
USERNAME_EXISTS = "Username already exists."
|
||||||
|
MOBILE_EXISTS = "Mobile number already exists."
|
||||||
|
INVALID_ID_CARD = "Please enter a valid ID card number."
|
||||||
|
ID_CARD_EXISTS = "ID card already exists."
|
||||||
|
INVALID_MOBILE = "Please enter a valid mobile number."
|
||||||
|
INVALID_ACCOUNT = "Please enter a valid account number."
|
||||||
|
UNDERAGE_ERROR = "You must be 18 and above to signup."
|
||||||
|
|
||||||
|
|
||||||
|
def validate_required_fields(data) -> Optional[Response]:
|
||||||
|
required_fields = {
|
||||||
|
"firstname": "First name",
|
||||||
|
"lastname": "Last name",
|
||||||
|
"username": "Username",
|
||||||
|
"address": "Address",
|
||||||
|
"mobile": "Mobile number",
|
||||||
|
"acc_no": "Account number",
|
||||||
|
"id_card": "ID card",
|
||||||
|
"dob": "Date of birth",
|
||||||
|
"atoll": "Atoll",
|
||||||
|
"island": "Island",
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, label in required_fields.items():
|
||||||
|
if not data.get(field):
|
||||||
|
return Response({"message": f"{label} is required."}, status=400)
|
||||||
|
|
||||||
|
if data.get("terms_accepted") is None:
|
||||||
|
return Response({"message": "Terms acceptance is required."}, status=400)
|
||||||
|
if data.get("policy_accepted") is None:
|
||||||
|
return Response({"message": "Policy acceptance is required."}, status=400)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
from .models import TemporaryUser, User
|
||||||
|
|
||||||
|
def validate_unique_fields(username, mobile, id_card) -> Optional[Response]:
|
||||||
|
if mobile and (TemporaryUser.objects.filter(t_mobile=mobile).exists() or User.objects.filter(mobile=mobile).exists()):
|
||||||
|
return Response({"message": ErrorMessages.MOBILE_EXISTS}, status=400)
|
||||||
|
|
||||||
|
if username and (TemporaryUser.objects.filter(t_username=username).exists() or User.objects.filter(username=username).exists()):
|
||||||
|
return Response({"message": ErrorMessages.USERNAME_EXISTS}, status=400)
|
||||||
|
|
||||||
|
if id_card and (TemporaryUser.objects.filter(t_id_card=id_card).exists() or User.objects.filter(id_card=id_card).exists()):
|
||||||
|
return Response({"message": ErrorMessages.ID_CARD_EXISTS}, status=400)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def validate_patterns(id_card, mobile, acc_no) -> Optional[Response]:
|
||||||
|
if id_card and not re.match(ID_CARD_PATTERN, id_card):
|
||||||
|
return Response({"message": ErrorMessages.INVALID_ID_CARD}, status=400)
|
||||||
|
|
||||||
|
if mobile is None or not re.match(MOBILE_PATTERN, mobile):
|
||||||
|
return Response({"message": ErrorMessages.INVALID_MOBILE}, status=400)
|
||||||
|
|
||||||
|
if acc_no is None or not re.match(ACCOUNT_NUMBER_PATTERN, acc_no):
|
||||||
|
return Response({"message": ErrorMessages.INVALID_ACCOUNT}, status=400)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_age(dob: date) -> int:
|
||||||
|
today = timezone.now().date()
|
||||||
|
return today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-24 18:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("api", "0017_alter_temporaryuser_t_id_card_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="agreement",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Upload the agreement file signed by the user.",
|
||||||
|
null=True,
|
||||||
|
upload_to="agreements/",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
+34
-1
@@ -8,6 +8,7 @@ from django.db import models
|
|||||||
from .managers import CustomUserManager
|
from .managers import CustomUserManager
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
import pyotp
|
import pyotp
|
||||||
|
from billing.models import WalletTransaction
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
@@ -34,12 +35,44 @@ class User(AbstractUser):
|
|||||||
island = models.ForeignKey(
|
island = models.ForeignKey(
|
||||||
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
"Island", on_delete=models.SET_NULL, null=True, blank=True, related_name="users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
agreement = models.FileField(
|
||||||
|
upload_to="agreements/",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text="Upload the agreement file signed by the user.",
|
||||||
|
)
|
||||||
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)
|
||||||
|
|
||||||
def get_all_fields(self, instance):
|
def get_all_fields(self, instance):
|
||||||
return [field.name for field in instance.get_fields()]
|
return [field.name for field in instance.get_fields()]
|
||||||
|
|
||||||
|
def add_wallet_funds(self, amount, description="", reference_id=None):
|
||||||
|
self.wallet_balance += amount
|
||||||
|
self.save(update_fields=["wallet_balance"])
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=amount,
|
||||||
|
transaction_type="TOPUP",
|
||||||
|
description=description,
|
||||||
|
reference_id=reference_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def deduct_wallet_funds(self, amount, description="", reference_id=None):
|
||||||
|
if self.wallet_balance >= amount:
|
||||||
|
self.wallet_balance -= amount
|
||||||
|
self.save(update_fields=["wallet_balance"])
|
||||||
|
WalletTransaction.objects.create(
|
||||||
|
user=self,
|
||||||
|
amount=amount,
|
||||||
|
transaction_type="DEBIT",
|
||||||
|
description=description,
|
||||||
|
reference_id=reference_id,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
objects = CustomUserManager()
|
objects = CustomUserManager()
|
||||||
|
|
||||||
|
|
||||||
@@ -103,7 +136,7 @@ class TemporaryUser(models.Model):
|
|||||||
verbose_name_plural = "Temporary Users"
|
verbose_name_plural = "Temporary Users"
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return str(self.t_username)
|
return f"{self.t_username}"
|
||||||
|
|
||||||
|
|
||||||
class Atoll(models.Model):
|
class Atoll(models.Model):
|
||||||
|
|||||||
+12
-1
@@ -48,6 +48,15 @@ class UserUpdateSerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgreementSerializer(serializers.ModelSerializer):
|
||||||
|
"""serializer for the user agreement object"""
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = User
|
||||||
|
fields = ("agreement",)
|
||||||
|
extra_kwargs = {"agreement": {"required": True, "allow_null": False}}
|
||||||
|
|
||||||
|
|
||||||
class CustomUserSerializer(serializers.ModelSerializer):
|
class CustomUserSerializer(serializers.ModelSerializer):
|
||||||
"""serializer for the user object"""
|
"""serializer for the user object"""
|
||||||
|
|
||||||
@@ -109,6 +118,8 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
|
|||||||
"address",
|
"address",
|
||||||
"acc_no",
|
"acc_no",
|
||||||
"id_card",
|
"id_card",
|
||||||
|
"agreement",
|
||||||
|
"wallet_balance",
|
||||||
)
|
)
|
||||||
depth = 1
|
depth = 1
|
||||||
|
|
||||||
@@ -148,7 +159,7 @@ class UserSerializer(serializers.ModelSerializer):
|
|||||||
extra_kwargs = {"password": {"write_only": True, "min_length": 5}}
|
extra_kwargs = {"password": {"write_only": True, "min_length": 5}}
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
return User.objects.create_user(**validated_data)
|
return User.objects.create_user(**validated_data) #type: ignore
|
||||||
|
|
||||||
|
|
||||||
class AuthSerializer(serializers.Serializer):
|
class AuthSerializer(serializers.Serializer):
|
||||||
|
|||||||
+13
-4
@@ -4,7 +4,7 @@ from django.template.loader import render_to_string
|
|||||||
from decouple import config
|
from decouple import config
|
||||||
from django_rest_passwordreset.signals import reset_password_token_created
|
from django_rest_passwordreset.signals import reset_password_token_created
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from api.models import User
|
from api.models import User, TemporaryUser
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from api.tasks import verify_user_with_person_api_task
|
from api.tasks import verify_user_with_person_api_task
|
||||||
|
|
||||||
@@ -21,7 +21,13 @@ def assign_device_permissions(sender, instance, created, **kwargs):
|
|||||||
topup_permissions = Permission.objects.filter(
|
topup_permissions = Permission.objects.filter(
|
||||||
content_type__model="topup"
|
content_type__model="topup"
|
||||||
).exclude(codename__startswith="delete_")
|
).exclude(codename__startswith="delete_")
|
||||||
|
wallet_transaction_permissions = Permission.objects.filter(
|
||||||
|
content_type__model="wallettransaction"
|
||||||
|
).exclude(codename__startswith="delete_")
|
||||||
|
user_read_only_permission = Permission.objects.get(
|
||||||
|
codename="view_user", content_type__model="user"
|
||||||
|
)
|
||||||
|
instance.user_permissions.add(user_read_only_permission)
|
||||||
for permission in topup_permissions:
|
for permission in topup_permissions:
|
||||||
instance.user_permissions.add(permission)
|
instance.user_permissions.add(permission)
|
||||||
for permission in device_permissions:
|
for permission in device_permissions:
|
||||||
@@ -29,12 +35,15 @@ def assign_device_permissions(sender, instance, created, **kwargs):
|
|||||||
instance.user_permissions.add(atoll_read_permission, island_read_permission)
|
instance.user_permissions.add(atoll_read_permission, island_read_permission)
|
||||||
for permission in payment_permissions:
|
for permission in payment_permissions:
|
||||||
instance.user_permissions.add(permission)
|
instance.user_permissions.add(permission)
|
||||||
|
for permission in wallet_transaction_permissions:
|
||||||
|
instance.user_permissions.add(permission)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
@receiver(post_save, sender=TemporaryUser)
|
||||||
def verify_user_with_person_api(sender, instance, created, **kwargs):
|
def verify_user_with_person_api(sender, instance, created, **kwargs):
|
||||||
if created:
|
if created:
|
||||||
verify_user_with_person_api_task(instance.id)
|
print(f"Temporary User Instance: {instance}")
|
||||||
|
verify_user_with_person_api_task(instance.t_id)
|
||||||
|
|
||||||
|
|
||||||
@receiver(reset_password_token_created)
|
@receiver(reset_password_token_created)
|
||||||
|
|||||||
+122
-62
@@ -1,5 +1,6 @@
|
|||||||
|
# pyright: reportGeneralTypeIssues=false
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from api.models import User
|
from api.models import TemporaryUser
|
||||||
from devices.models import Device
|
from devices.models import Device
|
||||||
from api.notifications import send_sms
|
from api.notifications import send_sms
|
||||||
import os
|
import os
|
||||||
@@ -8,9 +9,13 @@ from django.utils import timezone
|
|||||||
|
|
||||||
# from api.notifications import send_clean_telegram_markdown
|
# from api.notifications import send_clean_telegram_markdown
|
||||||
from api.omada import Omada
|
from api.omada import Omada
|
||||||
|
from api.bot import send_telegram_alert, telegram_loop, escape_markdown_v2
|
||||||
|
import asyncio
|
||||||
from apibase.env import env, BASE_DIR
|
from apibase.env import env, BASE_DIR
|
||||||
from procrastinate.contrib.django import app
|
from procrastinate.contrib.django import app
|
||||||
from procrastinate import builtin_tasks
|
from procrastinate import builtin_tasks
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -36,22 +41,64 @@ async def remove_old_jobs(context, timestamp):
|
|||||||
|
|
||||||
|
|
||||||
@app.periodic(
|
@app.periodic(
|
||||||
cron="0 0 */28 * *", queue="heavy_tasks", periodic_id="deactivate_expired_devices"
|
cron="0 22 * * *",
|
||||||
|
queue="heavy_tasks",
|
||||||
|
periodic_id="deactivate_expired_devices_and_block_in_omada",
|
||||||
) # type: ignore
|
) # type: ignore
|
||||||
@app.task
|
@app.task
|
||||||
def deactivate_expired_devices():
|
def deactivate_expired_devices_and_block_in_omada():
|
||||||
expired_devices = Device.objects.filter(
|
expired_devices = Device.objects.filter(
|
||||||
expiry_date__lte=timezone.localtime(timezone.now()), is_active=True
|
expiry_date__lte=timezone.localtime(timezone.now()), is_active=True
|
||||||
).select_related("user")
|
).select_related("user")
|
||||||
|
|
||||||
print("Expired Devices: ", expired_devices)
|
print("Expired Devices: ", expired_devices)
|
||||||
count = expired_devices.count()
|
count = expired_devices.count()
|
||||||
|
|
||||||
|
if count == 0:
|
||||||
|
return {"total_expired_devices": 0}
|
||||||
|
|
||||||
user_devices_map = {}
|
user_devices_map = {}
|
||||||
|
devices_successfully_blocked = []
|
||||||
|
devices_failed_to_block = []
|
||||||
|
omada_client = Omada()
|
||||||
|
|
||||||
|
# Single loop to collect data and block devices
|
||||||
for device in expired_devices:
|
for device in expired_devices:
|
||||||
|
# Collect devices for SMS notifications
|
||||||
if device.user and device.user.mobile:
|
if device.user and device.user.mobile:
|
||||||
if device.user.mobile not in user_devices_map:
|
if device.user.mobile not in user_devices_map:
|
||||||
user_devices_map[device.user.mobile] = []
|
user_devices_map[device.user.mobile] = []
|
||||||
user_devices_map[device.user.mobile].append(device.name)
|
user_devices_map[device.user.mobile].append(device.name)
|
||||||
|
|
||||||
|
# Try to block device in Omada
|
||||||
|
try:
|
||||||
|
omada_client.block_device(mac_address=device.mac, operation="block")
|
||||||
|
# Only prepare for update if Omada blocking succeeded
|
||||||
|
device.blocked = True
|
||||||
|
device.is_active = False
|
||||||
|
devices_successfully_blocked.append(device)
|
||||||
|
logger.info(f"Successfully blocked device {device.mac} in Omada")
|
||||||
|
time.sleep(20) # Sleep to avoid rate limiting
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to block device [omada] {device.mac}: {e}")
|
||||||
|
devices_failed_to_block.append(device)
|
||||||
|
# Continue to next device without updating this one
|
||||||
|
|
||||||
|
# Bulk update only successfully blocked devices
|
||||||
|
if devices_successfully_blocked:
|
||||||
|
try:
|
||||||
|
Device.objects.bulk_update(
|
||||||
|
devices_successfully_blocked, ["is_active", "blocked"]
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Successfully updated {len(devices_successfully_blocked)} devices in database"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to bulk update devices in database: {e}")
|
||||||
|
# You might want to handle this case - devices are blocked in Omada but not updated in DB
|
||||||
|
|
||||||
|
# Send SMS notifications
|
||||||
|
sms_count = 0
|
||||||
for mobile, device_names in user_devices_map.items():
|
for mobile, device_names in user_devices_map.items():
|
||||||
if not mobile:
|
if not mobile:
|
||||||
continue
|
continue
|
||||||
@@ -59,14 +106,25 @@ def deactivate_expired_devices():
|
|||||||
[f"{i + 1}. {name}" for i, name in enumerate(device_names)]
|
[f"{i + 1}. {name}" for i, name in enumerate(device_names)]
|
||||||
)
|
)
|
||||||
print("device list: ", device_list)
|
print("device list: ", device_list)
|
||||||
send_sms(
|
try:
|
||||||
mobile,
|
send_sms(
|
||||||
f"Dear {mobile}, \n\nThe following devices have expired: \n{device_list}. \n\nPlease make a payment to keep your devices active. \n\n- SAR Link",
|
mobile,
|
||||||
)
|
f"Dear {mobile}, \n\nThe following devices have expired: \n{device_list}. \n\nPlease make a payment to keep your devices active. \n\n- SAR Link",
|
||||||
# expired_devices.update(is_active=False)
|
)
|
||||||
print(f"Total {count} expired devices.")
|
sms_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to send SMS to {mobile}: {e}")
|
||||||
|
|
||||||
|
print(f"Total {count} expired devices processed.")
|
||||||
|
print(f"Successfully blocked: {len(devices_successfully_blocked)}")
|
||||||
|
print(f"Failed to block: {len(devices_failed_to_block)}")
|
||||||
|
print(f"SMS notifications sent: {sms_count}")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_expired_devices": count,
|
"total_expired_devices": count,
|
||||||
|
"successfully_blocked": len(devices_successfully_blocked),
|
||||||
|
"failed_to_block": len(devices_failed_to_block),
|
||||||
|
"sms_sent": sms_count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -86,36 +144,31 @@ def verify_user_with_person_api_task(user_id: int):
|
|||||||
Verify the user with the Person API.
|
Verify the user with the Person API.
|
||||||
:param user_id: The ID of the user to verify.
|
:param user_id: The ID of the user to verify.
|
||||||
"""
|
"""
|
||||||
if not user_id:
|
|
||||||
logger.error("User ID is not provided.")
|
|
||||||
return None
|
|
||||||
user = get_object_or_404(User, id=user_id)
|
|
||||||
if not user:
|
|
||||||
logger.error(f"User with ID {user_id} not found.")
|
|
||||||
return None
|
|
||||||
# Call the Person API to verify the user
|
|
||||||
|
|
||||||
# verification_failed_message = f"""
|
|
||||||
# _The following user verification failed_:
|
|
||||||
# *ID Card:* {user.id_card}
|
|
||||||
# *Name:* {user.first_name} {user.last_name}
|
|
||||||
# *House Name:* {user.address}
|
|
||||||
# *Date of Birth:* {user.dob}
|
|
||||||
# *Island:* {(user.atoll.name if user.atoll else "N/A")} {(user.island.name if user.island else "N/A")}
|
|
||||||
# *Mobile:* {user.mobile}
|
|
||||||
# Visit [SAR Link Portal](https://portal.sarlink.net) to manually verify this user.
|
|
||||||
# """
|
|
||||||
|
|
||||||
# logger.info(verification_failed_message)
|
|
||||||
PERSON_VERIFY_BASE_URL = env.str("PERSON_VERIFY_BASE_URL", default="") # type: ignore
|
PERSON_VERIFY_BASE_URL = env.str("PERSON_VERIFY_BASE_URL", default="") # type: ignore
|
||||||
|
|
||||||
if not PERSON_VERIFY_BASE_URL:
|
if not PERSON_VERIFY_BASE_URL:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"PERSON_VERIFY_BASE_URL is not set in the environment variables."
|
"PERSON_VERIFY_BASE_URL is not set in the environment variables."
|
||||||
)
|
)
|
||||||
import requests
|
|
||||||
|
|
||||||
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{user.id_card}")
|
print(f"Verifying user with ID: {user_id}")
|
||||||
|
if not user_id:
|
||||||
|
logger.error("User ID is not provided.")
|
||||||
|
return None
|
||||||
|
t_user = get_object_or_404(TemporaryUser, t_id=user_id)
|
||||||
|
if not t_user:
|
||||||
|
logger.error(f"User with ID {user_id} not found.")
|
||||||
|
return None
|
||||||
|
print("t_user:", t_user)
|
||||||
|
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{t_user.t_id_card}")
|
||||||
|
|
||||||
|
|
||||||
|
verification_failed_message = f"""*The following user verification failed*:\n\n*ID Card:* {t_user.t_id_card}\n*Name:* {t_user.t_first_name} {t_user.t_last_name}\n*House Name:* {t_user.t_address}\n*Date of Birth:* {t_user.t_dob}\n*Island:* {(t_user.t_atoll.name if t_user.t_atoll else "N/A")} {(t_user.t_island.name if t_user.t_island else "N/A")}\n*Mobile:* {t_user.t_mobile}\nVisit [SAR Link Portal](https://portal.sarlink.net/users/{user_id}/details) to manually verify this user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(verification_failed_message)
|
||||||
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
api_nic = data.get("nic")
|
api_nic = data.get("nic")
|
||||||
@@ -125,10 +178,10 @@ def verify_user_with_person_api_task(user_id: int):
|
|||||||
api_atoll = data.get("atoll_en")
|
api_atoll = data.get("atoll_en")
|
||||||
api_island_name = data.get("island_name_en")
|
api_island_name = data.get("island_name_en")
|
||||||
|
|
||||||
if not user.mobile or user.dob is None:
|
if not t_user.t_mobile or t_user.t_dob is None:
|
||||||
logger.error("User mobile or date of birth is not set.")
|
logger.error("User mobile or date of birth is not set.")
|
||||||
return None
|
return None
|
||||||
if not user.island or user.atoll is None:
|
if not t_user.t_island or t_user.t_atoll is None:
|
||||||
logger.error("User island or atoll is not set.")
|
logger.error("User island or atoll is not set.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -139,53 +192,60 @@ def verify_user_with_person_api_task(user_id: int):
|
|||||||
logger.info(f"API atoll: {api_atoll}")
|
logger.info(f"API atoll: {api_atoll}")
|
||||||
logger.info(f"API island name: {api_island_name}")
|
logger.info(f"API island name: {api_island_name}")
|
||||||
|
|
||||||
user_nic = user.id_card
|
user_nic = t_user.t_id_card
|
||||||
user_name = f"{user.first_name} {user.last_name}"
|
user_name = f"{t_user.t_first_name} {t_user.t_last_name}"
|
||||||
user_house_name = user.address
|
user_house_name = t_user.t_address
|
||||||
user_dob = user.dob.isoformat()
|
user_dob = t_user.t_dob.isoformat()
|
||||||
|
|
||||||
logger.info(f"User nic: {user_nic}")
|
logger.info(f"User nic: {user_nic}")
|
||||||
logger.info(f"User name: {user_name}")
|
logger.info(f"User name: {user_name}")
|
||||||
logger.info(f"User house name: {user_house_name}")
|
logger.info(f"User house name: {user_house_name}")
|
||||||
logger.info(f"User dob: {user_dob}")
|
logger.info(f"User dob: {user_dob}")
|
||||||
logger.info(f"User atoll: {user.atoll}")
|
logger.info(f"User atoll: {t_user.t_atoll.name if t_user.t_atoll else 'N/A'}")
|
||||||
logger.info(f"User island name: {user.island}")
|
logger.info(
|
||||||
|
f"User island name: {t_user.t_island.name if t_user.t_island else 'N/A'}"
|
||||||
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"case User atoll: {user.atoll.name == api_atoll.strip() if api_atoll else False}"
|
f"case User atoll: {t_user.t_atoll.name == api_atoll.strip() if api_atoll else False}"
|
||||||
) # Defensive check for api_atoll
|
) # Defensive check for api_atoll
|
||||||
logger.info(f"api atoll type: {type(api_atoll)}")
|
logger.info(f"api atoll type: {type(api_atoll)}")
|
||||||
logger.info(f"user atoll type: {type(user.atoll.name)}")
|
logger.info(f"user atoll type: {type(t_user.t_atoll.name)}")
|
||||||
logger.info(
|
logger.info(
|
||||||
f"case User island name: {user.island.name == api_island_name.strip() if api_island_name else False}"
|
f"case User island name: {t_user.t_island.name == api_island_name.strip() if api_island_name else False}"
|
||||||
) # Defensive check for api_island_name
|
) # Defensive check for api_island_name
|
||||||
logger.info(f"api island name type: {type(api_island_name)}")
|
logger.info(f"api island name type: {type(api_island_name)}")
|
||||||
logger.info(f"user island name type: {type(user.island.name)}")
|
logger.info(f"user island name type: {type(t_user.t_island.name)}")
|
||||||
|
|
||||||
|
|
||||||
|
print("CHECKING USER FIELDS AGAINST API DATA")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
data.get("nic") == user.id_card
|
data.get("nic") == t_user.t_id_card
|
||||||
and data.get("name_en") == f"{user.first_name} {user.last_name}"
|
and data.get("name_en") == f"{t_user.t_first_name} {t_user.t_last_name}"
|
||||||
and data.get("house_name_en") == user.address
|
and data.get("house_name_en") == t_user.t_address
|
||||||
and data.get("dob").split("T")[0] == user.dob.isoformat()
|
and data.get("dob").split("T")[0] == t_user.t_dob.isoformat()
|
||||||
and data.get("atoll_en").strip() == user.atoll.name
|
and data.get("atoll_en").strip() == t_user.t_atoll.name
|
||||||
and data.get("island_name_en").strip() == user.island.name
|
and data.get("island_name_en").strip() == t_user.t_island.name
|
||||||
):
|
):
|
||||||
user.verified = True
|
t_user.t_verified = True
|
||||||
user.save()
|
t_user.save()
|
||||||
send_sms(
|
|
||||||
user.mobile,
|
|
||||||
f"Dear {user.first_name} {user.last_name}, \n\nYour account has been successfully and verified. \n\nYou can now manage your devices and make payments through our portal at https://portal.sarlink.net. \n\n - SAR Link",
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
user.verified = False
|
t_user.t_verified = False
|
||||||
user.save()
|
t_user.save()
|
||||||
|
|
||||||
send_sms(
|
|
||||||
user.mobile,
|
|
||||||
f"Dear {user.first_name} {user.last_name}, \n\nYour account registration is being processed. \n\nWe will notify you once verification is complete. \n\n - SAR Link",
|
|
||||||
)
|
|
||||||
# send_clean_telegram_markdown(message=verification_failed_message)
|
# send_clean_telegram_markdown(message=verification_failed_message)
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
send_telegram_alert(
|
||||||
|
markdown_message=escape_markdown_v2(verification_failed_message)
|
||||||
|
),
|
||||||
|
telegram_loop,
|
||||||
|
).result()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("[Registration] TELEGRAM ALERT ERROR", e)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# Handle the error case
|
# Handle the error case
|
||||||
|
|||||||
+6
-4
@@ -18,11 +18,11 @@ from .views import (
|
|||||||
RetrieveUpdateDestroyIslandView,
|
RetrieveUpdateDestroyIslandView,
|
||||||
filter_user,
|
filter_user,
|
||||||
filter_temporary_user,
|
filter_temporary_user,
|
||||||
UpdateUserWalletView,
|
|
||||||
VerifyOTPView,
|
VerifyOTPView,
|
||||||
UserVerifyAPIView,
|
UserVerifyAPIView,
|
||||||
UserUpdateAPIView,
|
UserUpdateAPIView,
|
||||||
UserRejectAPIView,
|
UserRejectAPIView,
|
||||||
|
AgreementUpdateAPIView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -36,15 +36,17 @@ urlpatterns = [
|
|||||||
path("tokens/", KnoxTokenListApiView.as_view(), name="knox_tokens"),
|
path("tokens/", KnoxTokenListApiView.as_view(), name="knox_tokens"),
|
||||||
# path("auth/", CustomAuthToken.as_view()),
|
# path("auth/", CustomAuthToken.as_view()),
|
||||||
path("users/", ListUserView.as_view(), name="users"),
|
path("users/", ListUserView.as_view(), name="users"),
|
||||||
path(
|
|
||||||
"update-wallet/<int:pk>/", UpdateUserWalletView.as_view(), name="update-wallet"
|
|
||||||
),
|
|
||||||
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
path("users/<int:pk>/", UserDetailAPIView.as_view(), name="user-detail"),
|
||||||
path("users/<int:pk>/update/", UserUpdateAPIView.as_view(), name="user-update"),
|
path("users/<int:pk>/update/", UserUpdateAPIView.as_view(), name="user-update"),
|
||||||
path("users/filter/", filter_user, name="filter-users"),
|
path("users/filter/", filter_user, name="filter-users"),
|
||||||
path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"),
|
path("users/temp/filter/", filter_temporary_user, name="filter-temporary-users"),
|
||||||
# User verification flow
|
# User verification flow
|
||||||
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
path("users/<int:pk>/verify/", UserVerifyAPIView.as_view(), name="user-verify"),
|
||||||
|
path(
|
||||||
|
"users/<int:pk>/agreement/",
|
||||||
|
AgreementUpdateAPIView.as_view(),
|
||||||
|
name="user-agreement-update",
|
||||||
|
),
|
||||||
path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"),
|
path("users/<int:pk>/reject/", UserRejectAPIView.as_view(), name="user-reject"),
|
||||||
path("healthcheck/", healthcheck, name="healthcheck"),
|
path("healthcheck/", healthcheck, name="healthcheck"),
|
||||||
path("test/", test_email, name="testemail"),
|
path("test/", test_email, name="testemail"),
|
||||||
|
|||||||
+157
-166
@@ -2,6 +2,7 @@
|
|||||||
from django.contrib.auth import login
|
from django.contrib.auth import login
|
||||||
|
|
||||||
# rest_framework imports
|
# rest_framework imports
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
from rest_framework.authtoken.serializers import AuthTokenSerializer
|
||||||
from api.filters import UserFilter
|
from api.filters import UserFilter
|
||||||
@@ -15,25 +16,27 @@ from rest_framework.decorators import api_view, permission_classes
|
|||||||
from api.serializers import (
|
from api.serializers import (
|
||||||
AtollSerializer,
|
AtollSerializer,
|
||||||
IslandSerializer,
|
IslandSerializer,
|
||||||
CustomUserByWalletBalanceSerializer,
|
|
||||||
OTPVerificationSerializer,
|
OTPVerificationSerializer,
|
||||||
TemporaryUserSerializer,
|
TemporaryUserSerializer,
|
||||||
UserUpdateSerializer,
|
UserUpdateSerializer,
|
||||||
|
UserAgreementSerializer,
|
||||||
)
|
)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
# knox imports
|
# knox imports
|
||||||
from knox.views import LoginView as KnoxLoginView
|
from knox.views import LoginView as KnoxLoginView
|
||||||
from knox.models import AuthToken
|
from knox.models import AuthToken
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
import re
|
|
||||||
from typing import cast, Dict, Any
|
from typing import cast, Dict, Any
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from api.notifications import send_otp
|
from api.notifications import send_otp
|
||||||
from .utils import check_person_api_verification
|
from .utils import check_person_api_verification
|
||||||
|
import uuid
|
||||||
|
from .helpers import ErrorMessages, validate_required_fields, validate_unique_fields, validate_patterns, calculate_age
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# local apps import
|
# local apps import
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
@@ -41,23 +44,9 @@ from .serializers import (
|
|||||||
AuthSerializer,
|
AuthSerializer,
|
||||||
CustomUserSerializer,
|
CustomUserSerializer,
|
||||||
CustomReadOnlyUserSerializer,
|
CustomReadOnlyUserSerializer,
|
||||||
CustomReadOnlyUserByIDCardSerializer,
|
|
||||||
UserProfileUpdateSerializer,
|
UserProfileUpdateSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
|
|
||||||
MOBILE_PATTERN = r"^[7|9][0-9]{6}$"
|
|
||||||
ACCOUNT_NUMBER_PATTERN = r"^(7\d{12}|9\d{16})$"
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorMessages:
|
|
||||||
USERNAME_EXISTS = "Username already exists."
|
|
||||||
MOBILE_EXISTS = "Mobile number already exists."
|
|
||||||
INVALID_ID_CARD = "Please enter a valid ID card number."
|
|
||||||
ID_CARD_EXISTS = "ID card already exists."
|
|
||||||
INVALID_MOBILE = "Please enter a valid mobile number."
|
|
||||||
INVALID_ACCOUNT = "Please enter a valid account number."
|
|
||||||
UNDERAGE_ERROR = "You must be 18 and above to signup."
|
|
||||||
|
|
||||||
|
|
||||||
@api_view(["GET"])
|
@api_view(["GET"])
|
||||||
@@ -65,170 +54,94 @@ def healthcheck(request):
|
|||||||
return Response({"status": "Good"}, status=status.HTTP_200_OK)
|
return Response({"status": "Good"}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserWalletView(generics.UpdateAPIView):
|
|
||||||
# Create user API view
|
|
||||||
serializer_class = CustomUserByWalletBalanceSerializer
|
|
||||||
permission_classes = (permissions.IsAuthenticated,)
|
|
||||||
queryset = User.objects.all()
|
|
||||||
lookup_field = "pk"
|
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
|
||||||
id_to_update = kwargs.get("pk")
|
|
||||||
user_id = request.user.id
|
|
||||||
print(f"User ID: {user_id}")
|
|
||||||
print(f"ID to update: {id_to_update}")
|
|
||||||
if user_id != id_to_update:
|
|
||||||
return Response(
|
|
||||||
{"message": "You are not authorized to update this user."},
|
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
|
||||||
)
|
|
||||||
wallet_balance = request.data.get("wallet_balance")
|
|
||||||
if not wallet_balance:
|
|
||||||
return Response(
|
|
||||||
{"message": "wallet_balance is required."},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
user = self.get_object()
|
|
||||||
user.wallet_balance = wallet_balance
|
|
||||||
user.save()
|
|
||||||
return Response({"message": "Wallet balance updated successfully."})
|
|
||||||
|
|
||||||
|
|
||||||
class CreateTemporaryUserView(generics.CreateAPIView):
|
class CreateTemporaryUserView(generics.CreateAPIView):
|
||||||
# Create user API view
|
|
||||||
serializer_class = TemporaryUserSerializer
|
serializer_class = TemporaryUserSerializer
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
queryset = TemporaryUser.objects.all()
|
queryset = TemporaryUser.objects.all()
|
||||||
throttle_classes = []
|
throttle_classes = []
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
# Extract required fields from request data
|
# Extract data once
|
||||||
username = request.data.get("username")
|
data = request.data
|
||||||
address = request.data.get("address")
|
|
||||||
mobile = request.data.get("mobile")
|
|
||||||
acc_no = request.data.get("acc_no")
|
|
||||||
id_card = request.data.get("id_card")
|
|
||||||
dob = request.data.get("dob")
|
|
||||||
atoll_id = request.data.get("atoll")
|
|
||||||
island_id = request.data.get("island")
|
|
||||||
terms_accepted = request.data.get("terms_accepted")
|
|
||||||
policy_accepted = request.data.get("policy_accepted")
|
|
||||||
firstname = request.data.get("firstname")
|
|
||||||
lastname = request.data.get("lastname")
|
|
||||||
|
|
||||||
current_date = timezone.now()
|
# Validate required fields
|
||||||
|
required_error = validate_required_fields(data)
|
||||||
|
if required_error:
|
||||||
|
return required_error
|
||||||
|
|
||||||
|
# Parse DOB
|
||||||
|
dob_str = data.get("dob")
|
||||||
try:
|
try:
|
||||||
dob = timezone.datetime.strptime(str(dob), "%Y-%m-%d").date()
|
dob = timezone.datetime.strptime(str(dob_str), "%Y-%m-%d").date() # pyright: ignore[reportAttributeAccessIssue]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return Response(
|
return Response({"message": "Invalid date format for DOB. Use YYYY-MM-DD."}, status=400)
|
||||||
{"message": "Invalid date format for DOB. Use YYYY-MM-DD."}, status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
age_from_dob = (
|
# Check age
|
||||||
current_date.year
|
age = calculate_age(dob)
|
||||||
- dob.year
|
if age < 18:
|
||||||
- ((current_date.month, current_date.day) < (dob.month, dob.day))
|
|
||||||
)
|
|
||||||
|
|
||||||
if age_from_dob < 18:
|
|
||||||
return Response({"message": ErrorMessages.UNDERAGE_ERROR}, status=400)
|
return Response({"message": ErrorMessages.UNDERAGE_ERROR}, status=400)
|
||||||
|
|
||||||
if (
|
# Validate uniqueness
|
||||||
TemporaryUser.objects.filter(t_mobile=mobile).exists()
|
uniqueness_error = validate_unique_fields(
|
||||||
or User.objects.filter(mobile=mobile).exists()
|
username=data.get("username"),
|
||||||
):
|
mobile=data.get("mobile"),
|
||||||
return Response({"message": ErrorMessages.MOBILE_EXISTS}, status=400)
|
id_card=data.get("id_card"),
|
||||||
if (
|
)
|
||||||
TemporaryUser.objects.filter(t_username=username).exists()
|
if uniqueness_error:
|
||||||
or User.objects.filter(username=username).exists()
|
return uniqueness_error
|
||||||
):
|
|
||||||
return Response({"message": ErrorMessages.USERNAME_EXISTS}, status=400)
|
|
||||||
if (
|
|
||||||
TemporaryUser.objects.filter(t_id_card=id_card).exists()
|
|
||||||
or User.objects.filter(id_card=id_card).exists()
|
|
||||||
):
|
|
||||||
return Response({"message": "ID card already exists."}, status=400)
|
|
||||||
if (
|
|
||||||
TemporaryUser.objects.filter(t_id_card=id_card).exists()
|
|
||||||
or User.objects.filter(id_card=id_card).exists()
|
|
||||||
):
|
|
||||||
return Response({"message": ErrorMessages.ID_CARD_EXISTS}, status=400)
|
|
||||||
if id_card and not re.match(ID_CARD_PATTERN, id_card):
|
|
||||||
return Response({"message": ErrorMessages.INVALID_ID_CARD}, status=400)
|
|
||||||
if mobile is None or not re.match(MOBILE_PATTERN, mobile):
|
|
||||||
return Response({"message": ErrorMessages.INVALID_MOBILE}, status=400)
|
|
||||||
if acc_no is None or not re.match(ACCOUNT_NUMBER_PATTERN, acc_no):
|
|
||||||
return Response({"message": ErrorMessages.INVALID_ACCOUNT}, status=400)
|
|
||||||
|
|
||||||
# Validate required fields first
|
# Validate patterns
|
||||||
validation_error = self.validate_required_fields(request.data)
|
pattern_error = validate_patterns(
|
||||||
if validation_error:
|
id_card=data.get("id_card"),
|
||||||
return validation_error
|
mobile=data.get("mobile"),
|
||||||
|
acc_no=data.get("acc_no"),
|
||||||
|
)
|
||||||
|
if pattern_error:
|
||||||
|
return pattern_error
|
||||||
|
|
||||||
# Fetch Atoll and Island instances
|
# Fetch related objects
|
||||||
|
atoll_id = data.get("atoll")
|
||||||
|
island_id = data.get("island")
|
||||||
try:
|
try:
|
||||||
atoll = Atoll.objects.get(id=atoll_id)
|
atoll = Atoll.objects.get(id=atoll_id)
|
||||||
island = Island.objects.get(id=island_id)
|
island = Island.objects.get(id=island_id)
|
||||||
except Atoll.DoesNotExist:
|
except ObjectDoesNotExist as e:
|
||||||
return Response({"message": "Atoll not found."}, status=404)
|
model_name = "Atoll" if isinstance(e, Atoll.DoesNotExist) else "Island"
|
||||||
except Island.DoesNotExist:
|
return Response({"message": f"{model_name} not found."}, status=404)
|
||||||
return Response({"message": "Island not found."}, status=404)
|
|
||||||
|
|
||||||
# Create user
|
# Create user
|
||||||
temp_user = TemporaryUser.objects.create(
|
temp_user = TemporaryUser.objects.create(
|
||||||
t_first_name=firstname,
|
t_first_name=data.get("firstname"),
|
||||||
t_last_name=lastname,
|
t_last_name=data.get("lastname"),
|
||||||
t_username=str(username),
|
t_username=str(data.get("username")),
|
||||||
t_email=None,
|
t_email=None,
|
||||||
t_address=address,
|
t_address=data.get("address"),
|
||||||
t_mobile=mobile,
|
t_mobile=data.get("mobile"),
|
||||||
t_acc_no=acc_no,
|
t_acc_no=data.get("acc_no"),
|
||||||
t_id_card=id_card,
|
t_id_card=data.get("id_card"),
|
||||||
t_dob=dob,
|
t_dob=dob,
|
||||||
t_atoll=atoll,
|
t_atoll=atoll,
|
||||||
t_island=island,
|
t_island=island,
|
||||||
t_terms_accepted=terms_accepted,
|
t_terms_accepted=data.get("terms_accepted"),
|
||||||
t_policy_accepted=policy_accepted,
|
t_policy_accepted=data.get("policy_accepted"),
|
||||||
)
|
)
|
||||||
otp_expiry = timezone.now() + timedelta(minutes=3)
|
|
||||||
|
# Generate and send OTP
|
||||||
|
otp_expiry = timezone.now() + timezone.timedelta(minutes=3) #type: ignore
|
||||||
formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S")
|
formatted_time = otp_expiry.strftime("%d/%m/%Y %H:%M:%S")
|
||||||
otp = temp_user.generate_otp()
|
otp = temp_user.generate_otp()
|
||||||
send_otp(
|
send_otp(
|
||||||
str(temp_user.t_mobile),
|
str(temp_user.t_mobile),
|
||||||
f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link",
|
f"Your Registration SARLink OTP: {otp}. \nExpires at {formatted_time}. \n\n- SAR Link",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return success
|
||||||
serializer = self.get_serializer(temp_user)
|
serializer = self.get_serializer(temp_user)
|
||||||
headers = self.get_success_headers(serializer.data)
|
headers = self.get_success_headers(serializer.data)
|
||||||
return Response(
|
return Response(
|
||||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_required_fields(self, data):
|
|
||||||
required_fields = {
|
|
||||||
"firstname": "First name",
|
|
||||||
"lastname": "Last name",
|
|
||||||
"username": "Username",
|
|
||||||
"address": "Address",
|
|
||||||
"mobile": "Mobile number",
|
|
||||||
"acc_no": "Account number",
|
|
||||||
"id_card": "ID card",
|
|
||||||
"dob": "Date of birth",
|
|
||||||
"atoll": "Atoll",
|
|
||||||
"island": "Island",
|
|
||||||
}
|
|
||||||
|
|
||||||
for field, label in required_fields.items():
|
|
||||||
if not data.get(field):
|
|
||||||
return Response({"message": f"{label} is required."}, status=400)
|
|
||||||
|
|
||||||
if data.get("terms_accepted") is None:
|
|
||||||
return Response({"message": "Terms acceptance is required."}, status=400)
|
|
||||||
if data.get("policy_accepted") is None:
|
|
||||||
return Response({"message": "Policy acceptance is required."}, status=400)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyOTPView(generics.GenericAPIView):
|
class VerifyOTPView(generics.GenericAPIView):
|
||||||
permission_classes = (permissions.AllowAny,)
|
permission_classes = (permissions.AllowAny,)
|
||||||
serializer_class = OTPVerificationSerializer
|
serializer_class = OTPVerificationSerializer
|
||||||
@@ -270,17 +183,34 @@ class VerifyOTPView(generics.GenericAPIView):
|
|||||||
acc_no=temp_user.t_acc_no,
|
acc_no=temp_user.t_acc_no,
|
||||||
id_card=temp_user.t_id_card,
|
id_card=temp_user.t_id_card,
|
||||||
dob=temp_user.t_dob,
|
dob=temp_user.t_dob,
|
||||||
|
verified=temp_user.t_verified,
|
||||||
atoll=temp_user.t_atoll,
|
atoll=temp_user.t_atoll,
|
||||||
island=temp_user.t_island,
|
island=temp_user.t_island,
|
||||||
terms_accepted=temp_user.t_terms_accepted,
|
terms_accepted=temp_user.t_terms_accepted,
|
||||||
policy_accepted=temp_user.t_policy_accepted,
|
policy_accepted=temp_user.t_policy_accepted,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if temp_user.t_verified:
|
||||||
|
send_sms(
|
||||||
|
t_user.t_mobile,
|
||||||
|
f"Dear {temp_user.t_first_name} {temp_user.t_last_name}, \n\nYour account has been successfully verified. \n\nYou can now manage your devices and make payments through our portal at https://portal.sarlink.net. \n\n - SAR Link",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
send_sms(
|
||||||
|
t_user.t_mobile,
|
||||||
|
f"Dear {t_user.t_first_name} {t_user.t_last_name}, \n\nYour account registration is being processed. \n\nWe will notify you once verification is complete. \n\n - SAR Link",
|
||||||
|
)
|
||||||
|
|
||||||
# You can now trigger registry verification as a signal or task
|
# You can now trigger registry verification as a signal or task
|
||||||
temp_user.otp_verified = True
|
temp_user.otp_verified = True
|
||||||
temp_user.save()
|
temp_user.save()
|
||||||
|
|
||||||
return Response({"message": "User created successfully."})
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "User created successfully.",
|
||||||
|
"verified": temp_user.t_verified
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class LoginView(KnoxLoginView):
|
class LoginView(KnoxLoginView):
|
||||||
@@ -358,6 +288,59 @@ class UserUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
return super().update(request, *args, **kwargs)
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AgreementUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
|
serializer_class = UserAgreementSerializer
|
||||||
|
queryset = User.objects.all()
|
||||||
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
def update(self, request, *args, **kwargs):
|
||||||
|
user_id = kwargs.get("pk")
|
||||||
|
user = get_object_or_404(User, pk=user_id)
|
||||||
|
if user.is_superuser:
|
||||||
|
return Response(
|
||||||
|
{"message": "You cannot update a superuser."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if request.user != user and (
|
||||||
|
not request.user.is_authenticated
|
||||||
|
or not getattr(request.user, "is_admin", False)
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to update this user."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
serializer = self.get_serializer(
|
||||||
|
user,
|
||||||
|
data=request.data,
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
agreement = request.data.get("agreement")
|
||||||
|
if not agreement:
|
||||||
|
return Response(
|
||||||
|
{"message": "Agreement file is required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement.size > 10 * 1024 * 1024: # 5 MB limit
|
||||||
|
return Response(
|
||||||
|
{"message": "File size exceeds 10 MB limit."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if agreement.content_type not in [
|
||||||
|
"application/pdf",
|
||||||
|
]:
|
||||||
|
return Response(
|
||||||
|
{"message": "Invalid file type. Only PDF files are allowed."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# rename the file name to a random UUID followed by user_id
|
||||||
|
agreement.name = f"{uuid.uuid4()}_{user_id}_agreement.pdf"
|
||||||
|
if agreement:
|
||||||
|
user.agreement = agreement
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
user.save()
|
||||||
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class KnoxTokenListApiView(
|
class KnoxTokenListApiView(
|
||||||
StaffEditorPermissionMixin,
|
StaffEditorPermissionMixin,
|
||||||
generics.ListAPIView,
|
generics.ListAPIView,
|
||||||
@@ -378,7 +361,7 @@ class KnoxTokenListApiView(
|
|||||||
|
|
||||||
class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
|
class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
|
||||||
serializer_class = CustomReadOnlyUserSerializer
|
serializer_class = CustomReadOnlyUserSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend] #type: ignore
|
||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
filterset_class = UserFilter
|
filterset_class = UserFilter
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
@@ -414,6 +397,16 @@ class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
serializer = self.get_serializer(user, data=request.data, partial=True)
|
serializer = self.get_serializer(user, data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
result = check_person_api_verification(user_data=user, id_card=user.id_card)
|
result = check_person_api_verification(user_data=user, id_card=user.id_card)
|
||||||
|
# The verification system might not have the records of every user hence can be skipped if not found and verify directly.
|
||||||
|
if result.get("error") == "Not Found":
|
||||||
|
user.verified = True
|
||||||
|
user.save()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "User not found in the verification system. User marked as verified."
|
||||||
|
},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
if not result["ok"]:
|
if not result["ok"]:
|
||||||
return Response(
|
return Response(
|
||||||
result,
|
result,
|
||||||
@@ -481,12 +474,14 @@ def filter_user(request):
|
|||||||
return Response({"ok": False})
|
return Response({"ok": False})
|
||||||
|
|
||||||
filters = Q()
|
filters = Q()
|
||||||
if id_card is not None:
|
if id_card and mobile:
|
||||||
filters |= Q(id_card=id_card)
|
filters = Q(id_card=id_card) & Q(mobile=mobile)
|
||||||
if mobile is not None:
|
elif id_card:
|
||||||
filters |= Q(mobile=mobile)
|
filters = Q(id_card=id_card)
|
||||||
|
elif mobile:
|
||||||
|
filters = Q(mobile=mobile)
|
||||||
|
|
||||||
user = User.objects.filter(filters).first()
|
user = User.objects.only("id", "verified").filter(filters).first()
|
||||||
|
|
||||||
print(f"Querying with filters: {filters}")
|
print(f"Querying with filters: {filters}")
|
||||||
print(f"Found user: {user}")
|
print(f"Found user: {user}")
|
||||||
@@ -507,33 +502,29 @@ def filter_temporary_user(request):
|
|||||||
return Response({"ok": False})
|
return Response({"ok": False})
|
||||||
|
|
||||||
filters = Q()
|
filters = Q()
|
||||||
if id_card is not None:
|
if id_card and mobile:
|
||||||
|
filters |= Q(t_id_card=id_card) & Q(t_mobile=mobile)
|
||||||
|
elif id_card:
|
||||||
filters |= Q(t_id_card=id_card)
|
filters |= Q(t_id_card=id_card)
|
||||||
if mobile is not None:
|
elif mobile:
|
||||||
filters |= Q(t_mobile=mobile)
|
filters |= Q(t_mobile=mobile)
|
||||||
|
|
||||||
user = TemporaryUser.objects.filter(filters).first()
|
user = (
|
||||||
|
TemporaryUser.objects.only("t_id", "otp_verified", "t_verified")
|
||||||
|
.filter(filters)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
print(f"Querying with filters: {filters}")
|
print(f"Querying with filters: {filters}")
|
||||||
print(f"Found temporary user: {user}")
|
print(f"Found temporary user: {user}")
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
{"ok": True, "otp_verified": user.otp_verified}
|
{"ok": True, "otp_verified": user.otp_verified, "t_verified": user.t_verified}
|
||||||
if user
|
if user
|
||||||
else {"ok": False, "otp_verified": False}
|
else {"ok": False, "otp_verified": False, "t_verified": False}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ListUserByIDCardView(generics.ListAPIView):
|
|
||||||
# Create user API view
|
|
||||||
permission_classes = (permissions.AllowAny,)
|
|
||||||
serializer_class = CustomReadOnlyUserByIDCardSerializer
|
|
||||||
filter_backends = [DjangoFilterBackend]
|
|
||||||
filterset_fields = "__all__"
|
|
||||||
filterset_class = UserFilter
|
|
||||||
queryset = User.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = CustomReadOnlyUserSerializer
|
serializer_class = CustomReadOnlyUserSerializer
|
||||||
@@ -545,7 +536,7 @@ class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
|||||||
if (
|
if (
|
||||||
user != instance
|
user != instance
|
||||||
and not getattr(user, "is_admin", False)
|
and not getattr(user, "is_admin", False)
|
||||||
and not user.is_superuser
|
and not user.is_superuser #type: ignore
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "You are not authorized to view this user's details."},
|
{"message": "You are not authorized to view this user's details."},
|
||||||
|
|||||||
+6
-3
@@ -26,7 +26,7 @@ env.read_env(os.path.join(BASE_DIR, ".env"))
|
|||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = env("SECRET_KEY", default=get_random_secret_key())
|
SECRET_KEY = env("SECRET_KEY", default=get_random_secret_key()) #type: ignore
|
||||||
|
|
||||||
DEBUG = env.bool("DJANGO_DEBUG", default=True) # type: ignore
|
DEBUG = env.bool("DJANGO_DEBUG", default=True) # type: ignore
|
||||||
|
|
||||||
@@ -235,8 +235,11 @@ REST_FRAMEWORK = {
|
|||||||
"login": "1000/min",
|
"login": "1000/min",
|
||||||
},
|
},
|
||||||
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
|
"EXCEPTION_HANDLER": "api.exceptions.custom_exception_handler",
|
||||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
"DEFAULT_RENDERER_CLASSES": (
|
||||||
# "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema"
|
"rest_framework.renderers.JSONRenderer",
|
||||||
|
# "rest_framework.renderers.BrowsableAPIRenderer",
|
||||||
|
),
|
||||||
|
# "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -1,9 +1,28 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Payment, BillFormula, Topup
|
from .models import Payment, BillFormula, Topup, WalletTransaction
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"id",
|
||||||
|
"user",
|
||||||
|
"amount",
|
||||||
|
"transaction_type",
|
||||||
|
"description",
|
||||||
|
"reference_id",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__mobile",
|
||||||
|
"user__id_card",
|
||||||
|
)
|
||||||
|
list_filter = ("transaction_type",)
|
||||||
|
|
||||||
|
|
||||||
class PaymentAdmin(admin.ModelAdmin):
|
class PaymentAdmin(admin.ModelAdmin):
|
||||||
list_display = (
|
list_display = (
|
||||||
"id",
|
"id",
|
||||||
@@ -53,3 +72,4 @@ class TopupAdmin(admin.ModelAdmin):
|
|||||||
admin.site.register(Payment, PaymentAdmin)
|
admin.site.register(Payment, PaymentAdmin)
|
||||||
admin.site.register(BillFormula)
|
admin.site.register(BillFormula)
|
||||||
admin.site.register(Topup, TopupAdmin)
|
admin.site.register(Topup, TopupAdmin)
|
||||||
|
admin.site.register(WalletTransaction, WalletTransactionAdmin)
|
||||||
|
|||||||
+39
-2
@@ -1,5 +1,5 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from .models import Payment, Topup
|
from .models import Payment, Topup, WalletTransaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
amount = django_filters.RangeFilter(field_name="amount")
|
amount = django_filters.RangeFilter(field_name="amount")
|
||||||
number_of_months = django_filters.RangeFilter(field_name="number_of_months")
|
number_of_months = django_filters.RangeFilter(field_name="number_of_months")
|
||||||
paid = django_filters.BooleanFilter(field_name="paid")
|
paid = django_filters.BooleanFilter(field_name="paid")
|
||||||
|
user = django_filters.CharFilter(method="filter_user_search")
|
||||||
method = django_filters.ChoiceFilter(
|
method = django_filters.ChoiceFilter(
|
||||||
choices=Payment.PAYMENT_TYPES, lookup_expr="iexact"
|
choices=Payment.PAYMENT_TYPES, lookup_expr="iexact"
|
||||||
)
|
)
|
||||||
@@ -16,6 +17,14 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
created_at = django_filters.DateFromToRangeFilter()
|
created_at = django_filters.DateFromToRangeFilter()
|
||||||
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
||||||
|
|
||||||
|
def filter_user_search(self, queryset, name, value):
|
||||||
|
return queryset.filter(
|
||||||
|
Q(user__first_name__icontains=value)
|
||||||
|
| Q(user__last_name__icontains=value)
|
||||||
|
| Q(user__id_card__icontains=value)
|
||||||
|
| Q(user__mobile__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
def filter_is_expired(self, queryset, name, value):
|
def filter_is_expired(self, queryset, name, value):
|
||||||
"""
|
"""
|
||||||
Filter payments based on whether they are expired or not
|
Filter payments based on whether they are expired or not
|
||||||
@@ -29,7 +38,14 @@ class PaymentFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Payment
|
model = Payment
|
||||||
fields = "__all__"
|
fields = [
|
||||||
|
"amount",
|
||||||
|
"paid",
|
||||||
|
"method",
|
||||||
|
"user",
|
||||||
|
"created_at",
|
||||||
|
"is_expired",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TopupFilter(django_filters.FilterSet):
|
class TopupFilter(django_filters.FilterSet):
|
||||||
@@ -71,3 +87,24 @@ class TopupFilter(django_filters.FilterSet):
|
|||||||
"created_at",
|
"created_at",
|
||||||
"is_expired",
|
"is_expired",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionFilter(django_filters.FilterSet):
|
||||||
|
user = django_filters.CharFilter(method="filter_user_search")
|
||||||
|
amount = django_filters.RangeFilter(field_name="amount")
|
||||||
|
created_at = django_filters.DateFromToRangeFilter(field_name="created_at")
|
||||||
|
|
||||||
|
def filter_user_search(self, queryset, name, value):
|
||||||
|
"""
|
||||||
|
Search across multiple user fields: first_name, last_name, id_card, mobile
|
||||||
|
"""
|
||||||
|
return queryset.filter(
|
||||||
|
Q(user__first_name__icontains=value)
|
||||||
|
| Q(user__last_name__icontains=value)
|
||||||
|
| Q(user__id_card__icontains=value)
|
||||||
|
| Q(user__mobile__icontains=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = WalletTransaction
|
||||||
|
fields = ["user", "amount", "created_at", "transaction_type"]
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-25 08:34
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("billing", "0013_payment_expiry_notification_sent"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="WalletTransaction",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("amount", models.FloatField()),
|
||||||
|
(
|
||||||
|
"transaction_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[("TOPUP", "Topup"), ("DEBIT", "Debit")], max_length=10
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("description", models.TextField(blank=True, null=True)),
|
||||||
|
(
|
||||||
|
"reference_id",
|
||||||
|
models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="wallet_transactions",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created_at"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-07-27 07:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("billing", "0014_wallettransaction"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="topup",
|
||||||
|
name="payment_type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[("CASH", "Cash"), ("TRANSFER", "Transfer")],
|
||||||
|
default="TRANSFER",
|
||||||
|
max_length=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 5.2 on 2025-09-20 16:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("billing", "0015_topup_payment_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="payment",
|
||||||
|
name="source_bank",
|
||||||
|
field=models.CharField(blank=True, default="", null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="topup",
|
||||||
|
name="source_bank",
|
||||||
|
field=models.CharField(blank=True, default="", null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
+40
-5
@@ -1,11 +1,11 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from api.models import User
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from devices.models import Device
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
user = settings.AUTH_USER_MODEL
|
||||||
from devices.models import Device
|
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
|
|
||||||
@@ -17,10 +17,11 @@ class Payment(models.Model):
|
|||||||
]
|
]
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
mib_reference = models.CharField(default="", null=True, blank=True)
|
mib_reference = models.CharField(default="", null=True, blank=True)
|
||||||
|
source_bank = models.CharField(default="", null=True, blank=True)
|
||||||
number_of_months = models.IntegerField()
|
number_of_months = models.IntegerField()
|
||||||
amount = models.FloatField()
|
amount = models.FloatField()
|
||||||
paid = models.BooleanField(default=False)
|
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)
|
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")
|
||||||
expiry_notification_sent = models.BooleanField(default=False)
|
expiry_notification_sent = models.BooleanField(default=False)
|
||||||
@@ -65,7 +66,15 @@ class BillFormula(models.Model):
|
|||||||
class Topup(models.Model):
|
class Topup(models.Model):
|
||||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
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")
|
||||||
|
payment_type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=[
|
||||||
|
("CASH", "Cash"),
|
||||||
|
("TRANSFER", "Transfer"),
|
||||||
|
],
|
||||||
|
default="TRANSFER",
|
||||||
|
)
|
||||||
paid = models.BooleanField(default=False)
|
paid = models.BooleanField(default=False)
|
||||||
paid_at = models.DateTimeField(null=True, blank=True)
|
paid_at = models.DateTimeField(null=True, blank=True)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
@@ -78,6 +87,7 @@ class Topup(models.Model):
|
|||||||
default="PENDING",
|
default="PENDING",
|
||||||
)
|
)
|
||||||
mib_reference = models.CharField(default="", null=True, blank=True)
|
mib_reference = models.CharField(default="", null=True, blank=True)
|
||||||
|
source_bank = models.CharField(default="", null=True, blank=True)
|
||||||
expires_at = models.DateTimeField(null=True, blank=True)
|
expires_at = models.DateTimeField(null=True, blank=True)
|
||||||
expiry_notification_sent = models.BooleanField(default=False)
|
expiry_notification_sent = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(default=timezone.now)
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
@@ -94,3 +104,28 @@ class Topup(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ["-created_at"]
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransaction(models.Model):
|
||||||
|
TRANSACTION_TYPES = [
|
||||||
|
("TOPUP", "Topup"),
|
||||||
|
("DEBIT", "Debit"),
|
||||||
|
]
|
||||||
|
|
||||||
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="wallet_transactions",
|
||||||
|
)
|
||||||
|
amount = models.FloatField()
|
||||||
|
transaction_type = models.CharField(max_length=10, choices=TRANSACTION_TYPES)
|
||||||
|
description = models.TextField(blank=True, null=True)
|
||||||
|
reference_id = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.transaction_type} {self.amount} ({self.user.username})"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created_at"]
|
||||||
|
|||||||
+35
-3
@@ -1,11 +1,23 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from .models import Payment, Topup
|
from .models import Payment, Topup, WalletTransaction
|
||||||
from devices.serializers import DeviceSerializer
|
from devices.serializers import AdminDeviceSerializer
|
||||||
|
|
||||||
|
|
||||||
class PaymentSerializer(serializers.ModelSerializer):
|
class PaymentSerializer(serializers.ModelSerializer):
|
||||||
devices = DeviceSerializer(many=True, read_only=True)
|
devices = AdminDeviceSerializer(many=True, read_only=True)
|
||||||
is_expired = serializers.SerializerMethodField()
|
is_expired = serializers.SerializerMethodField()
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_user(self, obj):
|
||||||
|
user = obj.user
|
||||||
|
if user:
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"name": user.first_name + " " + user.last_name,
|
||||||
|
"id_card": user.id_card,
|
||||||
|
"mobile": user.mobile,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
def get_is_expired(self, obj):
|
def get_is_expired(self, obj):
|
||||||
return obj.is_expired
|
return obj.is_expired
|
||||||
@@ -57,3 +69,23 @@ class TopupSerializer(serializers.ModelSerializer):
|
|||||||
"updated_at",
|
"updated_at",
|
||||||
]
|
]
|
||||||
read_only_fields = ["id", "created_at", "updated_at"]
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|
||||||
|
|
||||||
|
class WalletTransactionSerializer(serializers.ModelSerializer):
|
||||||
|
user = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_user(self, obj):
|
||||||
|
user = obj.user
|
||||||
|
if user:
|
||||||
|
return {
|
||||||
|
"id": user.id,
|
||||||
|
"name": user.first_name + " " + user.last_name,
|
||||||
|
"id_card": user.id_card,
|
||||||
|
"mobile": user.mobile,
|
||||||
|
}
|
||||||
|
return None
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = WalletTransaction
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = ["id", "created_at", "updated_at"]
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ from .views import (
|
|||||||
VerifyTopupPaymentAPIView,
|
VerifyTopupPaymentAPIView,
|
||||||
TopupDetailAPIView,
|
TopupDetailAPIView,
|
||||||
CancelTopupView,
|
CancelTopupView,
|
||||||
|
ListWalletTransactionView,
|
||||||
|
AdminTopupCreateView,
|
||||||
|
# AlertTestView,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@@ -36,9 +39,18 @@ urlpatterns = [
|
|||||||
VerifyTopupPaymentAPIView.as_view(),
|
VerifyTopupPaymentAPIView.as_view(),
|
||||||
name="verify-topup-payment",
|
name="verify-topup-payment",
|
||||||
),
|
),
|
||||||
|
path("admin-topup/", AdminTopupCreateView.as_view(), name="admin-topup"),
|
||||||
path(
|
path(
|
||||||
"topup/<str:pk>/cancel/",
|
"topup/<str:pk>/cancel/",
|
||||||
CancelTopupView.as_view(),
|
CancelTopupView.as_view(),
|
||||||
name="cancel-topup",
|
name="cancel-topup",
|
||||||
),
|
),
|
||||||
|
# Wallet transactions
|
||||||
|
path(
|
||||||
|
"wallet-transactions/",
|
||||||
|
ListWalletTransactionView.as_view(),
|
||||||
|
name="list-wallet-transactions",
|
||||||
|
),
|
||||||
|
# Test tg notification
|
||||||
|
# path("test-alert/", AlertTestView.as_view(), name="test-alert"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
def calculate_total_new_price(number_of_devices, number_of_months):
|
||||||
|
monthly_price_map = {
|
||||||
|
1: 100,
|
||||||
|
2: 175,
|
||||||
|
3: 250,
|
||||||
|
4: 325,
|
||||||
|
5: 400,
|
||||||
|
6: 475,
|
||||||
|
7: 550,
|
||||||
|
8: 625,
|
||||||
|
9: 700,
|
||||||
|
10: 775,
|
||||||
|
11: 850,
|
||||||
|
12: 925,
|
||||||
|
13: 1000,
|
||||||
|
14: 1075,
|
||||||
|
15: 1150,
|
||||||
|
16: 1225,
|
||||||
|
17: 1300,
|
||||||
|
}
|
||||||
|
|
||||||
|
if number_of_devices < 1 or number_of_devices > 17:
|
||||||
|
raise ValueError("Number of devices must be between 1 and 17.")
|
||||||
|
|
||||||
|
monthly_price = monthly_price_map[number_of_devices]
|
||||||
|
total_price = monthly_price * number_of_months
|
||||||
|
print(f"Monthly price for {number_of_devices} devices: {monthly_price}")
|
||||||
|
|
||||||
|
print(f"Total price for {number_of_months} months: {total_price}")
|
||||||
|
return total_price
|
||||||
|
|
||||||
|
|
||||||
|
calculate_total_new_price(number_of_devices=2, number_of_months=3)
|
||||||
+247
-34
@@ -13,13 +13,25 @@ from rest_framework.response import Response
|
|||||||
from api.mixins import StaffEditorPermissionMixin
|
from api.mixins import StaffEditorPermissionMixin
|
||||||
from api.tasks import add_new_devices_to_omada
|
from api.tasks import add_new_devices_to_omada
|
||||||
from apibase.env import BASE_DIR, env
|
from apibase.env import BASE_DIR, env
|
||||||
|
from django.db.models import Prefetch
|
||||||
import logging
|
import logging
|
||||||
|
from .utils import calculate_total_new_price
|
||||||
|
|
||||||
from .models import Device, Payment, Topup
|
from .models import Device, Payment, Topup, WalletTransaction
|
||||||
from .serializers import PaymentSerializer, UpdatePaymentSerializer, TopupSerializer
|
from .serializers import (
|
||||||
from .filters import PaymentFilter, TopupFilter
|
PaymentSerializer,
|
||||||
|
UpdatePaymentSerializer,
|
||||||
|
TopupSerializer,
|
||||||
|
WalletTransactionSerializer,
|
||||||
|
)
|
||||||
|
from .filters import PaymentFilter, TopupFilter, WalletTransactionFilter
|
||||||
from dataclasses import dataclass, asdict
|
from dataclasses import dataclass, asdict
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from api.models import User
|
||||||
|
from api.omada import Omada
|
||||||
|
|
||||||
|
# from api.bot import send_telegram_alert, telegram_loop, escape_markdown_v2
|
||||||
|
# import asyncio
|
||||||
|
|
||||||
env.read_env(os.path.join(BASE_DIR, ".env"))
|
env.read_env(os.path.join(BASE_DIR, ".env"))
|
||||||
|
|
||||||
@@ -49,23 +61,31 @@ class InsufficientFundsError(Exception):
|
|||||||
class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
queryset = Payment.objects.all().select_related("user")
|
queryset = Payment.objects.all().select_related("user")
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend] #type: ignore
|
||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
filterset_class = PaymentFilter
|
filterset_class = PaymentFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
unpaid_qs = Payment.objects.filter(paid=False).order_by("-created_at")
|
||||||
if self.request.user.is_superuser:
|
device_qs = Device.objects.prefetch_related(
|
||||||
return queryset
|
Prefetch("payments", queryset=unpaid_qs, to_attr="unpaid_payments")
|
||||||
return queryset.filter(user=self.request.user)
|
)
|
||||||
|
queryset = Payment.objects.select_related("user").prefetch_related(
|
||||||
|
Prefetch("devices", queryset=device_qs)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.request.user.is_superuser: #type: ignore
|
||||||
|
queryset = queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
def create(self, request):
|
def create(self, request):
|
||||||
data = request.data
|
data = request.data
|
||||||
user = request.user
|
user = request.user
|
||||||
amount = data.get("amount")
|
|
||||||
number_of_months = data.get("number_of_months")
|
number_of_months = data.get("number_of_months")
|
||||||
|
number_of_devices = 0
|
||||||
device_ids = data.get("device_ids", [])
|
device_ids = data.get("device_ids", [])
|
||||||
print(amount, number_of_months, device_ids)
|
print(number_of_months, device_ids)
|
||||||
current_time = timezone.now()
|
current_time = timezone.now()
|
||||||
expires_at = current_time + timedelta(minutes=10)
|
expires_at = current_time + timedelta(minutes=10)
|
||||||
for device_id in device_ids:
|
for device_id in device_ids:
|
||||||
@@ -76,9 +96,10 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
|||||||
{"message": f"Device with id {device_id} not found."},
|
{"message": f"Device with id {device_id} not found."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if not amount or not number_of_months:
|
number_of_devices += 1
|
||||||
|
if not number_of_months:
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "amount and number_of_months are required."},
|
{"message": "number_of_months is required."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if not device_ids:
|
if not device_ids:
|
||||||
@@ -86,7 +107,9 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
|||||||
{"message": "device_ids are required."},
|
{"message": "device_ids are required."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
# Create payment
|
amount = calculate_total_new_price(
|
||||||
|
number_of_devices=number_of_devices, number_of_months=number_of_months
|
||||||
|
)
|
||||||
payment = Payment.objects.create(
|
payment = Payment.objects.create(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
number_of_months=number_of_months,
|
number_of_months=number_of_months,
|
||||||
@@ -137,7 +160,7 @@ class PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView)
|
|||||||
|
|
||||||
|
|
||||||
class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Payment.objects.select_related("user").all()
|
queryset = Payment.objects.select_related("user").prefetch_related("devices").all()
|
||||||
serializer_class = UpdatePaymentSerializer
|
serializer_class = UpdatePaymentSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -157,22 +180,22 @@ class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
queryset = Payment.objects.all()
|
queryset = Payment.objects.select_related("user").prefetch_related("devices").all()
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
def update(self, request, *args, **kwargs):
|
def update(self, request, *args, **kwargs):
|
||||||
# TODO: Fix check for success payment
|
|
||||||
payment = self.get_object()
|
payment = self.get_object()
|
||||||
|
devices = payment.devices.all()
|
||||||
data = request.data
|
data = request.data
|
||||||
user = request.user
|
user = request.user
|
||||||
print("logged in user", user)
|
user_details = f"{user.first_name.capitalize() if user.first_name else ''} {user.last_name.capitalize() if user.last_name else ''} {user.mobile}" # type: ignore
|
||||||
print("Payment user", payment.user)
|
omada_client = Omada()
|
||||||
if payment.paid:
|
if payment.paid:
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "Payment has already been verified."},
|
{"message": "Payment has already been verified."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if payment.user != user and not user.is_superuser:
|
if payment.user != user and not user.is_superuser: #type: ignore
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "You are not authorized to verify this payment."},
|
{"message": "You are not authorized to verify this payment."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -184,7 +207,6 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
devices = payment.devices.all()
|
|
||||||
if method == "WALLET":
|
if method == "WALLET":
|
||||||
if user.wallet_balance < payment.amount: # type: ignore
|
if user.wallet_balance < payment.amount: # type: ignore
|
||||||
return Response(
|
return Response(
|
||||||
@@ -193,8 +215,33 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.process_wallet_payment(
|
self.process_wallet_payment(
|
||||||
user,
|
user, # type: ignore
|
||||||
payment,
|
payment,
|
||||||
|
devices,
|
||||||
|
)
|
||||||
|
device_list = []
|
||||||
|
for device in devices:
|
||||||
|
device_list.append(
|
||||||
|
{
|
||||||
|
"mac": device.mac,
|
||||||
|
"name": f"{user_details} - {device.name}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if device.registered:
|
||||||
|
omada_client.block_device(
|
||||||
|
mac_address=device.mac, operation="unblock"
|
||||||
|
)
|
||||||
|
if not device.registered:
|
||||||
|
# Add to omada
|
||||||
|
add_new_devices_to_omada.defer(new_devices=device_list)
|
||||||
|
device.registered = True
|
||||||
|
device.save()
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"status": True,
|
||||||
|
"message": "Payment verified successfully using wallet.",
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
if method == "TRANSFER":
|
if method == "TRANSFER":
|
||||||
data = {
|
data = {
|
||||||
@@ -211,7 +258,6 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
expiry_date=expiry_date,
|
expiry_date=expiry_date,
|
||||||
has_a_pending_payment=False,
|
has_a_pending_payment=False,
|
||||||
registered=True,
|
|
||||||
)
|
)
|
||||||
payment.status = "PAID"
|
payment.status = "PAID"
|
||||||
payment.save()
|
payment.save()
|
||||||
@@ -221,9 +267,13 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
device_list.append(
|
device_list.append(
|
||||||
{
|
{
|
||||||
"mac": device.mac,
|
"mac": device.mac,
|
||||||
"name": device.name,
|
"name": f"{user_details} - {device.name}",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if device.registered:
|
||||||
|
omada_client.block_device(
|
||||||
|
mac_address=device.mac, operation="unblock"
|
||||||
|
)
|
||||||
if not device.registered:
|
if not device.registered:
|
||||||
# Add to omada
|
# Add to omada
|
||||||
add_new_devices_to_omada.defer(new_devices=device_list)
|
add_new_devices_to_omada.defer(new_devices=device_list)
|
||||||
@@ -250,16 +300,28 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_wallet_payment(self, user, payment):
|
def process_wallet_payment(self, user: User, payment: Payment, devices=None):
|
||||||
print("processing wallet payment...")
|
print("processing wallet payment...")
|
||||||
print(user, payment.amount)
|
print(user, payment.amount)
|
||||||
|
# Use passed devices or fetch if not provided
|
||||||
|
if devices is None:
|
||||||
|
devices = payment.devices.all()
|
||||||
|
|
||||||
payment.paid = True
|
payment.paid = True
|
||||||
payment.paid_at = timezone.now()
|
payment.paid_at = timezone.now()
|
||||||
payment.method = "WALLET"
|
payment.method = "WALLET"
|
||||||
|
payment.status = "PAID"
|
||||||
|
expiry_date = timezone.now() + timedelta(days=30 * payment.number_of_months)
|
||||||
|
devices.update(
|
||||||
|
is_active=True,
|
||||||
|
expiry_date=expiry_date,
|
||||||
|
has_a_pending_payment=False,
|
||||||
|
)
|
||||||
payment.save()
|
payment.save()
|
||||||
|
|
||||||
user.wallet_balance -= payment.amount
|
user.deduct_wallet_funds(
|
||||||
|
payment.amount, "Wallet payment for devices", payment.id
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -294,6 +356,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
payment.paid_at = timezone.now()
|
payment.paid_at = timezone.now()
|
||||||
payment.method = "TRANSFER"
|
payment.method = "TRANSFER"
|
||||||
payment.mib_reference = mib_resp["transaction"]["ref"] or ""
|
payment.mib_reference = mib_resp["transaction"]["ref"] or ""
|
||||||
|
payment.source_bank = mib_resp["transaction"]["sourceBank"] or ""
|
||||||
payment.save()
|
payment.save()
|
||||||
return PaymentVerificationResponse(
|
return PaymentVerificationResponse(
|
||||||
message=mib_resp["message"],
|
message=mib_resp["message"],
|
||||||
@@ -307,7 +370,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Payment.objects.all()
|
queryset = Payment.objects.select_related("user").all()
|
||||||
serializer_class = PaymentSerializer
|
serializer_class = PaymentSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -319,7 +382,7 @@ class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
{"message": "Payment has already been cancelled."},
|
{"message": "Payment has already been cancelled."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if instance.user != user and not user.is_superuser:
|
if instance.user != user and not user.is_superuser: #type: ignore
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "You are not authorized to cancel this payment."},
|
{"message": "You are not authorized to cancel this payment."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -337,9 +400,9 @@ class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||||
queryset = Topup.objects.all()
|
queryset = Topup.objects.all().prefetch_related("user")
|
||||||
serializer_class = TopupSerializer
|
serializer_class = TopupSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend] #type: ignore
|
||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
filterset_class = TopupFilter
|
filterset_class = TopupFilter
|
||||||
|
|
||||||
@@ -360,10 +423,34 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser:
|
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser: #type: ignore
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
all_topups = request.query_params.get("all_topups", "false").lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
request.user.is_authenticated
|
||||||
|
and getattr(request.user, "is_admin")
|
||||||
|
and bool(all_topups)
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||||
queryset = Topup.objects.all()
|
queryset = Topup.objects.all()
|
||||||
@@ -372,7 +459,7 @@ class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser:
|
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser: #type: ignore
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(user=self.request.user)
|
return queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
@@ -412,6 +499,7 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi
|
|||||||
topup.paid = True
|
topup.paid = True
|
||||||
topup.mib_reference = mib_resp["transaction"]["ref"] or ""
|
topup.mib_reference = mib_resp["transaction"]["ref"] or ""
|
||||||
topup.paid_at = mib_resp["transaction"]["trxDate"]
|
topup.paid_at = mib_resp["transaction"]["trxDate"]
|
||||||
|
topup.source_bank = mib_resp["transaction"]["sourceBank"] or ""
|
||||||
topup.save()
|
topup.save()
|
||||||
return PaymentVerificationResponse(
|
return PaymentVerificationResponse(
|
||||||
message=mib_resp["message"],
|
message=mib_resp["message"],
|
||||||
@@ -432,7 +520,7 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi
|
|||||||
{"message": "Payment has already been verified."},
|
{"message": "Payment has already been verified."},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
if topup_instance.user != user and not user.is_superuser:
|
if topup_instance.user != user and not user.is_superuser: #type: ignore
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "You are not allowed to pay for this topup."},
|
{"message": "You are not allowed to pay for this topup."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
@@ -449,7 +537,11 @@ class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIVi
|
|||||||
topup_verification_response = self.verify_transfer_topup(data, topup_instance)
|
topup_verification_response = self.verify_transfer_topup(data, topup_instance)
|
||||||
print("Topup verification response:", topup_verification_response)
|
print("Topup verification response:", topup_verification_response)
|
||||||
if topup_verification_response.success:
|
if topup_verification_response.success:
|
||||||
user.wallet_balance += topup_instance.amount # type: ignore
|
user.add_wallet_funds( # type: ignore
|
||||||
|
topup_instance.amount,
|
||||||
|
f"Topup of {topup_instance.amount} MVR",
|
||||||
|
topup_instance.id,
|
||||||
|
)
|
||||||
user.save()
|
user.save()
|
||||||
topup_instance.status = "PAID"
|
topup_instance.status = "PAID"
|
||||||
topup_instance.save()
|
topup_instance.save()
|
||||||
@@ -495,7 +587,7 @@ class CancelTopupView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
if (
|
if (
|
||||||
instance.user != user
|
instance.user != user
|
||||||
and getattr(user, "is_admin")
|
and getattr(user, "is_admin")
|
||||||
and not user.is_superuser
|
and not user.is_superuser #type: ignore
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
{"message": "You are not authorized to delete this topup."},
|
{"message": "You are not authorized to delete this topup."},
|
||||||
@@ -509,3 +601,124 @@ class CancelTopupView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
instance.status = "CANCELLED"
|
instance.status = "CANCELLED"
|
||||||
instance.save()
|
instance.save()
|
||||||
return super().update(request, *args, **kwargs)
|
return super().update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class AdminTopupCreateView(StaffEditorPermissionMixin, generics.CreateAPIView):
|
||||||
|
queryset = Topup.objects.all().select_related("user")
|
||||||
|
serializer_class = TopupSerializer
|
||||||
|
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
data = request.data
|
||||||
|
user_id = data.get("user_id")
|
||||||
|
amount = data.get("amount")
|
||||||
|
topup_description = ""
|
||||||
|
admin_description = data.get("description", "")
|
||||||
|
if not getattr(request.user, "is_admin", False):
|
||||||
|
return Response(
|
||||||
|
{"message": "You are not authorized to perform this action."},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
if not user_id:
|
||||||
|
return Response(
|
||||||
|
{"message": "user_id is required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
if not amount:
|
||||||
|
return Response(
|
||||||
|
{"message": "amount is required."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
user = User.objects.filter(id=user_id).first()
|
||||||
|
if not user:
|
||||||
|
return Response(
|
||||||
|
{"message": "User not found."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
topup = Topup.objects.create(
|
||||||
|
amount=amount,
|
||||||
|
user=user,
|
||||||
|
paid=True,
|
||||||
|
paid_at=timezone.now(),
|
||||||
|
payment_type="CASH",
|
||||||
|
status="PAID",
|
||||||
|
)
|
||||||
|
default_description = f"Topup of {amount} MVR (Cash)"
|
||||||
|
if admin_description and admin_description.strip() != "":
|
||||||
|
topup_description = admin_description.strip()
|
||||||
|
else:
|
||||||
|
topup_description = default_description
|
||||||
|
user.add_wallet_funds(amount, topup_description, topup.id)
|
||||||
|
serializer = TopupSerializer(topup)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class ListWalletTransactionView(StaffEditorPermissionMixin, generics.ListAPIView):
|
||||||
|
serializer_class = WalletTransactionSerializer
|
||||||
|
queryset = WalletTransaction.objects.all().select_related("user")
|
||||||
|
filter_backends = [DjangoFilterBackend] #type: ignore
|
||||||
|
filterset_fields = "__all__"
|
||||||
|
filterset_class = WalletTransactionFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
if getattr(self.request.user, "is_admin") or self.request.user.is_superuser: #type: ignore
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(user=self.request.user)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
|
all_transations = request.query_params.get(
|
||||||
|
"all_transations", "false"
|
||||||
|
).lower() in [
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
]
|
||||||
|
if (
|
||||||
|
request.user.is_authenticated
|
||||||
|
and getattr(request.user, "is_admin")
|
||||||
|
and bool(all_transations)
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(user=request.user)
|
||||||
|
|
||||||
|
page = self.paginate_queryset(queryset)
|
||||||
|
if page is not None:
|
||||||
|
serializer = self.get_serializer(page, many=True)
|
||||||
|
return self.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
# class AlertTestView(generics.GenericAPIView):
|
||||||
|
# def get(self, request, *args, **kwargs):
|
||||||
|
# msg = """*ID Card:* A265117\n*Name:* Abdulla Aidhaan\n*House Name:* Nooree Villa\n*Date of Birth:* 1997-08-24\n*Island:* Sh Funadhoo\n*Mobile:* 9697404\nVisit [SAR Link Portal](https://portal.sarlink.net) to manually verify this user."""
|
||||||
|
# print(msg)
|
||||||
|
# print("escaped:", escape_markdown_v2(msg))
|
||||||
|
# user = request.user
|
||||||
|
# print(user)
|
||||||
|
|
||||||
|
# global telegram_loop # Access the global loop
|
||||||
|
|
||||||
|
# if telegram_loop is None:
|
||||||
|
# return Response(
|
||||||
|
# {"message": "Telegram worker not initialized."},
|
||||||
|
# status=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
# )
|
||||||
|
|
||||||
|
# try:
|
||||||
|
# asyncio.run_coroutine_threadsafe(
|
||||||
|
# send_telegram_alert(markdown_message=escape_markdown_v2(msg)),
|
||||||
|
# telegram_loop,
|
||||||
|
# ).result()
|
||||||
|
|
||||||
|
# return Response(
|
||||||
|
# {"message": "Alert sent successfully."}, status=status.HTTP_200_OK
|
||||||
|
# )
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.warning("[alert test] TELEGRAM ALERT ERROR", e)
|
||||||
|
# return Response(
|
||||||
|
# {"message": "Alert failed to send."}, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
# )
|
||||||
|
|||||||
+4
-2
@@ -1,8 +1,10 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from api.models import User
|
|
||||||
import re
|
import re
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
user = settings.AUTH_USER_MODEL
|
||||||
|
|
||||||
|
|
||||||
def validate_mac_address(value):
|
def validate_mac_address(value):
|
||||||
@@ -38,7 +40,7 @@ class Device(models.Model):
|
|||||||
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)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
User, on_delete=models.SET_NULL, null=True, blank=True, related_name="devices"
|
user, on_delete=models.SET_NULL, null=True, blank=True, related_name="devices"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
+19
-3
@@ -36,9 +36,9 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
def get_pending_payment_id(self, obj):
|
def get_pending_payment_id(self, obj):
|
||||||
unpaid_payment = (
|
unpaid_payment = (
|
||||||
Payment.objects.filter(devices=obj, paid=False)
|
obj.unpaid_payments[0]
|
||||||
.order_by("-created_at")
|
if hasattr(obj, "unpaid_payments") and obj.unpaid_payments
|
||||||
.first()
|
else None
|
||||||
)
|
)
|
||||||
return unpaid_payment.id if unpaid_payment else None
|
return unpaid_payment.id if unpaid_payment else None
|
||||||
|
|
||||||
@@ -58,6 +58,22 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDeviceSerializer(serializers.ModelSerializer):
|
||||||
|
pending_payment_id = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_pending_payment_id(self, obj):
|
||||||
|
unpaid_payment = (
|
||||||
|
obj.unpaid_payments[0]
|
||||||
|
if hasattr(obj, "unpaid_payments") and obj.unpaid_payments
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return unpaid_payment.id if unpaid_payment else None
|
||||||
|
|
||||||
|
class Meta: # type: ignore
|
||||||
|
model = Device
|
||||||
|
fields = "__all__"
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyDeviceSerializer(serializers.ModelSerializer):
|
class ReadOnlyDeviceSerializer(serializers.ModelSerializer):
|
||||||
user = CustomReadOnlyUserSerializer(read_only=True)
|
user = CustomReadOnlyUserSerializer(read_only=True)
|
||||||
|
|
||||||
|
|||||||
+27
-7
@@ -3,7 +3,9 @@ from xmlrpc.client import Boolean
|
|||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from billing.models import Payment
|
||||||
from .models import Device
|
from .models import Device
|
||||||
|
from django.db.models import Prefetch
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
CreateDeviceSerializer,
|
CreateDeviceSerializer,
|
||||||
DeviceSerializer,
|
DeviceSerializer,
|
||||||
@@ -28,6 +30,13 @@ class DeviceListCreateAPIView(
|
|||||||
filterset_fields = "__all__"
|
filterset_fields = "__all__"
|
||||||
filterset_class = DeviceFilter
|
filterset_class = DeviceFilter
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
unpaid_qs = Payment.objects.filter(paid=False).order_by("-created_at")
|
||||||
|
base_qs = Device.objects.select_related("user").prefetch_related(
|
||||||
|
Prefetch("payments", queryset=unpaid_qs, to_attr="unpaid_payments")
|
||||||
|
)
|
||||||
|
return base_qs.all()
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
queryset = self.filter_queryset(self.get_queryset())
|
queryset = self.filter_queryset(self.get_queryset())
|
||||||
all_devices = request.query_params.get("all_devices", "false").lower() in [
|
all_devices = request.query_params.get("all_devices", "false").lower() in [
|
||||||
@@ -58,7 +67,17 @@ class DeviceListCreateAPIView(
|
|||||||
return DeviceSerializer
|
return DeviceSerializer
|
||||||
|
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
mac = request.data.get("mac", None)
|
user = request.user
|
||||||
|
name = request.data.get("name", None)
|
||||||
|
user_details = f"{user.first_name.capitalize() if user.first_name else ''} {user.last_name.capitalize() if user.last_name else ''} {user.mobile}" # type: ignore
|
||||||
|
omada_device_name = f"{user_details} - {name}" if name else user_details
|
||||||
|
if len(omada_device_name) > 64:
|
||||||
|
return Response(
|
||||||
|
{"message": "Device name is too long."},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
raw_mac = request.data.get("mac", None)
|
||||||
|
mac = raw_mac.strip() if raw_mac else None
|
||||||
MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$")
|
MAC_REGEX = re.compile(r"^([0-9A-Fa-f]{2}([.:-]?)){5}[0-9A-Fa-f]{2}$")
|
||||||
NORMALIZE_MAC_REGEX = re.compile(r"[^0-9A-Fa-f]")
|
NORMALIZE_MAC_REGEX = re.compile(r"[^0-9A-Fa-f]")
|
||||||
if not isinstance(mac, str) or not MAC_REGEX.match(mac):
|
if not isinstance(mac, str) or not MAC_REGEX.match(mac):
|
||||||
@@ -90,7 +109,7 @@ class DeviceDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = CreateDeviceSerializer
|
serializer_class = CreateDeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -116,7 +135,7 @@ class DeviceUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = BlockDeviceSerializer
|
serializer_class = BlockDeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
@@ -136,10 +155,11 @@ class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
if not isinstance(blocked, bool):
|
if not isinstance(blocked, bool):
|
||||||
return Response({"message": "Blocked field must be a boolean."}, status=400)
|
return Response({"message": "Blocked field must be a boolean."}, status=400)
|
||||||
omada_client = Omada()
|
omada_client = Omada()
|
||||||
blocked = omada_client.block_device(
|
omada_response = omada_client.block_device(
|
||||||
instance.mac, operation="block" if blocked else "unblock"
|
instance.mac, operation="block" if blocked else "unblock"
|
||||||
)
|
)
|
||||||
if blocked.errorCode == 0:
|
print(f"Blocked: {blocked}")
|
||||||
|
if omada_response.errorCode == 0:
|
||||||
instance.blocked = blocked
|
instance.blocked = blocked
|
||||||
instance.save()
|
instance.save()
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=False)
|
serializer = self.get_serializer(instance, data=request.data, partial=False)
|
||||||
@@ -148,13 +168,13 @@ class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
|||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{"message": blocked.msg},
|
{"message": omada_response.msg},
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
class DeviceDestroyAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.select_related("user").all()
|
||||||
serializer_class = DeviceSerializer
|
serializer_class = DeviceSerializer
|
||||||
lookup_field = "pk"
|
lookup_field = "pk"
|
||||||
|
|
||||||
|
|||||||
+9
-14
@@ -1,16 +1,11 @@
|
|||||||
{
|
{
|
||||||
"venvPath": ".",
|
"venvPath": ".",
|
||||||
"venv": ".venv",
|
"venv": ".venv",
|
||||||
"reportMissingImports": "error",
|
"reportMissingImports": "error",
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"typeCheckingMode": "standard",
|
"typeCheckingMode": "standard",
|
||||||
"reportArgumentType": "warning",
|
"reportArgumentType": "warning",
|
||||||
"reportUnusedVariable": "warning",
|
"reportUnusedVariable": "warning",
|
||||||
"reportFunctionMemberAccess": "none",
|
"reportFunctionMemberAccess": "none",
|
||||||
"exclude": [
|
"exclude": ["council-api/**/migrations", "**/__pycache__"]
|
||||||
"council-api/**/migrations",
|
|
||||||
"**/__pycache__",
|
|
||||||
"src/experimental",
|
|
||||||
"src/typestubs"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user