"""Internal ticket model. Fallback ticket table for L1 intake when the account has no PSA integration. Tracks the customer-facing problem, resolution lifecycle, and optional links to a flow, flow proposal, AI session, and assigned engineer. """ import uuid from datetime import datetime, timezone from typing import Optional, TYPE_CHECKING from sqlalchemy import String, Text, DateTime, ForeignKey, CheckConstraint from sqlalchemy import text as sa_text from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID from app.core.database import Base if TYPE_CHECKING: from app.models.account import Account from app.models.user import User from app.models.tree import Tree from app.models.flow_proposal import FlowProposal from app.models.ai_session import AISession class InternalTicket(Base): """A fallback support ticket for accounts without a PSA integration. status lifecycle: - open: Submitted, not yet picked up. - walking: L1 technician is actively walking the flow. - resolved: Issue resolved; resolution_notes captured. - escalated: Could not resolve; requires higher-tier intervention. """ __tablename__ = "internal_tickets" __table_args__ = ( CheckConstraint( "status IN ('open', 'walking', 'resolved', 'escalated')", name="ck_internal_tickets_status", ), ) id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) account_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("accounts.id", ondelete="CASCADE"), nullable=False, index=True, ) created_by_user_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="RESTRICT"), nullable=False, ) # ── Customer info ── customer_name: Mapped[Optional[str]] = mapped_column(String(120), nullable=True) customer_contact: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) problem_statement: Mapped[str] = mapped_column(Text(), nullable=False) # ── Lifecycle ── status: Mapped[str] = mapped_column( String(30), nullable=False, server_default=sa_text("'open'"), index=True, ) # ── Optional links ── flow_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("trees.id", ondelete="SET NULL"), nullable=True, ) flow_proposal_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("flow_proposals.id", ondelete="SET NULL"), nullable=True, ) ai_session_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("ai_sessions.id", ondelete="SET NULL"), nullable=True, ) assigned_user_id: Mapped[Optional[uuid.UUID]] = mapped_column( UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True, ) # ── Resolution ── resolution_notes: Mapped[Optional[str]] = mapped_column(Text(), nullable=True) psa_promoted_ticket_id: Mapped[Optional[str]] = mapped_column( String(64), nullable=True, comment="External PSA ticket ID when this ticket is promoted to a PSA system", ) # ── Timestamps ── created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) ) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), ) resolved_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True, ) # ── Relationships ── account: Mapped["Account"] = relationship("Account") created_by: Mapped["User"] = relationship("User", foreign_keys=[created_by_user_id]) assigned_user: Mapped[Optional["User"]] = relationship("User", foreign_keys=[assigned_user_id]) flow: Mapped[Optional["Tree"]] = relationship("Tree") flow_proposal: Mapped[Optional["FlowProposal"]] = relationship("FlowProposal") ai_session: Mapped[Optional["AISession"]] = relationship("AISession")