Tenant-scoped fallback ticket model for accounts without PSA integration. Tracks customer-name, problem-statement, status lifecycle (open/walking/ resolved/escalated), and optional links to flow/proposal/ai_session/ assigned engineer + PSA promotion ID. Account-scoped RLS policy uses app.current_account_id session setting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
3.8 KiB
Python
80 lines
3.8 KiB
Python
"""create_internal_tickets
|
|
|
|
Revision ID: a1e6a018af02
|
|
Revises: ff6fe5895ea2
|
|
Create Date: 2026-05-28 16:29:32.624317
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = 'a1e6a018af02'
|
|
down_revision: Union[str, None] = 'ff6fe5895ea2'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
_NULL_UUID = "00000000-0000-0000-0000-000000000000"
|
|
_CURRENT_ACCOUNT = (
|
|
f"COALESCE(NULLIF(current_setting('app.current_account_id', TRUE), ''), "
|
|
f"'{_NULL_UUID}')::uuid"
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'internal_tickets',
|
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column('account_id', postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column('created_by_user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column('customer_name', sa.String(120), nullable=True),
|
|
sa.Column('customer_contact', sa.String(200), nullable=True),
|
|
sa.Column('problem_statement', sa.Text(), nullable=False),
|
|
sa.Column('status', sa.String(30), nullable=False, server_default='open'),
|
|
sa.Column('flow_id', postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column('flow_proposal_id', postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column('ai_session_id', postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column('assigned_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column('resolution_notes', sa.Text(), nullable=True),
|
|
sa.Column('psa_promoted_ticket_id', sa.String(64), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
|
|
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.ForeignKeyConstraint(['account_id'], ['accounts.id'], ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ondelete='RESTRICT'),
|
|
sa.ForeignKeyConstraint(['flow_id'], ['trees.id'], ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['flow_proposal_id'], ['flow_proposals.id'], ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['ai_session_id'], ['ai_sessions.id'], ondelete='SET NULL'),
|
|
sa.ForeignKeyConstraint(['assigned_user_id'], ['users.id'], ondelete='SET NULL'),
|
|
sa.CheckConstraint(
|
|
"status IN ('open', 'walking', 'resolved', 'escalated')",
|
|
name='ck_internal_tickets_status',
|
|
),
|
|
)
|
|
op.create_index('ix_internal_tickets_account_id', 'internal_tickets', ['account_id'])
|
|
op.create_index('ix_internal_tickets_status', 'internal_tickets', ['status'])
|
|
op.create_index('ix_internal_tickets_assigned_user_id', 'internal_tickets', ['assigned_user_id'])
|
|
|
|
op.execute("ALTER TABLE internal_tickets ENABLE ROW LEVEL SECURITY")
|
|
op.execute("ALTER TABLE internal_tickets FORCE ROW LEVEL SECURITY")
|
|
op.execute(f"""
|
|
CREATE POLICY tenant_isolation ON internal_tickets
|
|
USING (account_id = {_CURRENT_ACCOUNT})
|
|
WITH CHECK (account_id = {_CURRENT_ACCOUNT})
|
|
""")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP POLICY IF EXISTS tenant_isolation ON internal_tickets")
|
|
op.execute("ALTER TABLE internal_tickets DISABLE ROW LEVEL SECURITY")
|
|
op.execute("ALTER TABLE internal_tickets NO FORCE ROW LEVEL SECURITY")
|
|
op.drop_index('ix_internal_tickets_assigned_user_id', 'internal_tickets')
|
|
op.drop_index('ix_internal_tickets_status', 'internal_tickets')
|
|
op.drop_index('ix_internal_tickets_account_id', 'internal_tickets')
|
|
op.drop_table('internal_tickets')
|