feat(billing): implement notification for expired payments and refactor SMS handling

This commit is contained in:
2025-07-09 20:13:42 +05:00
parent 1644bd47b9
commit 911d01b8e3

View File

@ -4,16 +4,22 @@ 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
from enum import Enum
logger = logging.getLogger(__name__)
class NotificationType(Enum):
TOPUP = "TOPUP"
PAYMENT = "PAYMENT"
@app.periodic(
cron="*/1 * * * * *", periodic_id="notify_expired_topups", queue="heavy_tasks"
) # every 1 minute
)
@app.task
def update_expired_topups(timestamp: int):
expired_topups_qs = Topup.objects.filter(
@ -33,8 +39,9 @@ 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=NotificationType.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
@ -43,7 +50,6 @@ def update_expired_topups(timestamp: int):
topup.expiry_notification_sent = True
topup.save()
else:
# Mark as notified even if we can't send SMS (no mobile number)
topup.expiry_notification_sent = True
topup.save()
return
@ -53,29 +59,94 @@ def update_expired_topups(timestamp: int):
}
@app.periodic(
cron="*/1 * * * * *", periodic_id="notify_expired_payments", queue="heavy_tasks"
)
@app.task
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}
with transaction.atomic():
count = expired_payments_qs.count()
logger.info(f"Found {count} payments to expire.")
for payment in expired_payments_qs:
if (
payment.user
and payment.user.mobile
and not payment.expiry_notification_sent
):
send_sms_task.defer(
mobile=payment.user.mobile,
type=NotificationType.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, topup_id: str, created_at: str
user: str,
mobile: str,
amount: float,
model_id: str,
created_at: str,
type: NotificationType = NotificationType.TOPUP,
):
# Parse the ISO formatted date string
try:
dt = datetime.fromisoformat(created_at)
formatted_date = dt.strftime("%d %b %Y, %I:%M %p")
except Exception:
formatted_date = created_at # fallback to original if parsing fails
formatted_date = created_at
message: str = ""
if type == NotificationType.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 == NotificationType.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 topup of {amount} MVR.")
logger.info(f"SMS sent to {mobile} for expired {type.value} of {amount} MVR.")
# Mark the topup as notified after successful SMS sending
if type == NotificationType.TOPUP:
try:
topup = Topup.objects.get(id=topup_id)
topup = Topup.objects.get(id=model_id)
topup.expiry_notification_sent = True
topup.save()
logger.info(f"Marked topup {topup_id} as notified.")
logger.info(f"Marked topup {model_id} as notified.")
except Topup.DoesNotExist:
logger.error(f"Topup {topup_id} not found when trying to mark as notified.")
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."
)