Author SHA1 Message Date
i701 4aae0064ca fix(views): optimize database queries to solve N+1 problems 🔨🐛 2025-07-25 23:22:35 +05:00
i701 118ad52c71 feat(wallet): add transaction_type field to WalletTransactionFilter for enhanced filtering options 2025-07-25 16:02:01 +05:00
i701 8d9a2ed2e0 fix(payment): update payment verification response status to 200 OK and set payment status to PAID 2025-07-25 15:32:28 +05:00
i701 3200d8e41c feat(user): add wallet_balance field to CustomReadOnlyUserSerializer 2025-07-25 15:19:05 +05:00
i701 1554829b9a feat(wallet): implement wallet transaction model, views, and serializers for fund management 2025-07-25 14:38:34 +05:00
i701 f8c91e8f14 feat(admin): add agreement field to UserAdmin for improved user management 2025-07-25 10:44:58 +05:00
i701 f84f03fd5b feat(agreement): implement user agreement update functionality with validation checks 2025-07-25 10:12:04 +05:00
i701 fd603daaaf refactor: remove UpdateAgreementView and its URL path from user management 🔨 2025-07-25 00:05:35 +05:00
i701 9e4449d0d6 feat(agreement): add agreement field to user model and implement agreement upload functionality 2025-07-25 00:01:56 +05:00
i701 087782e351 feat(verification): handle user verification not found scenario and mark user as verified 2025-07-24 23:35:53 +05:00
i701 446ca6653e feat(filters, serializers): add user filtering and enhance user serialization in Payment and Device models 2025-07-24 20:34:50 +05:00
Abdulla AidhaanandGitHub f8c0725558 Merge pull request #17 from i701/feat/user-verification-flow
Feat/user verification flow
2025-07-23 22:34:37 +05:00
i701 7c5ed1e89d feat(payment): enhance list view to support admin access to all payments 2025-07-23 22:10:49 +05:00
i701 976a119fcc refactor(models): add db_index to mobile and id_card fields in User and TemporaryUser models 🔨 2025-07-16 01:49:17 +05:00
Abdulla AidhaanandGitHub ea57598e8d Merge pull request #16 from i701/feat/user-verification-flow
refactor(verification): enhance error handling and response structure in user verification process 🔨
2025-07-16 00:39:17 +05:00
i701 d64a2675e4 refactor(verification): enhance error handling and response structure in user verification process 🔨 2025-07-15 23:04:11 +05:00
Abdulla AidhaanandGitHub 4cc6e91a66 Merge pull request #15 from i701/feat/user-verification-flow
feat(user): add user rejection endpoint and improve verification response messages 
2025-07-13 19:42:59 +05:00
i701 eee314af46 feat(user): add user rejection endpoint and improve verification response messages 2025-07-13 19:38:01 +05:00
i701 ff065fa4a9 feat(user): add user update endpoint with authorization checks and serializer support 2025-07-11 19:55:25 +05:00
i701 72c2ea1ecc refactor(views): update user queryset logic to filter out superusers for non-admins 🔨 2025-07-11 15:10:01 +05:00
i701 596ce510c7 feat(filters): add id_card and mobile filters to UserFilter class 2025-07-11 15:09:51 +05:00
i701 436a8b7d7a feat(profile): add user profile update serializer and update profile URL to use UserprofileAPIView 2025-07-11 11:44:40 +05:00
i701 82ae1e6cea feat(user): enhance user profile management with dynamic serializer selection and authorization check 2025-07-11 11:44:21 +05:00
i701 56ab79bd8c refactor(signals): exclude delete permissions for payment and topup in user permission assignment 🔨 2025-07-11 09:57:58 +05:00
Abdulla AidhaanandGitHub 64c2189209 Merge pull request #14 from i701/feat/task-queues
refactor(tasks): update job scheduling frequency and reduce max hours for old job removal 🔨
2025-07-11 09:54:00 +05:00
Abdulla AidhaanandGitHub dacf821bad Merge pull request #13 from i701/feat/task-queues
feat(devices): add pending_payment_id field to DeviceSerializer for tracking unpaid payments 
2025-07-09 22:07:46 +05:00
Abdulla AidhaanandGitHub 3f0a5f0f03 Merge pull request #12 from i701/feat/task-queues
refactor(devices): update device payment status in background task and remove unused serializer field 🔨
2025-07-09 21:56:48 +05:00
Abdulla AidhaanandGitHub db53874ff4 Merge pull request #11 from i701/feat/task-queues
refactor(tasks): replace Enum with string literals for notification types in SMS tasks 🔨
2025-07-09 20:33:03 +05:00
Abdulla AidhaanandGitHub d557bb879f Merge pull request #10 from i701/feat/task-queues
feature / task queues cleanup 
2025-07-09 20:16:48 +05:00
22 changed files with 755 additions and 131 deletions
+1
View File
@@ -163,3 +163,4 @@ cython_debug/
#staticfiles #staticfiles
staticfiles/ staticfiles/
postgres_data/ postgres_data/
media/
+1
View File
@@ -47,6 +47,7 @@ class UserAdmin(BaseUserAdmin):
"island", "island",
"terms_accepted", "terms_accepted",
"policy_accepted", "policy_accepted",
"agreement",
) )
}, },
), ),
+2
View File
@@ -6,6 +6,8 @@ class UserFilter(django_filters.FilterSet):
last_name = django_filters.CharFilter(lookup_expr="icontains") last_name = django_filters.CharFilter(lookup_expr="icontains")
first_name = django_filters.CharFilter(lookup_expr="icontains") first_name = django_filters.CharFilter(lookup_expr="icontains")
email = django_filters.CharFilter(lookup_expr="icontains") email = django_filters.CharFilter(lookup_expr="icontains")
id_card = django_filters.CharFilter(lookup_expr="icontains")
mobile = django_filters.CharFilter(lookup_expr="icontains")
class Meta: class Meta:
model = User model = User
@@ -0,0 +1,40 @@
# Generated by Django 5.2 on 2025-07-15 20:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("api", "0016_user_is_admin"),
]
operations = [
migrations.AlterField(
model_name="temporaryuser",
name="t_id_card",
field=models.CharField(
blank=True, db_index=True, max_length=255, null=True, unique=True
),
),
migrations.AlterField(
model_name="temporaryuser",
name="t_mobile",
field=models.CharField(
blank=True, db_index=True, max_length=255, null=True, unique=True
),
),
migrations.AlterField(
model_name="user",
name="id_card",
field=models.CharField(
blank=True, db_index=True, max_length=255, null=True, unique=True
),
),
migrations.AlterField(
model_name="user",
name="mobile",
field=models.CharField(
blank=True, db_index=True, max_length=255, null=True, unique=True
),
),
]
+22
View File
@@ -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/",
),
),
]
+46 -5
View File
@@ -8,15 +8,20 @@ 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):
address = models.CharField(max_length=255, blank=True) address = models.CharField(max_length=255, blank=True)
email = models.EmailField(blank=True, null=True, unique=True) email = models.EmailField(blank=True, null=True, unique=True)
mobile = models.CharField(max_length=255, blank=True, unique=True, null=True) mobile = models.CharField(
max_length=255, blank=True, unique=True, null=True, db_index=True
)
designation = models.CharField(max_length=255, blank=True) designation = models.CharField(max_length=255, blank=True)
acc_no = models.CharField(max_length=255, blank=True) acc_no = models.CharField(max_length=255, blank=True)
id_card = models.CharField(max_length=255, blank=True, unique=True, null=True) id_card = models.CharField(
max_length=255, blank=True, unique=True, null=True, db_index=True
)
verified = models.BooleanField(default=False) verified = models.BooleanField(default=False)
is_admin = models.BooleanField(default=False) is_admin = models.BooleanField(default=False)
dob = models.DateField(blank=True, null=True) dob = models.DateField(blank=True, null=True)
@@ -30,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()
@@ -46,10 +83,14 @@ class TemporaryUser(models.Model):
t_last_name = models.CharField(max_length=255, blank=True) t_last_name = models.CharField(max_length=255, blank=True)
t_address = models.CharField(max_length=255, blank=True) t_address = models.CharField(max_length=255, blank=True)
t_email = models.EmailField(blank=True, null=True, unique=True) t_email = models.EmailField(blank=True, null=True, unique=True)
t_mobile = models.CharField(max_length=255, blank=True, unique=True, null=True) t_mobile = models.CharField(
max_length=255, blank=True, unique=True, null=True, db_index=True
)
t_designation = models.CharField(max_length=255, blank=True) t_designation = models.CharField(max_length=255, blank=True)
t_acc_no = models.CharField(max_length=255, blank=True) t_acc_no = models.CharField(max_length=255, blank=True)
t_id_card = models.CharField(max_length=255, blank=True, unique=True, null=True) t_id_card = models.CharField(
max_length=255, blank=True, unique=True, null=True, db_index=True
)
t_verified = models.BooleanField(default=False) t_verified = models.BooleanField(default=False)
t_dob = models.DateField(blank=True, null=True) t_dob = models.DateField(blank=True, null=True)
t_terms_accepted = models.BooleanField(default=False) t_terms_accepted = models.BooleanField(default=False)
@@ -95,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):
+41 -1
View File
@@ -1,9 +1,12 @@
from knox.models import AuthToken from knox.models import AuthToken
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from api.models import User, Atoll, Island, TemporaryUser from api.models import Atoll, Island, TemporaryUser
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class IslandSerializer(serializers.ModelSerializer): class IslandSerializer(serializers.ModelSerializer):
@@ -21,6 +24,40 @@ class AtollSerializer(serializers.ModelSerializer):
depth = 2 depth = 2
class UserProfileUpdateSerializer(serializers.ModelSerializer):
class Meta: # type: ignore
model = User
fields = (
"email",
"mobile",
) # Only allow these fields
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta: # type: ignore
model = User
fields = (
"id_card",
"mobile",
"first_name",
"last_name",
"address",
"dob",
"atoll",
"island",
"agreement",
)
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"""
@@ -80,7 +117,10 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
"username", "username",
"mobile", "mobile",
"address", "address",
"acc_no",
"id_card", "id_card",
"agreement",
"wallet_balance",
) )
depth = 1 depth = 1
+6 -3
View File
@@ -12,12 +12,15 @@ from api.tasks import verify_user_with_person_api_task
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def assign_device_permissions(sender, instance, created, **kwargs): def assign_device_permissions(sender, instance, created, **kwargs):
if created: if created:
# Assign all permissions for devices and read permission for atoll and island
device_permissions = Permission.objects.filter(content_type__model="device") device_permissions = Permission.objects.filter(content_type__model="device")
atoll_read_permission = Permission.objects.get(codename="view_atoll") atoll_read_permission = Permission.objects.get(codename="view_atoll")
island_read_permission = Permission.objects.get(codename="view_island") island_read_permission = Permission.objects.get(codename="view_island")
payment_permissions = Permission.objects.filter(content_type__model="payment") payment_permissions = Permission.objects.filter(
topup_permissions = Permission.objects.filter(content_type__model="topup") content_type__model="payment"
).exclude(codename__startswith="delete_")
topup_permissions = Permission.objects.filter(
content_type__model="topup"
).exclude(codename__startswith="delete_")
for permission in topup_permissions: for permission in topup_permissions:
instance.user_permissions.add(permission) instance.user_permissions.add(permission)
+14 -7
View File
@@ -5,7 +5,7 @@ from knox import views as knox_views
from .views import ( from .views import (
LoginView, LoginView,
CreateTemporaryUserView, CreateTemporaryUserView,
ManageUserView, UserprofileAPIView,
KnoxTokenListApiView, KnoxTokenListApiView,
ListUserView, ListUserView,
UserDetailAPIView, UserDetailAPIView,
@@ -18,29 +18,36 @@ from .views import (
RetrieveUpdateDestroyIslandView, RetrieveUpdateDestroyIslandView,
filter_user, filter_user,
filter_temporary_user, filter_temporary_user,
UpdateUserWalletView,
VerifyOTPView, VerifyOTPView,
UserVerifyAPIView, UserVerifyAPIView,
UserUpdateAPIView,
UserRejectAPIView,
AgreementUpdateAPIView,
) )
urlpatterns = [ urlpatterns = [
path("register/", CreateTemporaryUserView.as_view(), name="register"), path("register/", CreateTemporaryUserView.as_view(), name="register"),
path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"), path("register/verify/", VerifyOTPView.as_view(), name="verify-otp"),
path("profile/", ManageUserView.as_view(), name="profile"), path("profile/", UserprofileAPIView.as_view(), name="profile"),
path("login/", LoginView.as_view(), name="knox_login"), path("login/", LoginView.as_view(), name="knox_login"),
path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"), path("logout/", knox_views.LogoutView.as_view(), name="knox_logout"),
path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"), path("logoutall/", knox_views.LogoutAllView.as_view(), name="knox_logoutall"),
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>/verify/", UserVerifyAPIView.as_view(), name="user-verify"), 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
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("healthcheck/", healthcheck, name="healthcheck"), path("healthcheck/", healthcheck, name="healthcheck"),
path("test/", test_email, name="testemail"), path("test/", test_email, name="testemail"),
path("atolls/", ListAtollView.as_view(), name="atolls"), path("atolls/", ListAtollView.as_view(), name="atolls"),
+27 -18
View File
@@ -1,5 +1,5 @@
import logging import logging
from typing import List, TypedDict from typing import List, Optional, TypedDict
import requests import requests
from decouple import config from decouple import config
from api.models import User from api.models import User
@@ -40,7 +40,9 @@ def reverse_dhivehi_string(input_str):
class MismatchResult(TypedDict): class MismatchResult(TypedDict):
ok: bool ok: bool
mismatch_fields: List[str] mismatch_fields: Optional[List[str]]
error: Optional[str]
detail: Optional[str]
def check_person_api_verification( def check_person_api_verification(
@@ -63,20 +65,22 @@ def check_person_api_verification(
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."
) )
print(id_card)
response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}") response = requests.get(f"{PERSON_VERIFY_BASE_URL}/api/person/{id_card}")
api_reponse = response.json()
if response.status_code != 200: if response.status_code != 200:
logger.error( logger.error(
f"Failed to fetch data from Person API for ID Card '{id_card}'. " f"Failed to fetch data from Person API for ID Card '{id_card}'. "
f"Status Code: {response.status_code}, Response: {response.text}" f"Status Code: {response.status_code}, Response: {response.text}"
) )
return {"ok": False, "mismatch_fields": ["api_error"]} return {
api_data = response.json() "ok": False,
if not api_data: "mismatch_fields": None,
logger.error( "error": response.json()["error"] if "error" in response.json() else None,
f"No data found in Person API for ID Card '{id_card}'. Response: {response.text}" "detail": response.json()["detail"]
) if "detail" in response.json()
return {"ok": False, "mismatch_fields": ["no_data"]} else None,
}
# Initialize a list to hold fields that do not match # Initialize a list to hold fields that do not match
mismatch_fields = [] mismatch_fields = []
@@ -86,12 +90,12 @@ def check_person_api_verification(
user_dob_iso = user_data.dob.isoformat() if user_data.dob else None user_dob_iso = user_data.dob.isoformat() if user_data.dob else None
# Prepare API data for comparison # Prepare API data for comparison
api_nic = api_data.get("nic") api_nic = api_reponse.get("nic")
api_name = api_data.get("name_en") api_name = api_reponse.get("name_en")
api_house_name = api_data.get("house_name_en") api_house_name = api_reponse.get("house_name_en")
api_dob = api_data.get("dob") api_dob = api_reponse.get("dob")
api_atoll = api_data.get("atoll_en") api_atoll = api_reponse.get("atoll_en")
api_island_name = api_data.get("island_name_en") api_island_name = api_reponse.get("island_name_en")
# Perform comparisons and identify mismatches # Perform comparisons and identify mismatches
if user_data.id_card != api_nic: if user_data.id_card != api_nic:
@@ -134,6 +138,11 @@ def check_person_api_verification(
) )
if mismatch_fields: if mismatch_fields:
return {"ok": False, "mismatch_fields": mismatch_fields} return {
"ok": False,
"mismatch_fields": mismatch_fields,
"error": None,
"detail": None,
}
else: else:
return {"ok": True, "mismatch_fields": []} return {"ok": True, "mismatch_fields": [], "error": None, "detail": None}
+197 -61
View File
@@ -7,6 +7,7 @@ from rest_framework.authtoken.serializers import AuthTokenSerializer
from api.filters import UserFilter from api.filters import UserFilter
from api.mixins import StaffEditorPermissionMixin from api.mixins import StaffEditorPermissionMixin
from api.models import User, Atoll, Island, TemporaryUser from api.models import User, Atoll, Island, TemporaryUser
from api.notifications import send_sms
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@@ -14,9 +15,10 @@ 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,
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
@@ -32,6 +34,7 @@ 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
# local apps import # local apps import
from .serializers import ( from .serializers import (
@@ -39,7 +42,7 @@ from .serializers import (
AuthSerializer, AuthSerializer,
CustomUserSerializer, CustomUserSerializer,
CustomReadOnlyUserSerializer, CustomReadOnlyUserSerializer,
CustomReadOnlyUserByIDCardSerializer, UserProfileUpdateSerializer,
) )
ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$" ID_CARD_PATTERN = r"^[A-Z]{1,2}[0-9]{6,7}$"
@@ -62,35 +65,6 @@ 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 # Create user API view
serializer_class = TemporaryUserSerializer serializer_class = TemporaryUserSerializer
@@ -305,17 +279,129 @@ class LoginView(KnoxLoginView):
return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST) return Response({"message": message}, status=status.HTTP_400_BAD_REQUEST)
class ManageUserView(generics.RetrieveUpdateAPIView): class UserprofileAPIView(generics.RetrieveUpdateAPIView):
"""Manage the authenticated user""" """Retrieve user api view"""
serializer_class = CustomUserSerializer queryset = User.objects.all()
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
def get_serializer_class(self):
"""Return the serializer class based on the request method"""
if self.request.method == "GET":
return CustomReadOnlyUserSerializer
elif self.request.method == "PUT" or self.request.method == "PATCH":
return UserProfileUpdateSerializer
return super().get_serializer_class()
def get_object(self): def get_object(self):
"""Retrieve and return authenticated user""" """Retrieve and return authenticated user"""
return self.request.user return self.request.user
class UserUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
serializer_class = UserUpdateSerializer
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_file = request.data.get("agreement_file")
if not agreement_file:
return Response(
{"message": "Agreement file is required."},
status=status.HTTP_400_BAD_REQUEST,
)
if agreement_file.size > 10 * 1024 * 1024: # 5 MB limit
return Response(
{"message": "File size exceeds 10 MB limit."},
status=status.HTTP_400_BAD_REQUEST,
)
if agreement_file.content_type not in [
"application/pdf",
]:
return Response(
{"message": "Invalid file type. Only PDF files are allowed."},
status=status.HTTP_400_BAD_REQUEST,
)
if agreement_file:
user.agreement = agreement_file
serializer.is_valid(raise_exception=True)
user.save()
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,
@@ -343,9 +429,9 @@ class ListUserView(StaffEditorPermissionMixin, generics.ListAPIView):
def get_queryset(self): def get_queryset(self):
user = self.request.user user = self.request.user
if user.is_authenticated and user.is_staff: if user.is_authenticated and getattr(user, "is_admin"):
return User.objects.all() return User.objects.filter(is_superuser=False)
return User.objects.filter(is_staff=False) return User.objects.none()
class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
@@ -364,30 +450,80 @@ class UserVerifyAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
{"message": "You are not authorized to update this user."}, {"message": "You are not authorized to update this user."},
status=status.HTTP_403_FORBIDDEN, status=status.HTTP_403_FORBIDDEN,
) )
serializer = self.get_serializer(user, data=request.data, partial=True) if user.verified:
serializer.is_valid(raise_exception=True)
verified_person = check_person_api_verification(
user_data=user, id_card=user.id_card
)
if not verified_person["ok"]:
return Response( return Response(
{ {"message": "User is already verified."},
"message": "User verification failed. Please check sarlink user details.",
"mismatch_fields": verified_person["mismatch_fields"],
},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if verified_person["mismatch_fields"]: serializer = self.get_serializer(user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
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( return Response(
{ {
"message": "User verification failed due to mismatched fields.", "message": "User not found in the verification system. User marked as verified."
"mismatch_fields": verified_person["mismatch_fields"],
}, },
status=status.HTTP_404_NOT_FOUND,
)
if not result["ok"]:
return Response(
result,
status=status.HTTP_404_NOT_FOUND,
)
if result["mismatch_fields"]:
return Response(
result,
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
user.verified = True user.verified = True
user.save() user.save()
return Response({"message": "User verification status updated."}) return Response({"message": "User successfully verified."})
class UserRejectAPIView(StaffEditorPermissionMixin, generics.DestroyAPIView):
serializer_class = CustomUserSerializer
queryset = User.objects.all()
lookup_field = "pk"
def destroy(self, request, *args, **kwargs):
rejection_details = request.data.get("rejection_details", "")
if not rejection_details:
return Response(
{"message": "Rejection details are required."},
status=status.HTTP_400_BAD_REQUEST,
)
user_id = kwargs.get("pk")
user = get_object_or_404(User, pk=user_id)
mobile_number = user.mobile
if not mobile_number:
return Response(
{"message": "User does not have a mobile number."},
status=status.HTTP_400_BAD_REQUEST,
)
if user.is_superuser:
return Response(
{"message": "You cannot remove 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 reject this user."},
status=status.HTTP_403_FORBIDDEN,
)
user.delete()
t_user = get_object_or_404(TemporaryUser, t_mobile=user.mobile)
t_user.delete()
send_sms(message=rejection_details, mobile=mobile_number)
return Response(
{"message": "User successfully rejected."},
status=status.HTTP_204_NO_CONTENT,
)
@api_view(["GET"]) @api_view(["GET"])
@@ -442,16 +578,6 @@ def filter_temporary_user(request):
) )
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
@@ -459,6 +585,16 @@ class UserDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
user = request.user
if (
user != instance
and not getattr(user, "is_admin", False)
and not user.is_superuser
):
return Response(
{"message": "You are not authorized to view this user's details."},
status=status.HTTP_403_FORBIDDEN,
)
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
data = serializer.data data = serializer.data
+5 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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"],
},
),
]
+30 -5
View File
@@ -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.
@@ -20,7 +20,7 @@ class Payment(models.Model):
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 +65,7 @@ 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")
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(
@@ -94,3 +94,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
View File
@@ -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"]
+7
View File
@@ -10,6 +10,7 @@ from .views import (
VerifyTopupPaymentAPIView, VerifyTopupPaymentAPIView,
TopupDetailAPIView, TopupDetailAPIView,
CancelTopupView, CancelTopupView,
ListWalletTransactionView,
) )
urlpatterns = [ urlpatterns = [
@@ -41,4 +42,10 @@ urlpatterns = [
CancelTopupView.as_view(), CancelTopupView.as_view(),
name="cancel-topup", name="cancel-topup",
), ),
# Wallet transactions
path(
"wallet-transactions/",
ListWalletTransactionView.as_view(),
name="list-wallet-transactions",
),
] ]
+131 -15
View File
@@ -13,13 +13,20 @@ 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 .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
env.read_env(os.path.join(BASE_DIR, ".env")) env.read_env(os.path.join(BASE_DIR, ".env"))
@@ -54,10 +61,18 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
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:
queryset = queryset.filter(user=self.request.user)
return queryset
def create(self, request): def create(self, request):
data = request.data data = request.data
@@ -105,6 +120,30 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
serializer = PaymentSerializer(payment) serializer = PaymentSerializer(payment)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
all_payments = request.query_params.get("all_payments", "false").lower() in [
"true",
"1",
"yes",
]
if (
request.user.is_authenticated
and getattr(request.user, "is_admin")
and bool(all_payments)
):
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 PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView): class PaymentDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
queryset = Payment.objects.select_related("user").all() queryset = Payment.objects.select_related("user").all()
@@ -133,11 +172,10 @@ 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").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()
data = request.data data = request.data
user = request.user user = request.user
@@ -169,9 +207,16 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
) )
else: else:
self.process_wallet_payment( self.process_wallet_payment(
user, user, # type: ignore
payment, payment,
) )
return Response(
{
"status": True,
"message": "Payment verified successfully using wallet.",
},
status=status.HTTP_200_OK,
)
if method == "TRANSFER": if method == "TRANSFER":
data = { data = {
"benefName": f"{user.first_name} {user.last_name}", # type: ignore "benefName": f"{user.first_name} {user.last_name}", # type: ignore
@@ -226,16 +271,19 @@ 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):
print("processing wallet payment...") print("processing wallet payment...")
print(user, payment.amount) print(user, payment.amount)
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"
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
@@ -283,7 +331,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"
@@ -313,7 +361,7 @@ 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]
filterset_fields = "__all__" filterset_fields = "__all__"
@@ -340,6 +388,30 @@ class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView
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()
@@ -425,7 +497,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()
@@ -485,3 +561,43 @@ 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 ListWalletTransactionView(StaffEditorPermissionMixin, generics.ListAPIView):
serializer_class = WalletTransactionSerializer
queryset = WalletTransaction.objects.all().select_related("user")
filter_backends = [DjangoFilterBackend]
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:
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)
+4 -2
View File
@@ -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
View File
@@ -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)
+12 -3
View File
@@ -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 [
@@ -90,7 +99,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 +125,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"
@@ -154,7 +163,7 @@ class DeviceBlockAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
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"