Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3fc48fddc | ||
|
|
bb2d0348c2 | ||
|
|
e2ede37f4f | ||
|
|
6418b1469d | ||
|
|
911d01b8e3 | ||
|
|
1644bd47b9 | ||
|
|
ff897ee2ab | ||
|
|
35384ef049 | ||
|
|
8657435fbf | ||
|
|
39da7607f6 | ||
|
|
d0a8408121 | ||
|
|
c17e34a592 | ||
|
|
cdfa2d9192 | ||
|
|
342e963861 | ||
|
|
4f794571e9 | ||
|
|
6bc2d71a0e | ||
|
|
ceb30025ee | ||
|
|
950f42ae3f | ||
|
|
f10fa74fbb | ||
|
|
60e394fffa
|
||
|
|
193ce850b4 | ||
|
|
27f89b6d3d | ||
|
|
63b1a6b9ef | ||
|
|
3dafc7d4c8 | ||
|
|
8e564f766b |
@@ -39,3 +39,7 @@ jobs:
|
||||
docker compose --progress plain down portal-api portal-api-nginx && \
|
||||
docker compose --progress plain up -d portal-api portal-api-nginx && \
|
||||
docker compose exec portal-api python manage.py migrate"
|
||||
|
||||
- name: Clean up dangling images
|
||||
if: github.event_name != 'pull_request'
|
||||
run: ssh root@10.0.1.5 -t "docker image prune -f"
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@ def assign_device_permissions(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
async def verify_user_with_person_api(sender, instance, created, **kwargs):
|
||||
def verify_user_with_person_api(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
await verify_user_with_person_api_task.defer_async(instance.id)
|
||||
verify_user_with_person_api_task(instance.id)
|
||||
|
||||
|
||||
@receiver(reset_password_token_created)
|
||||
|
||||
+19
-7
@@ -5,10 +5,12 @@ from api.notifications import send_sms
|
||||
import os
|
||||
import logging
|
||||
from django.utils import timezone
|
||||
from api.notifications import send_clean_telegram_markdown
|
||||
|
||||
# from api.notifications import send_clean_telegram_markdown
|
||||
from api.omada import Omada
|
||||
from apibase.env import env, BASE_DIR
|
||||
from procrastinate.contrib.django import app
|
||||
from procrastinate import builtin_tasks
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -16,10 +18,21 @@ logger = logging.getLogger(__name__)
|
||||
env.read_env(os.path.join(BASE_DIR, ".env"))
|
||||
|
||||
|
||||
@app.task
|
||||
def add(x, y):
|
||||
logger.info(f"Executing test background task with {x} and {y}")
|
||||
return x + y
|
||||
@app.periodic(cron="0 * * * *") # every 1 hour
|
||||
@app.task(
|
||||
queueing_lock="remove_old_jobs",
|
||||
pass_context=True,
|
||||
)
|
||||
async def remove_old_jobs(context, timestamp):
|
||||
logger.info("Running remove_old_jobs task...")
|
||||
return await builtin_tasks.remove_old_jobs(
|
||||
context,
|
||||
queue="heavy_tasks",
|
||||
max_hours=1,
|
||||
remove_failed=True,
|
||||
remove_cancelled=True,
|
||||
remove_aborted=True,
|
||||
)
|
||||
|
||||
|
||||
@app.periodic(
|
||||
@@ -68,8 +81,7 @@ def add_new_devices_to_omada(new_devices: list[dict]):
|
||||
omada_client.add_new_devices_to_omada(new_devices)
|
||||
|
||||
|
||||
@app.task
|
||||
async def verify_user_with_person_api_task(user_id: int):
|
||||
def verify_user_with_person_api_task(user_id: int):
|
||||
"""
|
||||
Verify the user with the Person API.
|
||||
:param user_id: The ID of the user to verify.
|
||||
|
||||
@@ -31,7 +31,6 @@ from typing import cast, Dict, Any
|
||||
from django.core.mail import send_mail
|
||||
from django.db.models import Q
|
||||
from api.notifications import send_otp
|
||||
from .tasks import add
|
||||
from .utils import check_person_api_verification
|
||||
|
||||
# local apps import
|
||||
@@ -60,7 +59,6 @@ class ErrorMessages:
|
||||
|
||||
@api_view(["GET"])
|
||||
def healthcheck(request):
|
||||
add.defer(1, 2)
|
||||
return Response({"status": "Good"}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,15 @@ class PaymentAdmin(admin.ModelAdmin):
|
||||
"paid_at",
|
||||
"method",
|
||||
"created_at",
|
||||
"expires_at",
|
||||
"is_expired",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
@admin.display(boolean=True, description="Expired")
|
||||
def is_expired(self, obj):
|
||||
return obj.is_expired
|
||||
|
||||
|
||||
class TopupAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
|
||||
+28
-1
@@ -1,6 +1,7 @@
|
||||
import django_filters
|
||||
from .models import Payment, Topup
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class PaymentFilter(django_filters.FilterSet):
|
||||
@@ -13,6 +14,18 @@ class PaymentFilter(django_filters.FilterSet):
|
||||
mib_reference = django_filters.CharFilter(lookup_expr="icontains")
|
||||
paid_at = django_filters.DateFromToRangeFilter()
|
||||
created_at = django_filters.DateFromToRangeFilter()
|
||||
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
||||
|
||||
def filter_is_expired(self, queryset, name, value):
|
||||
"""
|
||||
Filter payments based on whether they are expired or not
|
||||
"""
|
||||
now = timezone.now()
|
||||
queryset = queryset.filter(paid=False)
|
||||
if value:
|
||||
return queryset.filter(expires_at__isnull=False, expires_at__lt=now)
|
||||
else:
|
||||
return queryset.filter(Q(expires_at__isnull=True) | Q(expires_at__gte=now))
|
||||
|
||||
class Meta:
|
||||
model = Payment
|
||||
@@ -24,6 +37,7 @@ class TopupFilter(django_filters.FilterSet):
|
||||
paid = django_filters.BooleanFilter(field_name="paid")
|
||||
user = django_filters.CharFilter(method="filter_user_search")
|
||||
created_at = django_filters.DateFromToRangeFilter(field_name="created_at")
|
||||
is_expired = django_filters.BooleanFilter(method="filter_is_expired")
|
||||
|
||||
def filter_user_search(self, queryset, name, value):
|
||||
"""
|
||||
@@ -36,11 +50,24 @@ class TopupFilter(django_filters.FilterSet):
|
||||
| Q(user__mobile__icontains=value)
|
||||
)
|
||||
|
||||
def filter_is_expired(self, queryset, name, value):
|
||||
"""
|
||||
Filter topups based on whether they are expired or not
|
||||
"""
|
||||
now = timezone.now()
|
||||
queryset = queryset.filter(paid=False)
|
||||
if value:
|
||||
return queryset.filter(expires_at__isnull=False, expires_at__lt=now)
|
||||
else:
|
||||
return queryset.filter(Q(expires_at__isnull=True) | Q(expires_at__gte=now))
|
||||
|
||||
class Meta:
|
||||
model = Topup # Assuming Topup is a subclass of Payment
|
||||
model = Topup
|
||||
fields = [
|
||||
"amount",
|
||||
"paid",
|
||||
"status",
|
||||
"user",
|
||||
"created_at",
|
||||
"is_expired",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.2 on 2025-07-06 15:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("billing", "0011_topup_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="payment",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("PAID", "Paid"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PENDING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2 on 2025-07-09 14:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("billing", "0012_payment_status"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="payment",
|
||||
name="expiry_notification_sent",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -23,10 +23,26 @@ class Payment(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="payments")
|
||||
paid_at = models.DateTimeField(null=True, blank=True)
|
||||
method = models.CharField(max_length=255, choices=PAYMENT_TYPES, default="TRANSFER")
|
||||
expiry_notification_sent = models.BooleanField(default=False)
|
||||
expires_at = models.DateTimeField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
devices = models.ManyToManyField(Device, related_name="payments")
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
("PENDING", "Pending"),
|
||||
("PAID", "Paid"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
],
|
||||
default="PENDING",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
if self.expires_at is None:
|
||||
return False
|
||||
return timezone.now() > self.expires_at
|
||||
|
||||
def __str__(self):
|
||||
return f"Payment by {self.user}"
|
||||
|
||||
@@ -5,6 +5,10 @@ from devices.serializers import DeviceSerializer
|
||||
|
||||
class PaymentSerializer(serializers.ModelSerializer):
|
||||
devices = DeviceSerializer(many=True, read_only=True)
|
||||
is_expired = serializers.SerializerMethodField()
|
||||
|
||||
def get_is_expired(self, obj):
|
||||
return obj.is_expired
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = Payment
|
||||
@@ -44,6 +48,7 @@ class TopupSerializer(serializers.ModelSerializer):
|
||||
"amount",
|
||||
"user",
|
||||
"paid",
|
||||
"paid_at",
|
||||
"status",
|
||||
"mib_reference",
|
||||
"is_expired",
|
||||
|
||||
+110
-22
@@ -4,20 +4,26 @@ from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from procrastinate.contrib.django import app
|
||||
from api.notifications import send_sms
|
||||
from billing.models import Topup
|
||||
from billing.models import Topup, Payment
|
||||
from django.utils.timezone import localtime
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@app.periodic(
|
||||
cron="*/30 * * * * *", periodic_id="notify_expired_topups", queue="heavy_tasks"
|
||||
) # every 30 seconds
|
||||
cron="*/1 * * * * *", periodic_id="notify_expired_topups", queue="heavy_tasks"
|
||||
)
|
||||
@app.task
|
||||
def update_expired_topups(timestamp: int):
|
||||
expired_topups_qs = Topup.objects.filter(
|
||||
expires_at__lte=timezone.now(), expiry_notification_sent=False
|
||||
expires_at__lte=timezone.now(),
|
||||
expiry_notification_sent=False,
|
||||
paid=False,
|
||||
).select_related("user")
|
||||
if not expired_topups_qs.exists():
|
||||
logger.info("No expired topups found.")
|
||||
return {"total_expired_topups": 0}
|
||||
|
||||
with transaction.atomic():
|
||||
count = expired_topups_qs.count()
|
||||
@@ -27,35 +33,117 @@ def update_expired_topups(timestamp: int):
|
||||
if topup.user and topup.user.mobile and not topup.expiry_notification_sent:
|
||||
send_sms_task.defer(
|
||||
mobile=topup.user.mobile,
|
||||
type="TOPUP",
|
||||
amount=topup.amount,
|
||||
topup_id=str(topup.id),
|
||||
model_id=str(topup.id),
|
||||
created_at=localtime(topup.created_at).isoformat(),
|
||||
user=f"{topup.user.first_name + ' ' + topup.user.last_name}"
|
||||
if topup.user.last_name and topup.user.first_name
|
||||
else "User",
|
||||
)
|
||||
else:
|
||||
# Mark as notified even if we can't send SMS (no mobile number)
|
||||
topup.expiry_notification_sent = True
|
||||
topup.save()
|
||||
else:
|
||||
topup.expiry_notification_sent = True
|
||||
topup.save()
|
||||
return
|
||||
|
||||
return {
|
||||
"total_expired_topups": count,
|
||||
}
|
||||
|
||||
|
||||
# Assuming you have a separate task for sending SMS if you go that route
|
||||
@app.periodic(
|
||||
cron="*/1 * * * * *", periodic_id="notify_expired_payments", queue="heavy_tasks"
|
||||
)
|
||||
@app.task
|
||||
def send_sms_task(mobile: str, amount: float, topup_id: str, created_at: str):
|
||||
message = (
|
||||
f"Dear {mobile}, \n\nYour topup of {amount} MVR [created at {created_at}] has expired. "
|
||||
"Please make a new topup to update your wallet. \n\n- SAR Link"
|
||||
)
|
||||
send_sms(mobile, message)
|
||||
logger.info(f"SMS sent to {mobile} for expired topup of {amount} MVR.")
|
||||
def update_expired_payments(timestamp: int):
|
||||
expired_payments_qs = Payment.objects.filter(
|
||||
expires_at__lte=timezone.now(),
|
||||
expiry_notification_sent=False,
|
||||
paid=False,
|
||||
).select_related("user")
|
||||
if not expired_payments_qs.exists():
|
||||
logger.info("No expired payments found.")
|
||||
return {"total_expired_payments": 0}
|
||||
|
||||
# Mark the topup as notified after successful SMS sending
|
||||
with transaction.atomic():
|
||||
count = expired_payments_qs.count()
|
||||
logger.info(f"Found {count} payments to expire.")
|
||||
|
||||
for payment in expired_payments_qs:
|
||||
for device in payment.devices.all():
|
||||
device.has_a_pending_payment = False
|
||||
device.save()
|
||||
if (
|
||||
payment.user
|
||||
and payment.user.mobile
|
||||
and not payment.expiry_notification_sent
|
||||
):
|
||||
send_sms_task.defer(
|
||||
mobile=payment.user.mobile,
|
||||
type="PAYMENT",
|
||||
amount=payment.amount,
|
||||
model_id=str(payment.id),
|
||||
created_at=localtime(payment.created_at).isoformat(),
|
||||
user=f"{payment.user.first_name + ' ' + payment.user.last_name}"
|
||||
if payment.user.last_name and payment.user.first_name
|
||||
else "User",
|
||||
)
|
||||
payment.expiry_notification_sent = True
|
||||
payment.save()
|
||||
else:
|
||||
payment.expiry_notification_sent = True
|
||||
payment.save()
|
||||
return
|
||||
|
||||
return {
|
||||
"total_expired_payments": count,
|
||||
}
|
||||
|
||||
|
||||
@app.task
|
||||
def send_sms_task(
|
||||
user: str,
|
||||
mobile: str,
|
||||
amount: float,
|
||||
model_id: str,
|
||||
created_at: str,
|
||||
type: str = "TOPUP", # Default to TOPUP if not provided,
|
||||
):
|
||||
try:
|
||||
topup = Topup.objects.get(id=topup_id)
|
||||
topup.expiry_notification_sent = True
|
||||
topup.save()
|
||||
logger.info(f"Marked topup {topup_id} as notified.")
|
||||
except Topup.DoesNotExist:
|
||||
logger.error(f"Topup {topup_id} not found when trying to mark as notified.")
|
||||
dt = datetime.fromisoformat(created_at)
|
||||
formatted_date = dt.strftime("%d %b %Y, %I:%M %p")
|
||||
except Exception:
|
||||
formatted_date = created_at
|
||||
message: str = ""
|
||||
if type == "TOPUP":
|
||||
message = (
|
||||
f"Dear {user}, \n\nYour topup of {amount} MVR [created at {formatted_date}] has expired. "
|
||||
"Please make a new topup to update your wallet. \n\n- SAR Link"
|
||||
)
|
||||
elif type == "PAYMENT":
|
||||
message = f"Dear {user}, \n\nYour payment of {amount} MVR [created at {formatted_date}] has expired. \n\n- SAR Link"
|
||||
send_sms(mobile, message)
|
||||
logger.info(f"SMS sent to {mobile} for expired {type} of {amount} MVR.")
|
||||
|
||||
if type == "TOPUP":
|
||||
try:
|
||||
topup = Topup.objects.get(id=model_id)
|
||||
topup.expiry_notification_sent = True
|
||||
topup.save()
|
||||
logger.info(f"Marked topup {model_id} as notified.")
|
||||
except Topup.DoesNotExist:
|
||||
logger.error(
|
||||
f"Topup id: {model_id} not found when trying to mark as notified."
|
||||
)
|
||||
else:
|
||||
try:
|
||||
topup = Payment.objects.get(id=model_id)
|
||||
topup.expiry_notification_sent = True
|
||||
topup.save()
|
||||
logger.info(f"Marked payment {model_id} as notified.")
|
||||
except Payment.DoesNotExist:
|
||||
logger.error(
|
||||
f"Payment id: {model_id} not found when trying to mark as notified."
|
||||
)
|
||||
|
||||
+45
-1
@@ -3,7 +3,7 @@ from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
from django.contrib.auth import get_user_model
|
||||
from .models import Topup
|
||||
from .models import Topup, Payment
|
||||
from .serializers import TopupSerializer
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
@@ -205,3 +205,47 @@ class TopupTests(TestCase):
|
||||
response = self.client.delete(url, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(Topup.objects.count(), 0)
|
||||
|
||||
|
||||
class PaymentTests(TestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.real_user = User.objects.create_user(
|
||||
username="testuser",
|
||||
password="testpassword",
|
||||
first_name=REAL_USER_FIRST_NAME,
|
||||
last_name=REAL_USER_LAST_NAME,
|
||||
acc_no="7770000010629",
|
||||
is_admin=True,
|
||||
)
|
||||
self.user = User.objects.create_user(
|
||||
username="plskillme",
|
||||
password="modewasgayithink",
|
||||
first_name="mode",
|
||||
last_name="hussain",
|
||||
acc_no="1122334455",
|
||||
)
|
||||
self.admin_user = User.objects.create_superuser(
|
||||
username="adminuser",
|
||||
password="adminpassword",
|
||||
email="admin@example.com",
|
||||
first_name="Admin",
|
||||
last_name="User",
|
||||
acc_no="987654321",
|
||||
)
|
||||
self.client.force_authenticate(user=self.real_user)
|
||||
|
||||
def test_cancel_payment(self):
|
||||
payment = Payment.objects.create(
|
||||
amount=100.00,
|
||||
user=self.real_user,
|
||||
status="PENDING",
|
||||
number_of_months=1,
|
||||
paid=False,
|
||||
)
|
||||
url = reverse("cancel-payment", kwargs={"pk": payment.pk})
|
||||
response = self.client.patch(url, format="json")
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
payment.refresh_from_db()
|
||||
self.assertEqual(payment.status, "CANCELLED")
|
||||
self.assertFalse(payment.paid)
|
||||
|
||||
+4
-4
@@ -5,7 +5,7 @@ from .views import (
|
||||
VerifyPaymentView,
|
||||
PaymentDetailAPIView,
|
||||
UpdatePaymentAPIView,
|
||||
DeletePaymentView,
|
||||
CancelPaymentView,
|
||||
ListCreateTopupView,
|
||||
VerifyTopupPaymentAPIView,
|
||||
TopupDetailAPIView,
|
||||
@@ -21,9 +21,9 @@ urlpatterns = [
|
||||
name="update-payment",
|
||||
),
|
||||
path(
|
||||
"payment/<str:pk>/delete/",
|
||||
DeletePaymentView.as_view(),
|
||||
name="delete-payment",
|
||||
"payment/<str:pk>/cancel/",
|
||||
CancelPaymentView.as_view(),
|
||||
name="cancel-payment",
|
||||
),
|
||||
path(
|
||||
"payment/<str:pk>/verify/", VerifyPaymentView.as_view(), name="verify-payment"
|
||||
|
||||
+68
-37
@@ -28,6 +28,20 @@ PAYMENT_BASE_URL = env("PAYMENT_BASE_URL", default="") # type: ignore
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
ref: str
|
||||
sourceBank: str
|
||||
trxDate: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentVerificationResponse:
|
||||
message: str
|
||||
success: bool
|
||||
transaction: Optional[Transaction] = None
|
||||
|
||||
|
||||
class InsufficientFundsError(Exception):
|
||||
pass
|
||||
|
||||
@@ -52,7 +66,8 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
||||
number_of_months = data.get("number_of_months")
|
||||
device_ids = data.get("device_ids", [])
|
||||
print(amount, number_of_months, device_ids)
|
||||
|
||||
current_time = timezone.now()
|
||||
expires_at = current_time + timedelta(minutes=10)
|
||||
for device_id in device_ids:
|
||||
device = Device.objects.filter(id=device_id, user=user).first()
|
||||
print("DEVICE", device)
|
||||
@@ -77,6 +92,7 @@ class ListCreatePaymentView(StaffEditorPermissionMixin, generics.ListCreateAPIVi
|
||||
number_of_months=number_of_months,
|
||||
paid=data.get("paid", False),
|
||||
user=user,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
||||
# Connect devices to payment
|
||||
@@ -110,7 +126,7 @@ class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_update(serializer)
|
||||
devices.update(
|
||||
is_active=True, expiry_date=device_expire_date, has_a_pending_payment=False
|
||||
is_active=False, expiry_date=device_expire_date, has_a_pending_payment=False
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
@@ -145,7 +161,6 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
)
|
||||
|
||||
devices = payment.devices.all()
|
||||
payment_status = False
|
||||
if method == "WALLET":
|
||||
if user.wallet_balance < payment.amount: # type: ignore
|
||||
return Response(
|
||||
@@ -153,7 +168,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
else:
|
||||
payment_status = self.process_wallet_payment(
|
||||
self.process_wallet_payment(
|
||||
user,
|
||||
payment,
|
||||
)
|
||||
@@ -161,15 +176,12 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
data = {
|
||||
"benefName": f"{user.first_name} {user.last_name}", # type: ignore
|
||||
"accountNo": user.acc_no, # type: ignore
|
||||
"absAmount": payment.amount,
|
||||
"time": localtime(timezone.now() + timedelta(minutes=5)).strftime(
|
||||
"%Y-%m-%d %H:%M"
|
||||
),
|
||||
"absAmount": "{:.2f}".format(payment.amount),
|
||||
"time": localtime(payment.created_at).strftime("%Y-%m-%d %H:%M"),
|
||||
}
|
||||
payment_status = self.verify_transfer_payment(data, payment)
|
||||
payment_verification_response = self.verify_transfer_payment(data, payment)
|
||||
|
||||
if payment_status:
|
||||
# Update devices
|
||||
if payment_verification_response.success:
|
||||
expiry_date = timezone.now() + timedelta(days=30 * payment.number_of_months)
|
||||
devices.update(
|
||||
is_active=True,
|
||||
@@ -177,7 +189,9 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
has_a_pending_payment=False,
|
||||
registered=True,
|
||||
)
|
||||
# Need to add to omada if its a new device and not an existing device
|
||||
payment.status = "PAID"
|
||||
payment.save()
|
||||
# add to omada if its a new device and not an existing device
|
||||
device_list = []
|
||||
for device in devices:
|
||||
device_list.append(
|
||||
@@ -193,12 +207,22 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
device.save()
|
||||
|
||||
return Response(
|
||||
{"message": f"Payment verified successfully using [{method}]."},
|
||||
{
|
||||
"status": payment_verification_response.success,
|
||||
"message": payment_verification_response.message,
|
||||
"transaction": asdict(payment_verification_response.transaction)
|
||||
if payment_verification_response.transaction
|
||||
else None,
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"message": f"Payment verification FAILED using [{method}]."},
|
||||
{
|
||||
"status": payment_verification_response.success,
|
||||
"message": payment_verification_response.message
|
||||
or "Topup payment verification failed.",
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@@ -215,7 +239,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
user.save()
|
||||
return True
|
||||
|
||||
def verify_transfer_payment(self, data, payment):
|
||||
def verify_transfer_payment(self, data, payment) -> PaymentVerificationResponse:
|
||||
if not PAYMENT_BASE_URL:
|
||||
raise ValueError(
|
||||
"PAYMENT_BASE_URL is not set. Please set it in your environment variables."
|
||||
@@ -230,41 +254,62 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
response.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
logger.error(f"HTTPError: {e}")
|
||||
return False
|
||||
return PaymentVerificationResponse(
|
||||
message="Payment verification failed.", success=False, transaction=None
|
||||
)
|
||||
mib_resp = response.json()
|
||||
logger.info("MIB Verification Response ->", mib_resp)
|
||||
if not response.json().get("success"):
|
||||
return mib_resp["success"]
|
||||
return PaymentVerificationResponse(
|
||||
message=mib_resp["message"],
|
||||
success=mib_resp["success"],
|
||||
transaction=None,
|
||||
)
|
||||
else:
|
||||
payment.paid = True
|
||||
payment.paid_at = timezone.now()
|
||||
payment.method = "TRANSFER"
|
||||
payment.mib_reference = mib_resp["transaction"]["ref"] or ""
|
||||
payment.save()
|
||||
return True
|
||||
return PaymentVerificationResponse(
|
||||
message=mib_resp["message"],
|
||||
success=mib_resp["success"],
|
||||
transaction=Transaction(
|
||||
ref=payment.mib_reference,
|
||||
sourceBank=mib_resp["transaction"]["sourceBank"],
|
||||
trxDate=mib_resp["transaction"]["trxDate"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView):
|
||||
class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
queryset = Payment.objects.all()
|
||||
serializer_class = PaymentSerializer
|
||||
lookup_field = "pk"
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
def update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
user = request.user
|
||||
if instance.status == "CANCELLED":
|
||||
return Response(
|
||||
{"message": "Payment has already been cancelled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if instance.user != user and not user.is_superuser:
|
||||
return Response(
|
||||
{"message": "You are not authorized to delete this payment."},
|
||||
{"message": "You are not authorized to cancel this payment."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
if instance.paid:
|
||||
return Response(
|
||||
{"message": "Paid payments cannot be deleted."},
|
||||
{"message": "Paid payments cannot be cancelled."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
devices = instance.devices.all()
|
||||
instance.status = "CANCELLED"
|
||||
instance.save()
|
||||
devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False)
|
||||
return super().delete(request, *args, **kwargs)
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
|
||||
@@ -308,20 +353,6 @@ class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
|
||||
return queryset.filter(user=self.request.user)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
ref: str
|
||||
sourceBank: str
|
||||
trxDate: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class PaymentVerificationResponse:
|
||||
message: str
|
||||
success: bool
|
||||
transaction: Optional[Transaction] = None
|
||||
|
||||
|
||||
class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
|
||||
queryset = Topup.objects.all()
|
||||
serializer_class = TopupSerializer
|
||||
|
||||
@@ -10,6 +10,7 @@ class DeviceAdmin(admin.ModelAdmin):
|
||||
"user",
|
||||
"mac",
|
||||
"vendor",
|
||||
"expiry_date",
|
||||
"blocked_by",
|
||||
"name",
|
||||
"created_at",
|
||||
|
||||
+9
-10
@@ -31,8 +31,16 @@ class BlockDeviceSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
pending_payment_id = serializers.SerializerMethodField()
|
||||
user = serializers.SerializerMethodField()
|
||||
pending_payment_id = serializers.SerializerMethodField()
|
||||
|
||||
def get_pending_payment_id(self, obj):
|
||||
unpaid_payment = (
|
||||
Payment.objects.filter(devices=obj, paid=False)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
return unpaid_payment.id if unpaid_payment else None
|
||||
|
||||
def get_user(self, obj):
|
||||
user = obj.user
|
||||
@@ -45,15 +53,6 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
}
|
||||
return None
|
||||
|
||||
def get_pending_payment_id(self, obj):
|
||||
# Query the last unpaid payment for the device
|
||||
unpaid_payment = (
|
||||
Payment.objects.filter(devices=obj, paid=False)
|
||||
.order_by("-created_at")
|
||||
.first()
|
||||
)
|
||||
return unpaid_payment.id if unpaid_payment else None
|
||||
|
||||
class Meta: # type: ignore
|
||||
model = Device
|
||||
fields = "__all__"
|
||||
|
||||
Reference in New Issue
Block a user