Files
customerTicket/apps/notifications/services.py
T

72 lines
4.5 KiB
Python

from datetime import timedelta
from uuid import uuid4
from django.utils import timezone
from .models import NotificationJob,NotificationPolicy
def enqueue(channel,event_type,ticket,dedupe_key,payload=None,customer=None,staff_user=None):
return NotificationJob.objects.get_or_create(dedupe_key=dedupe_key,defaults={"channel":channel,"event_type":event_type,"ticket":ticket,"payload":payload or {},"customer":customer,"staff_user":staff_user,"next_attempt_at":timezone.now()})[0]
def enqueue_unit_supervisor_ticket_sms(ticket):
"""Queue one concise new-ticket SMS for each configured unit supervisor."""
unit = ticket.owning_unit
if not unit:
return []
return [
enqueue(
NotificationJob.Channel.SMS,
"UNIT_SUPERVISOR_NEW_TICKET",
ticket,
f"ticket:{ticket.pk}:unit-supervisor:{mobile}",
payload={"recipient_mobile": mobile},
)
for mobile in unit.supervisor_mobiles
]
def staff_reply_policy():
return NotificationPolicy.objects.get_or_create(key=NotificationPolicy.Key.STAFF_REPLY_SMS,defaults={"quiet_period_seconds":300})[0]
def customer_reply_bale_policy():
return NotificationPolicy.objects.get_or_create(key=NotificationPolicy.Key.CUSTOMER_REPLY_BALE,defaults={"quiet_period_seconds":600})[0]
def cancel_staff_reply_sms(ticket,reason):
return NotificationJob.objects.filter(ticket=ticket,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED]).update(status=NotificationJob.Status.CANCELLED,last_error=reason)
def schedule_staff_reply_sms(ticket,message):
"""Debounce a chat burst into one SMS; caller already locks the ticket."""
policy=staff_reply_policy()
if not policy.is_active:
cancel_staff_reply_sms(ticket,"policy_disabled")
return None,False
due=timezone.now()+timedelta(seconds=policy.quiet_period_seconds)
job=NotificationJob.objects.filter(ticket=ticket,customer=ticket.customer,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED]).order_by("-created_at").first()
payload={"latest_message_id":message.pk}
if job:
job.payload=payload; job.next_attempt_at=due; job.status=NotificationJob.Status.PENDING; job.attempts=0; job.last_error=""
job.save(update_fields=["payload","next_attempt_at","status","attempts","last_error"])
return job,False
return NotificationJob.objects.create(channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",ticket=ticket,customer=ticket.customer,payload=payload,next_attempt_at=due,dedupe_key=f"ticket:{ticket.pk}:staff-reply:{uuid4().hex}"),True
def cancel_staff_reply_sms_if_read(ticket,customer):
jobs=NotificationJob.objects.filter(ticket=ticket,customer=customer,channel=NotificationJob.Channel.SMS,event_type="STAFF_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED])
cancelled=0
for job in jobs:
message_id=job.payload.get("latest_message_id")
if message_id and customer.message_read_receipts.filter(message_id=message_id).exists():
job.status=NotificationJob.Status.CANCELLED; job.last_error="customer_read"; job.save(update_fields=["status","last_error"]); cancelled+=1
return cancelled
def schedule_customer_reply_bale(ticket,message):
"""Debounce consecutive customer chat messages into one Bale group alert."""
if not ticket.owning_unit_id or not ticket.owning_unit.bale_chat_id:
return None,False
policy=customer_reply_bale_policy()
if not policy.is_active: return None,False
due=timezone.now()+timedelta(seconds=policy.quiet_period_seconds)
job=NotificationJob.objects.filter(ticket=ticket,channel=NotificationJob.Channel.BALE,event_type="CUSTOMER_TICKET_REPLY",status__in=[NotificationJob.Status.PENDING,NotificationJob.Status.FAILED]).order_by("-created_at").first()
count=int((job.payload if job else {}).get("message_count",0))+1
payload={"message_id":message.pk,"message_count":count}
if job:
job.payload=payload; job.next_attempt_at=due; job.status=NotificationJob.Status.PENDING; job.attempts=0; job.last_error=""
job.save(update_fields=["payload","next_attempt_at","status","attempts","last_error"])
return job,False
return NotificationJob.objects.create(channel=NotificationJob.Channel.BALE,event_type="CUSTOMER_TICKET_REPLY",ticket=ticket,customer=ticket.customer,payload=payload,next_attempt_at=due,dedupe_key=f"ticket:{ticket.pk}:customer-reply-bale:{uuid4().hex}"),True