feat(tickets): deliver canonical operational lifecycle
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from govoplan_core.db.base import Base, TimestampMixin
|
||||
|
||||
|
||||
def new_uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class Ticket(Base, TimestampMixin):
|
||||
__tablename__ = "tickets"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "ticket_number", name="uq_ticket_number"),
|
||||
Index("ix_ticket_queue", "tenant_id", "queue_ref", "status", "priority"),
|
||||
Index("ix_ticket_catalog", "tenant_id", "status", "updated_at"),
|
||||
Index("ix_ticket_service_target", "tenant_id", "service_target_at", "status"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(255), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
ticket_number: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
revision: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
ticket_type: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
|
||||
priority: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
|
||||
status: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
|
||||
title: Mapped[str] = mapped_column(String(500), nullable=False)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
visibility: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
|
||||
queue_ref: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
assignee: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
reporter: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
requester: Mapped[dict[str, Any] | None] = mapped_column(JSON, nullable=True)
|
||||
participants: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False, default=list)
|
||||
links: Mapped[list[dict[str, Any]]] = mapped_column(JSON, nullable=False, default=list)
|
||||
metadata_payload: Mapped[dict[str, Any]] = mapped_column("metadata", JSON, nullable=False, default=dict)
|
||||
search_text: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
received_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
recorded_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
change_reason: Mapped[str] = mapped_column(String(1_000), nullable=False)
|
||||
service_target_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
resolved_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
resolution_summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
created_by: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
updated_by: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
|
||||
|
||||
class TicketHistory(Base, TimestampMixin):
|
||||
__tablename__ = "ticket_history"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "ticket_id", "revision", name="uq_ticket_history_revision"),
|
||||
UniqueConstraint("tenant_id", "idempotency_key", name="uq_ticket_history_idempotency"),
|
||||
Index("ix_ticket_history_timeline", "tenant_id", "ticket_id", "occurred_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
ticket_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("tickets.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
revision: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
event_type: Mapped[str] = mapped_column(String(120), nullable=False, index=True)
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
actor_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
reason: Mapped[str] = mapped_column(String(1_000), nullable=False)
|
||||
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
request_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
snapshot: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False)
|
||||
details: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
|
||||
|
||||
|
||||
class TicketComment(Base, TimestampMixin):
|
||||
__tablename__ = "ticket_comments"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "comment_id", name="uq_ticket_comment"),
|
||||
Index("ix_ticket_comment_timeline", "tenant_id", "ticket_id", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
ticket_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("tickets.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
comment_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
ticket_revision: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
visibility: Mapped[str] = mapped_column(String(40), nullable=False, index=True)
|
||||
body: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
created_by: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
|
||||
|
||||
class TicketEscalation(Base, TimestampMixin):
|
||||
__tablename__ = "ticket_escalations"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("tenant_id", "ticket_id", "idempotency_key", name="uq_ticket_escalation_replay"),
|
||||
UniqueConstraint("tenant_id", "provider_id", "case_id", name="uq_ticket_case_link"),
|
||||
Index("ix_ticket_escalation_timeline", "tenant_id", "ticket_id", "occurred_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
tenant_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
ticket_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("tickets.id", ondelete="RESTRICT"), nullable=False, index=True
|
||||
)
|
||||
provider_id: Mapped[str] = mapped_column(String(200), nullable=False, index=True)
|
||||
idempotency_key: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
request_sha256: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
|
||||
actor_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||
case_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True)
|
||||
case_number: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
case_url: Mapped[str] = mapped_column(String(1_500), nullable=False)
|
||||
handoff_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
outcome: Mapped[dict[str, Any]] = mapped_column(JSON, nullable=False, default=dict)
|
||||
|
||||
|
||||
__all__ = ["Ticket", "TicketComment", "TicketEscalation", "TicketHistory"]
|
||||
Reference in New Issue
Block a user