84 lines
5.1 KiB
Python
84 lines
5.1 KiB
Python
from datetime import timedelta
|
|
from uuid import uuid4
|
|
from django.utils import timezone
|
|
from apps.core.models import VoipConfiguration
|
|
from .models import AnnouncementJob,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 schedule_announcement(message):
|
|
"""The ticket transaction also commits this one-message reminder."""
|
|
delay = VoipConfiguration.load().announcement_delay_minutes
|
|
return AnnouncementJob.objects.get_or_create(
|
|
message=message,
|
|
defaults={
|
|
"next_attempt_at": message.created_at + timedelta(minutes=delay),
|
|
"idempotency_key": f"staff-message-{message.public_id.hex}",
|
|
},
|
|
)[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.RETRYING,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.RETRYING,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
|