"""support_tickets + support_ticket_messages — the IT help-desk (求助域). Revision ID: 301 Revises: 300 Create Date: 2026-06-17 PR2 of the employee↔IT communication theme: the free-form "ask IT for help" surface (closing the template-only dead-end) with a full ticket lifecycle and a message timeline. All person/ticket FKs are built with the parent's charset/collation (``id_type_for`` / ``table_kw_for``) so the inline FKs can't trip error 3780 on prod TiDB. """ from __future__ import annotations import sqlalchemy as sa from alembic import op from app.core.migration_helpers import id_type_for, table_kw_for revision = "301" down_revision = "300" branch_labels = None depends_on = None TICKETS = "support_tickets" MESSAGES = "support_ticket_messages" def _table_exists(bind, name: str) -> bool: return bind.execute( sa.text( "SELECT 1 FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :n" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() persons_id = id_type_for("identity_persons") kw = table_kw_for("identity_persons") if not _table_exists(bind, TICKETS): op.create_table( TICKETS, sa.Column("id", persons_id, primary_key=True), sa.Column("ticket_number", sa.String(24), nullable=False), sa.Column("requester_id", persons_id, nullable=False), sa.Column("title", sa.String(255), nullable=False), sa.Column("body", sa.Text(), nullable=False), sa.Column("category", sa.String(32), nullable=False, server_default="other"), sa.Column("priority", sa.String(16), nullable=False, server_default="normal"), sa.Column("status", sa.String(24), nullable=False, server_default="open"), sa.Column("source", sa.String(16), nullable=False, server_default="web"), sa.Column("assignee_id", persons_id, nullable=True), sa.Column("resolution", sa.Text(), nullable=True), sa.Column("feishu_message_id", sa.String(64), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("first_response_at", sa.DateTime(timezone=True), nullable=True), sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True), sa.Column("closed_at", sa.DateTime(timezone=True), nullable=True), sa.ForeignKeyConstraint(["requester_id"], ["identity_persons.id"], name="fk_support_tickets_requester"), sa.ForeignKeyConstraint(["assignee_id"], ["identity_persons.id"], name="fk_support_tickets_assignee"), sa.UniqueConstraint("ticket_number", name="uq_support_tickets_number"), **kw, ) op.create_index("ix_support_tickets_requester", TICKETS, ["requester_id", "status"]) op.create_index("ix_support_tickets_assignee", TICKETS, ["assignee_id", "status"]) op.create_index("ix_support_tickets_status_created", TICKETS, ["status", "created_at"]) if not _table_exists(bind, MESSAGES): op.create_table( MESSAGES, sa.Column("id", persons_id, primary_key=True), sa.Column("ticket_id", persons_id, nullable=False), sa.Column("author_id", persons_id, nullable=True), sa.Column("kind", sa.String(16), nullable=False, server_default="comment"), sa.Column("body", sa.Text(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.ForeignKeyConstraint(["ticket_id"], [f"{TICKETS}.id"], name="fk_support_ticket_messages_ticket"), sa.ForeignKeyConstraint(["author_id"], ["identity_persons.id"], name="fk_support_ticket_messages_author"), **kw, ) op.create_index( "ix_support_ticket_messages_ticket", MESSAGES, ["ticket_id", "created_at"], ) def downgrade() -> None: op.drop_table(MESSAGES) op.drop_table(TICKETS)