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
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
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
18 changed files with 495 additions and 82 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",
) )
}, },
), ),
+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/",
),
),
]
+34 -1
View File
@@ -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
View File
@@ -45,9 +45,19 @@ class UserUpdateSerializer(serializers.ModelSerializer):
"dob", "dob",
"atoll", "atoll",
"island", "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"""
@@ -109,6 +119,8 @@ class CustomReadOnlyUserSerializer(serializers.ModelSerializer):
"address", "address",
"acc_no", "acc_no",
"id_card", "id_card",
"agreement",
"wallet_balance",
) )
depth = 1 depth = 1
+6 -4
View File
@@ -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"),
+85 -41
View File
@@ -15,10 +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, 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
@@ -34,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 (
@@ -41,7 +42,6 @@ from .serializers import (
AuthSerializer, AuthSerializer,
CustomUserSerializer, CustomUserSerializer,
CustomReadOnlyUserSerializer, CustomReadOnlyUserSerializer,
CustomReadOnlyUserByIDCardSerializer,
UserProfileUpdateSerializer, UserProfileUpdateSerializer,
) )
@@ -65,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
@@ -353,6 +324,79 @@ class UserUpdateAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
data=request.data, data=request.data,
partial=True, 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) serializer.is_valid(raise_exception=True)
user.save() user.save()
return super().update(request, *args, **kwargs) return super().update(request, *args, **kwargs)
@@ -414,6 +458,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,
@@ -524,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
+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",
),
] ]
+107 -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
@@ -157,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
@@ -193,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
@@ -250,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
@@ -307,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"
@@ -337,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__"
@@ -364,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()
@@ -449,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()
@@ -509,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"