import uuid from datetime import timedelta from django.conf import settings from django.db import models from django.utils import timezone def default_ticket_draft_expiry(): return timezone.now() + timedelta(hours=24) class TicketStatus(models.TextChoices): PENDING_INFO = "pending_info", "Pending info" PENDING = "pending", "Pending" IN_PROGRESS = "in_progress", "In progress" PENDING_CONFIRM = "pending_confirm", "Pending confirm" RESOLVED = "resolved", "Resolved" CLOSED = "closed", "Closed" class TicketPriority(models.TextChoices): LOW = "low", "Low" MEDIUM = "medium", "Medium" HIGH = "high", "High" URGENT = "urgent", "Urgent" class TicketCategory(models.Model): parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL) name = models.CharField(max_length=120) code = models.CharField(max_length=80, unique=True) description = models.TextField(blank=True) enabled = models.BooleanField(default=True) sort_order = models.PositiveIntegerField(default=0) class Meta: ordering = ["sort_order", "name"] def __str__(self) -> str: return self.name class Ticket(models.Model): ticket_no = models.CharField(max_length=32, unique=True) title = models.CharField(max_length=200) description = models.TextField() category = models.ForeignKey(TicketCategory, null=True, blank=True, on_delete=models.SET_NULL) status = models.CharField( max_length=32, choices=TicketStatus.choices, default=TicketStatus.PENDING, ) priority = models.CharField( max_length=20, choices=TicketPriority.choices, default=TicketPriority.MEDIUM, ) problem_performance = models.TextField(blank=True) problem_object = models.CharField(max_length=160, blank=True) submitter_name = models.CharField(max_length=80, blank=True) submitter_org = models.CharField(max_length=160, blank=True) ai_solution = models.TextField(blank=True) resolution_summary = models.TextField(blank=True) is_resolved = models.BooleanField(default=False) replied_to_customer = models.BooleanField(default=False) impact_scope = models.CharField(max_length=80, blank=True) environment = models.CharField(max_length=80, blank=True) related_system = models.CharField(max_length=120, blank=True) requester = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="requested_tickets", ) assignee = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="assigned_tickets", ) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="created_tickets", ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) resolved_at = models.DateTimeField(null=True, blank=True) closed_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-created_at"] indexes = [ models.Index(fields=["ticket_no"]), models.Index(fields=["status", "priority"]), models.Index(fields=["created_at"]), ] def __str__(self) -> str: return f"{self.ticket_no} {self.title}" class TicketComment(models.Model): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="comments") content = models.TextField() comment_type = models.CharField(max_length=40, default="user") visibility = models.CharField(max_length=40, default="internal") created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, ) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at"] class TicketAttachment(models.Model): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="attachments") file_name = models.CharField(max_length=255) file_type = models.CharField(max_length=80, blank=True) mime_type = models.CharField(max_length=120, blank=True) file_size = models.PositiveBigIntegerField(default=0) storage_path = models.CharField(max_length=500) ocr_text = models.TextField(blank=True) parsed_text = models.TextField(blank=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, ) created_at = models.DateTimeField(auto_now_add=True) class TicketDraft(models.Model): public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="ticket_drafts", ) status = models.CharField(max_length=24, default="editing") revision = models.PositiveIntegerField(default=1) source_text = models.TextField(blank=True) current_fields = models.JSONField(default=dict) last_ai_fields = models.JSONField(default=dict) missing_fields = models.JSONField(default=list) questions = models.JSONField(default=list) guided_rounds = models.PositiveSmallIntegerField(default=0) latest_agent_run = models.ForeignKey( "agents.AgentRun", null=True, blank=True, on_delete=models.SET_NULL, related_name="ticket_drafts", ) latest_artifact = models.ForeignKey( "agents.AgentArtifact", null=True, blank=True, on_delete=models.SET_NULL, related_name="ticket_drafts", ) confirmed_ticket = models.OneToOneField( Ticket, null=True, blank=True, on_delete=models.PROTECT, related_name="source_draft", ) decision = models.CharField(max_length=24, blank=True) error_code = models.CharField(max_length=80, blank=True) error_message = models.TextField(blank=True) expires_at = models.DateTimeField(default=default_ticket_draft_expiry) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) confirmed_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ["-updated_at"] indexes = [ models.Index(fields=["created_by", "status", "updated_at"]), models.Index(fields=["status", "expires_at"]), ] class TicketDraftMessage(models.Model): draft = models.ForeignKey(TicketDraft, on_delete=models.CASCADE, related_name="messages") role = models.CharField(max_length=24) content = models.TextField() round_number = models.PositiveSmallIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at"] class TicketDraftImage(models.Model): public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) draft = models.ForeignKey(TicketDraft, on_delete=models.CASCADE, related_name="images") file_name = models.CharField(max_length=255) mime_type = models.CharField(max_length=120) file_size = models.PositiveBigIntegerField() sha256 = models.CharField(max_length=64) storage_path = models.CharField(max_length=500) status = models.CharField(max_length=24, default="temporary") confirmed_attachment = models.OneToOneField( TicketAttachment, null=True, blank=True, on_delete=models.SET_NULL, related_name="source_draft_image", ) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at"] class TicketEvent(models.Model): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name="events") event_type = models.CharField(max_length=80) from_value = models.JSONField(default=dict, blank=True) to_value = models.JSONField(default=dict, blank=True) operator = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, ) remark = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ["created_at"]