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 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.""" 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