From 6ec31023c79813b6e893ceeb6b00396dfbe868e9 Mon Sep 17 00:00:00 2001 From: i701 Date: Sat, 5 Jul 2025 14:33:10 +0500 Subject: [PATCH] =?UTF-8?q?feat(topup):=20add=20expiry=20notification=20ha?= =?UTF-8?q?ndling=20and=20SMS=20notification=20task=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0_add_expiry_notification_sent_to_topup.py | 17 ++++++ billing/models.py | 1 + billing/tasks.py | 59 +++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 billing/migrations/0010_add_expiry_notification_sent_to_topup.py create mode 100644 billing/tasks.py diff --git a/billing/migrations/0010_add_expiry_notification_sent_to_topup.py b/billing/migrations/0010_add_expiry_notification_sent_to_topup.py new file mode 100644 index 0000000..b8a3c3e --- /dev/null +++ b/billing/migrations/0010_add_expiry_notification_sent_to_topup.py @@ -0,0 +1,17 @@ +# Generated by Django 5.2 on 2025-07-05 09:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("billing", "0009_remove_topup_expired"), + ] + + operations = [ + migrations.AddField( + model_name="topup", + name="expiry_notification_sent", + field=models.BooleanField(default=False), + ), + ] diff --git a/billing/models.py b/billing/models.py index 8fee3d6..e73cbe0 100644 --- a/billing/models.py +++ b/billing/models.py @@ -54,6 +54,7 @@ class Topup(models.Model): paid_at = models.DateTimeField(null=True, blank=True) mib_reference = models.CharField(default="", null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) + expiry_notification_sent = models.BooleanField(default=False) created_at = models.DateTimeField(default=timezone.now) updated_at = models.DateTimeField(auto_now=True) diff --git a/billing/tasks.py b/billing/tasks.py new file mode 100644 index 0000000..e1f89da --- /dev/null +++ b/billing/tasks.py @@ -0,0 +1,59 @@ +import logging + +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 + +logger = logging.getLogger(__name__) + + +@app.periodic( + cron="*/30 * * * * *", periodic_id="notify_expired_topups", queue="heavy_tasks" +) # every 30 seconds +@app.task +def update_expired_topups(timestamp: int): + expired_topups_qs = Topup.objects.filter( + expires_at__lte=timezone.now(), expiry_notification_sent=False + ).select_related("user") + + with transaction.atomic(): + count = expired_topups_qs.count() + logger.info(f"Found {count} topups to expire.") + + for topup in expired_topups_qs: + if topup.user and topup.user.mobile and not topup.expiry_notification_sent: + send_sms_task.defer( + mobile=topup.user.mobile, + amount=topup.amount, + topup_id=str(topup.id), + ) + else: + # Mark as notified even if we can't send SMS (no mobile number) + topup.expiry_notification_sent = True + topup.save() + + return { + "total_expired_topups": count, + } + + +# Assuming you have a separate task for sending SMS if you go that route +@app.task +def send_sms_task(mobile: str, amount: float, topup_id: str): + message = ( + f"Dear {mobile}, \n\nYour topup of {amount} MVR 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.") + + # Mark the topup as notified after successful SMS sending + 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.")