feat(tickets): deliver canonical operational lifecycle

This commit is contained in:
2026-08-22 11:41:45 +02:00
parent b4bb52d8b8
commit 7cbf3bed96
25 changed files with 4742 additions and 106 deletions
+123
View File
@@ -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"]