This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
"""initial support schema
|
||||
|
||||
Revision ID: 0001_initial_support_schema
|
||||
Revises:
|
||||
Create Date: 2026-07-08 00:00:00
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0001_initial_support_schema"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
support_status_codes = sa.table(
|
||||
"support_status_codes",
|
||||
sa.column("code", sa.String(length=20)),
|
||||
sa.column("name", sa.String(length=50)),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
)
|
||||
|
||||
support_category_codes = sa.table(
|
||||
"support_category_codes",
|
||||
sa.column("code", sa.String(length=20)),
|
||||
sa.column("name", sa.String(length=50)),
|
||||
sa.column("sort_order", sa.Integer()),
|
||||
)
|
||||
|
||||
service_types = sa.table(
|
||||
"service_types",
|
||||
sa.column("id", sa.BigInteger()),
|
||||
sa.column("service_code", sa.String(length=50)),
|
||||
sa.column("service_name", sa.String(length=100)),
|
||||
sa.column("description", sa.Text()),
|
||||
sa.column("is_active", sa.Boolean()),
|
||||
)
|
||||
|
||||
workspaces = sa.table(
|
||||
"workspaces",
|
||||
sa.column("workspace_type", sa.String(length=20)),
|
||||
sa.column("software_app_id", sa.BigInteger()),
|
||||
sa.column("service_type_id", sa.BigInteger()),
|
||||
sa.column("workspace_code", sa.String(length=50)),
|
||||
sa.column("workspace_name", sa.String(length=100)),
|
||||
sa.column("is_active", sa.Boolean()),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"software_apps",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("app_code", sa.String(length=50), nullable=False),
|
||||
sa.Column("app_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("app_code", name="uq_software_apps_app_code"),
|
||||
sa.UniqueConstraint("app_name", name="uq_software_apps_app_name"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"service_types",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("service_code", sa.String(length=50), nullable=False),
|
||||
sa.Column("service_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("service_code", name="uq_service_types_service_code"),
|
||||
sa.UniqueConstraint("service_name", name="uq_service_types_service_name"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"workspaces",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("workspace_type", sa.String(length=20), nullable=False),
|
||||
sa.Column("software_app_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("service_type_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("workspace_code", sa.String(length=50), nullable=False),
|
||||
sa.Column("workspace_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.CheckConstraint(
|
||||
"(workspace_type = 'SOFTWARE_APP' AND software_app_id IS NOT NULL AND service_type_id IS NULL) OR "
|
||||
"(workspace_type = 'INTRANET_SERVICE' AND service_type_id IS NOT NULL AND software_app_id IS NULL)",
|
||||
name="chk_workspaces_scope",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["service_type_id"], ["service_types.id"], name="fk_workspaces_service_type"),
|
||||
sa.ForeignKeyConstraint(["software_app_id"], ["software_apps.id"], name="fk_workspaces_software_app"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("workspace_code", name="uq_workspaces_workspace_code"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"user_workspace_access",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("user_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("workspace_role", sa.String(length=30), nullable=False, server_default="USER"),
|
||||
sa.Column("can_read", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("can_write", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("can_manage", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("can_approve", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("page_scope", sa.String(length=50), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_user_workspace_access_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"user_id",
|
||||
"tenant_id",
|
||||
"workspace_id",
|
||||
name="uq_user_workspace_access_user_tenant_workspace",
|
||||
),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"workspace_channel_mappings",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("abc_channel_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("abc_channel_key", sa.String(length=100), nullable=True),
|
||||
sa.Column("form_template_version", sa.String(length=30), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_workspace_channel_mappings_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("workspace_id", "abc_channel_id", name="uq_workspace_channel_mappings_workspace_channel"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"workspace_field_mappings",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("abc_channel_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("abc_field_key", sa.String(length=100), nullable=False),
|
||||
sa.Column("local_field_code", sa.String(length=100), nullable=False),
|
||||
sa.Column("field_label", sa.String(length=100), nullable=False),
|
||||
sa.Column("field_type", sa.String(length=30), nullable=False),
|
||||
sa.Column("is_required", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"),
|
||||
sa.Column("validation_rule", sa.JSON(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_workspace_field_mappings_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"workspace_id",
|
||||
"abc_channel_id",
|
||||
"abc_field_key",
|
||||
name="uq_workspace_field_mappings_workspace_channel_field",
|
||||
),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"support_status_codes",
|
||||
sa.Column("code", sa.String(length=20), nullable=False),
|
||||
sa.Column("name", sa.String(length=50), nullable=False),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("code"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"support_category_codes",
|
||||
sa.Column("code", sa.String(length=20), nullable=False),
|
||||
sa.Column("name", sa.String(length=50), nullable=False),
|
||||
sa.Column("sort_order", sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint("code"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"support_tickets",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("requester_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("requester_tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("ticket_type", sa.String(length=30), nullable=False),
|
||||
sa.Column("source_system", sa.String(length=30), nullable=False, server_default="ABC"),
|
||||
sa.Column("title", sa.String(length=255), nullable=False),
|
||||
sa.Column("category_code", sa.String(length=20), nullable=True),
|
||||
sa.Column("status_code", sa.String(length=20), nullable=False),
|
||||
sa.Column("is_secret", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("requires_approval", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("current_assignee_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("current_assignee_tenant_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("requested_start_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column("requested_end_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column("priority", sa.String(length=20), nullable=False, server_default="NORMAL"),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(),
|
||||
nullable=False,
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(["category_code"], ["support_category_codes.code"], name="fk_support_tickets_category_code"),
|
||||
sa.ForeignKeyConstraint(["status_code"], ["support_status_codes.code"], name="fk_support_tickets_status_code"),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_support_tickets_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"abc_feedback_mappings",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("ticket_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("abc_channel_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("abc_feedback_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("abc_feedback_url", sa.Text(), nullable=True),
|
||||
sa.Column("sync_status", sa.String(length=20), nullable=False, server_default="SYNCED"),
|
||||
sa.Column("last_synced_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["ticket_id"], ["support_tickets.id"], name="fk_abc_feedback_mappings_ticket"),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_abc_feedback_mappings_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("abc_feedback_id", name="uq_abc_feedback_mappings_feedback_id"),
|
||||
sa.UniqueConstraint("ticket_id", name="uq_abc_feedback_mappings_ticket_id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"request_approvals",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("ticket_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("approver_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("approver_tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("approval_status", sa.String(length=20), nullable=False),
|
||||
sa.Column("comment", sa.Text(), nullable=True),
|
||||
sa.Column("approved_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["ticket_id"], ["support_tickets.id"], name="fk_request_approvals_ticket"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
support_status_codes,
|
||||
[
|
||||
{"code": "RECEIVED", "name": "접수", "sort_order": 10},
|
||||
{"code": "PENDING_APPROVAL", "name": "승인 대기", "sort_order": 20},
|
||||
{"code": "APPROVED", "name": "승인 완료", "sort_order": 30},
|
||||
{"code": "REJECTED", "name": "반려", "sort_order": 40},
|
||||
{"code": "IN_PROGRESS", "name": "처리 중", "sort_order": 50},
|
||||
{"code": "RESOLVED", "name": "처리 완료", "sort_order": 60},
|
||||
{"code": "CLOSED", "name": "종료", "sort_order": 70},
|
||||
],
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
support_category_codes,
|
||||
[
|
||||
{"code": "SUPPLIES", "name": "물품 신청", "sort_order": 10},
|
||||
{"code": "BOOK", "name": "도서 신청", "sort_order": 20},
|
||||
{"code": "VEHICLE", "name": "차량 신청", "sort_order": 30},
|
||||
{"code": "EQUIPMENT", "name": "비품 대여", "sort_order": 40},
|
||||
{"code": "GENERAL_QNA", "name": "일반 문의", "sort_order": 50},
|
||||
],
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
service_types,
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"service_code": "SUPPLIES_REQUEST",
|
||||
"service_name": "물품 신청",
|
||||
"description": "인트라넷 물품 신청 서비스",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"service_code": "BOOK_REQUEST",
|
||||
"service_name": "도서 신청",
|
||||
"description": "인트라넷 도서 신청 서비스",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"service_code": "VEHICLE_REQUEST",
|
||||
"service_name": "출장 차량 신청",
|
||||
"description": "인트라넷 차량 배차 신청 서비스",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"service_code": "EQUIPMENT_RENTAL",
|
||||
"service_name": "비품 대여",
|
||||
"description": "인트라넷 비품 대여 서비스",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"service_code": "GENERAL_QNA",
|
||||
"service_name": "사내 일반 문의",
|
||||
"description": "인트라넷 일반 문의 서비스",
|
||||
"is_active": True,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
op.bulk_insert(
|
||||
workspaces,
|
||||
[
|
||||
{
|
||||
"workspace_type": "INTRANET_SERVICE",
|
||||
"software_app_id": None,
|
||||
"service_type_id": 1,
|
||||
"workspace_code": "INTRA_SUPPLIES_REQUEST",
|
||||
"workspace_name": "인트라넷 물품 신청",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"workspace_type": "INTRANET_SERVICE",
|
||||
"software_app_id": None,
|
||||
"service_type_id": 2,
|
||||
"workspace_code": "INTRA_BOOK_REQUEST",
|
||||
"workspace_name": "인트라넷 도서 신청",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"workspace_type": "INTRANET_SERVICE",
|
||||
"software_app_id": None,
|
||||
"service_type_id": 3,
|
||||
"workspace_code": "INTRA_VEHICLE_REQUEST",
|
||||
"workspace_name": "인트라넷 차량 신청",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"workspace_type": "INTRANET_SERVICE",
|
||||
"software_app_id": None,
|
||||
"service_type_id": 4,
|
||||
"workspace_code": "INTRA_EQUIPMENT_RENTAL",
|
||||
"workspace_name": "인트라넷 비품 대여",
|
||||
"is_active": True,
|
||||
},
|
||||
{
|
||||
"workspace_type": "INTRANET_SERVICE",
|
||||
"software_app_id": None,
|
||||
"service_type_id": 5,
|
||||
"workspace_code": "INTRA_GENERAL_QNA",
|
||||
"workspace_name": "인트라넷 일반 문의",
|
||||
"is_active": True,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("request_approvals")
|
||||
op.drop_table("abc_feedback_mappings")
|
||||
op.drop_table("support_tickets")
|
||||
op.drop_table("support_category_codes")
|
||||
op.drop_table("support_status_codes")
|
||||
op.drop_table("workspace_field_mappings")
|
||||
op.drop_table("workspace_channel_mappings")
|
||||
op.drop_table("user_workspace_access")
|
||||
op.drop_table("workspaces")
|
||||
op.drop_table("service_types")
|
||||
op.drop_table("software_apps")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""add support ticket content fields
|
||||
|
||||
Revision ID: 0002_support_fields
|
||||
Revises: 0001_initial_support_schema
|
||||
Create Date: 2026-07-10 00:00:00
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
revision = "0002_support_fields"
|
||||
down_revision = "0001_initial_support_schema"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
existing_columns = {column["name"] for column in inspector.get_columns("support_tickets")}
|
||||
|
||||
if "requester_contact" not in existing_columns:
|
||||
op.add_column("support_tickets", sa.Column("requester_contact", sa.String(length=100), nullable=True))
|
||||
|
||||
if "description" not in existing_columns:
|
||||
op.add_column("support_tickets", sa.Column("description", sa.Text(), nullable=True))
|
||||
|
||||
if "approval_status" not in existing_columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column("approval_status", sa.String(length=20), nullable=False, server_default="NOT_REQUIRED"),
|
||||
)
|
||||
|
||||
if "sync_status" not in existing_columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column("sync_status", sa.String(length=20), nullable=False, server_default="PENDING"),
|
||||
)
|
||||
|
||||
if "issue_link_status" not in existing_columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column("issue_link_status", sa.String(length=20), nullable=False, server_default="NOT_REQUIRED"),
|
||||
)
|
||||
|
||||
if "extra_fields" not in existing_columns:
|
||||
op.add_column("support_tickets", sa.Column("extra_fields", sa.JSON(), nullable=True))
|
||||
|
||||
op.execute("UPDATE support_tickets SET description = '' WHERE description IS NULL")
|
||||
op.alter_column("support_tickets", "description", existing_type=sa.Text(), nullable=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("support_tickets", "extra_fields")
|
||||
op.drop_column("support_tickets", "issue_link_status")
|
||||
op.drop_column("support_tickets", "sync_status")
|
||||
op.drop_column("support_tickets", "approval_status")
|
||||
op.drop_column("support_tickets", "description")
|
||||
op.drop_column("support_tickets", "requester_contact")
|
||||
@@ -0,0 +1,204 @@
|
||||
"""add comment, attachment, and approval workflow tables
|
||||
|
||||
Revision ID: 0003_support_comment_flow
|
||||
Revises: 0002_support_fields
|
||||
Create Date: 2026-07-13 00:00:00
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
revision = "0003_support_comment_flow"
|
||||
down_revision = "0002_support_fields"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
inspector = inspect(bind)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
|
||||
if "workspace_approval_policies" not in existing_tables:
|
||||
op.create_table(
|
||||
"workspace_approval_policies",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("request_category_code", sa.String(length=50), nullable=True),
|
||||
sa.Column("ticket_type", sa.String(length=30), nullable=True),
|
||||
sa.Column("rule_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("conditions", sa.JSON(), nullable=True),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("version", sa.Integer(), nullable=False, server_default="1"),
|
||||
sa.Column("created_by", sa.String(length=100), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(),
|
||||
nullable=False,
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_workspace_approval_policies_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"workspace_id",
|
||||
"rule_name",
|
||||
"version",
|
||||
name="uq_workspace_approval_policies_workspace_rule_version",
|
||||
),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
if "workspace_approval_steps" not in existing_tables:
|
||||
op.create_table(
|
||||
"workspace_approval_steps",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("policy_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("step_order", sa.Integer(), nullable=False),
|
||||
sa.Column("approver_type", sa.String(length=30), nullable=False),
|
||||
sa.Column("approver_key", sa.String(length=100), nullable=False),
|
||||
sa.Column("approver_name", sa.String(length=100), nullable=True),
|
||||
sa.Column("approval_mode", sa.String(length=20), nullable=False, server_default="ALL"),
|
||||
sa.Column("is_required", sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||
sa.Column("sla_hours", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(["policy_id"], ["workspace_approval_policies.id"], name="fk_workspace_approval_steps_policy"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("policy_id", "step_order", name="uq_workspace_approval_steps_policy_order"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
if "ticket_comments" not in existing_tables:
|
||||
op.create_table(
|
||||
"ticket_comments",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("ticket_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("parent_comment_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("author_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("author_tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("author_name", sa.String(length=100), nullable=True),
|
||||
sa.Column("comment_type", sa.String(length=20), nullable=False, server_default="COMMENT"),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("is_internal", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
sa.Column("abc_comment_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("sync_status", sa.String(length=20), nullable=False, server_default="PENDING"),
|
||||
sa.Column("edited_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(),
|
||||
nullable=False,
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
),
|
||||
sa.Column("deleted_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["ticket_id"], ["support_tickets.id"], name="fk_ticket_comments_ticket"),
|
||||
sa.ForeignKeyConstraint(["parent_comment_id"], ["ticket_comments.id"], name="fk_ticket_comments_parent_comment"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("abc_comment_id", name="uq_ticket_comments_abc_comment_id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index("idx_ticket_comments_ticket_created", "ticket_comments", ["ticket_id", "created_at"])
|
||||
|
||||
if "attachments" not in existing_tables:
|
||||
op.create_table(
|
||||
"attachments",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("ticket_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("comment_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("uploader_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("uploader_tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("original_file_name", sa.String(length=255), nullable=False),
|
||||
sa.Column("stored_file_name", sa.String(length=255), nullable=True),
|
||||
sa.Column("storage_provider", sa.String(length=30), nullable=False, server_default="LOCAL"),
|
||||
sa.Column("storage_bucket", sa.String(length=255), nullable=True),
|
||||
sa.Column("storage_key", sa.String(length=500), nullable=False),
|
||||
sa.Column("mime_type", sa.String(length=100), nullable=True),
|
||||
sa.Column("file_extension", sa.String(length=20), nullable=True),
|
||||
sa.Column("file_size", sa.BigInteger(), nullable=True),
|
||||
sa.Column("checksum_sha256", sa.String(length=64), nullable=True),
|
||||
sa.Column("abc_attachment_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("attachment_status", sa.String(length=20), nullable=False, server_default="ACTIVE"),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(),
|
||||
nullable=False,
|
||||
server_default=sa.text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
),
|
||||
sa.Column("deleted_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.CheckConstraint(
|
||||
"comment_id IS NULL OR ticket_id IS NOT NULL",
|
||||
name="chk_attachments_comment_requires_ticket",
|
||||
),
|
||||
sa.ForeignKeyConstraint(["comment_id"], ["ticket_comments.id"], name="fk_attachments_comment"),
|
||||
sa.ForeignKeyConstraint(["ticket_id"], ["support_tickets.id"], name="fk_attachments_ticket"),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_attachments_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("storage_provider", "storage_key", name="uq_attachments_storage_provider_key"),
|
||||
sa.UniqueConstraint("abc_attachment_id", name="uq_attachments_abc_attachment_id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index("idx_attachments_ticket_created", "attachments", ["ticket_id", "created_at"])
|
||||
op.create_index("idx_attachments_comment_created", "attachments", ["comment_id", "created_at"])
|
||||
|
||||
approval_columns = {column["name"] for column in inspector.get_columns("request_approvals")}
|
||||
|
||||
if "workflow_policy_id" not in approval_columns:
|
||||
op.add_column("request_approvals", sa.Column("workflow_policy_id", sa.BigInteger(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"fk_request_approvals_workflow_policy",
|
||||
"request_approvals",
|
||||
"workspace_approval_policies",
|
||||
["workflow_policy_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
if "workflow_step_id" not in approval_columns:
|
||||
op.add_column("request_approvals", sa.Column("workflow_step_id", sa.BigInteger(), nullable=True))
|
||||
op.create_foreign_key(
|
||||
"fk_request_approvals_workflow_step",
|
||||
"request_approvals",
|
||||
"workspace_approval_steps",
|
||||
["workflow_step_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
if "step_order" not in approval_columns:
|
||||
op.add_column("request_approvals", sa.Column("step_order", sa.Integer(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
approval_columns = {column["name"] for column in inspect(op.get_bind()).get_columns("request_approvals")}
|
||||
|
||||
if "step_order" in approval_columns:
|
||||
op.drop_column("request_approvals", "step_order")
|
||||
|
||||
if "workflow_step_id" in approval_columns:
|
||||
op.drop_constraint("fk_request_approvals_workflow_step", "request_approvals", type_="foreignkey")
|
||||
op.drop_column("request_approvals", "workflow_step_id")
|
||||
|
||||
if "workflow_policy_id" in approval_columns:
|
||||
op.drop_constraint("fk_request_approvals_workflow_policy", "request_approvals", type_="foreignkey")
|
||||
op.drop_column("request_approvals", "workflow_policy_id")
|
||||
|
||||
existing_tables = set(inspect(op.get_bind()).get_table_names())
|
||||
|
||||
if "attachments" in existing_tables:
|
||||
op.drop_index("idx_attachments_comment_created", table_name="attachments")
|
||||
op.drop_index("idx_attachments_ticket_created", table_name="attachments")
|
||||
op.drop_table("attachments")
|
||||
|
||||
if "ticket_comments" in existing_tables:
|
||||
op.drop_index("idx_ticket_comments_ticket_created", table_name="ticket_comments")
|
||||
op.drop_table("ticket_comments")
|
||||
|
||||
if "workspace_approval_steps" in existing_tables:
|
||||
op.drop_table("workspace_approval_steps")
|
||||
|
||||
if "workspace_approval_policies" in existing_tables:
|
||||
op.drop_table("workspace_approval_policies")
|
||||
@@ -0,0 +1,52 @@
|
||||
"""seed role_access software workspaces
|
||||
|
||||
Revision ID: 0004_role_access_workspaces
|
||||
Revises: 0003_support_comment_flow
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0004_role_access_workspaces"
|
||||
down_revision = "0003_support_comment_flow"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
software_apps = sa.table(
|
||||
"software_apps",
|
||||
sa.column("app_code", sa.String(50)),
|
||||
sa.column("app_name", sa.String(100)),
|
||||
sa.column("description", sa.Text()),
|
||||
sa.column("is_active", sa.Boolean()),
|
||||
)
|
||||
op.execute(sa.text("""
|
||||
INSERT IGNORE INTO software_apps
|
||||
(app_code, app_name, description, is_active)
|
||||
VALUES
|
||||
('EGBIM', 'EGBIM', 'EGBIM Q&A', 1),
|
||||
('TOVA', 'TOVA', 'TOVA Q&A', 1),
|
||||
('GAIA', 'GAIA', 'GAIA Q&A', 1),
|
||||
('KNGIL', 'KNGIL', 'KNGIL Q&A', 1),
|
||||
('INTRANET_QNA', 'INTRANET_QNA', '인트라넷 공통 Q&A', 1)
|
||||
"""))
|
||||
|
||||
op.execute(sa.text("""
|
||||
INSERT IGNORE INTO workspaces
|
||||
(workspace_type, software_app_id, service_type_id, workspace_code, workspace_name, is_active)
|
||||
SELECT 'SOFTWARE_APP', id, NULL, app_code, app_name, 1
|
||||
FROM software_apps
|
||||
WHERE app_code IN ('EGBIM', 'TOVA', 'GAIA', 'KNGIL', 'INTRANET_QNA')
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(sa.text("""
|
||||
DELETE FROM workspaces
|
||||
WHERE workspace_code IN ('EGBIM', 'TOVA', 'GAIA', 'KNGIL', 'INTRANET_QNA')
|
||||
"""))
|
||||
op.execute(sa.text("""
|
||||
DELETE FROM software_apps
|
||||
WHERE app_code IN ('EGBIM', 'TOVA', 'GAIA', 'KNGIL', 'INTRANET_QNA')
|
||||
"""))
|
||||
@@ -0,0 +1,53 @@
|
||||
"""fix test workspace display name
|
||||
|
||||
Revision ID: 0005_qna_workspace_code
|
||||
Revises: 0004_role_access_workspaces
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0005_qna_workspace_code"
|
||||
down_revision = "0004_role_access_workspaces"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE software_apps SET app_code = :new_code, app_name = :name, description = :description "
|
||||
"WHERE app_code = :old_code"
|
||||
).bindparams(
|
||||
new_code="Q&A_Platform",
|
||||
name="Q&A_Platform",
|
||||
description="Q&A_Platform feedback",
|
||||
old_code="INTRANET_QNA",
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE workspaces SET workspace_code = :new_code, workspace_name = :name "
|
||||
"WHERE workspace_code = :old_code"
|
||||
).bindparams(new_code="Q&A_Platform", name="Q&A_Platform", old_code="INTRANET_QNA")
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE software_apps SET app_code = :new_code, app_name = :name, description = :description "
|
||||
"WHERE app_code = :old_code"
|
||||
).bindparams(
|
||||
new_code="INTRANET_QNA",
|
||||
name="INTRANET_QNA",
|
||||
description="인트라넷 공통 Q&A",
|
||||
old_code="Q&A_Platform",
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE workspaces SET workspace_code = :new_code, workspace_name = :name "
|
||||
"WHERE workspace_code = :old_code"
|
||||
).bindparams(new_code="INTRANET_QNA", name="INTRANET_QNA", old_code="Q&A_Platform")
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
"""add internal support identities and role assignments
|
||||
|
||||
Revision ID: 0006_internal_identity_roles
|
||||
Revises: 0005_qna_workspace_code
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0006_internal_identity_roles"
|
||||
down_revision = "0005_qna_workspace_code"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
ROLE_SEED = (
|
||||
("SYSTEM_ADMIN", "시스템 관리자", "전체 workspace와 운영 설정에 접근"),
|
||||
("SUPER_ADMIN", "전체 관리자", "모든 workspace의 운영 권한"),
|
||||
("PROJECT_MANAGER", "프로젝트 관리자", "배정된 workspace의 운영·댓글·승인 권한"),
|
||||
("END_USER", "일반 사용자", "피드백 작성과 본인 데이터 조회"),
|
||||
("FEEDBACK_PROVIDER", "피드백 제공자", "피드백 작성과 배정된 workspace 조회"),
|
||||
)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"support_users",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("sso_subject", sa.String(length=100), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("email", sa.String(length=320), nullable=True),
|
||||
sa.Column("name", sa.String(length=100), nullable=True),
|
||||
sa.Column("department", sa.String(length=100), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), server_default=sa.text("'ACTIVE'"), nullable=False),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(),
|
||||
server_default=sa.text("CURRENT_TIMESTAMP"),
|
||||
server_onupdate=sa.text("CURRENT_TIMESTAMP"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("sso_subject", "tenant_id", name="uq_support_users_subject_tenant"),
|
||||
mysql_engine="InnoDB",
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_table(
|
||||
"support_roles",
|
||||
sa.Column("code", sa.String(length=40), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
|
||||
sa.PrimaryKeyConstraint("code"),
|
||||
mysql_engine="InnoDB",
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
op.create_table(
|
||||
"support_role_assignments",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("support_user_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("role_code", sa.String(length=40), nullable=False),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False),
|
||||
sa.ForeignKeyConstraint(["role_code"], ["support_roles.code"], name="fk_support_role_assignments_role"),
|
||||
sa.ForeignKeyConstraint(["support_user_id"], ["support_users.id"], name="fk_support_role_assignments_user"),
|
||||
sa.ForeignKeyConstraint(["workspace_id"], ["workspaces.id"], name="fk_support_role_assignments_workspace"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"support_user_id",
|
||||
"role_code",
|
||||
"workspace_id",
|
||||
name="uq_support_role_assignment_user_role_workspace",
|
||||
),
|
||||
mysql_engine="InnoDB",
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
roles = sa.table(
|
||||
"support_roles",
|
||||
sa.column("code", sa.String(40)),
|
||||
sa.column("name", sa.String(100)),
|
||||
sa.column("description", sa.Text()),
|
||||
)
|
||||
op.bulk_insert(
|
||||
roles,
|
||||
[
|
||||
{"code": code, "name": name, "description": description}
|
||||
for code, name, description in ROLE_SEED
|
||||
],
|
||||
)
|
||||
|
||||
# Preserve existing access rows while moving the authority to the internal
|
||||
# identity/role tables. Existing rows are legacy effective permissions.
|
||||
op.execute(sa.text("""
|
||||
INSERT IGNORE INTO support_users
|
||||
(sso_subject, tenant_id, status)
|
||||
SELECT DISTINCT user_id, tenant_id, 'ACTIVE'
|
||||
FROM user_workspace_access
|
||||
"""))
|
||||
op.execute(sa.text("""
|
||||
INSERT IGNORE INTO support_role_assignments
|
||||
(support_user_id, role_code, workspace_id)
|
||||
SELECT su.id,
|
||||
CASE
|
||||
WHEN uwa.workspace_role IN (
|
||||
'SYSTEM_ADMIN', 'SUPER_ADMIN', 'PROJECT_MANAGER',
|
||||
'END_USER', 'FEEDBACK_PROVIDER'
|
||||
) THEN uwa.workspace_role
|
||||
ELSE 'END_USER'
|
||||
END,
|
||||
uwa.workspace_id
|
||||
FROM user_workspace_access uwa
|
||||
JOIN support_users su
|
||||
ON su.sso_subject = uwa.user_id
|
||||
AND su.tenant_id = uwa.tenant_id
|
||||
"""))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("support_role_assignments")
|
||||
op.drop_table("support_roles")
|
||||
op.drop_table("support_users")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""store BARON-SSO phone numbers for support identities"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0007_support_user_profile_phone"
|
||||
down_revision = "0006_internal_identity_roles"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"support_users",
|
||||
sa.Column("phone_number", sa.String(length=50), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("support_users", "phone_number")
|
||||
@@ -0,0 +1,24 @@
|
||||
"""store requester profile snapshot on support tickets"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0008_ticket_requester_profile"
|
||||
down_revision = "0007_support_user_profile_phone"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("support_tickets", sa.Column("requester_email", sa.String(length=320), nullable=True))
|
||||
op.add_column("support_tickets", sa.Column("requester_name", sa.String(length=100), nullable=True))
|
||||
op.add_column("support_tickets", sa.Column("requester_department", sa.String(length=100), nullable=True))
|
||||
op.add_column("support_tickets", sa.Column("requester_phone_number", sa.String(length=50), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("support_tickets", "requester_phone_number")
|
||||
op.drop_column("support_tickets", "requester_department")
|
||||
op.drop_column("support_tickets", "requester_name")
|
||||
op.drop_column("support_tickets", "requester_email")
|
||||
@@ -0,0 +1,28 @@
|
||||
"""add the ticket secret flag for existing support databases"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0009_ticket_secret_flag"
|
||||
down_revision = "0008_ticket_requester_profile"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in inspect(bind).get_columns("support_tickets")}
|
||||
if "is_secret" not in columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column("is_secret", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {column["name"] for column in inspect(bind).get_columns("support_tickets")}
|
||||
if "is_secret" in columns:
|
||||
op.drop_column("support_tickets", "is_secret")
|
||||
@@ -0,0 +1,93 @@
|
||||
"""add migration batch and source mapping tables
|
||||
|
||||
Revision ID: 0010_egbim_migration_tracking
|
||||
Revises: 0009_ticket_secret_flag
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import inspect
|
||||
|
||||
|
||||
revision = "0010_egbim_migration_tracking"
|
||||
down_revision = "0009_ticket_secret_flag"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
|
||||
if "migration_batches" not in existing_tables:
|
||||
op.create_table(
|
||||
"migration_batches",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("batch_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("source_system", sa.String(length=50), nullable=False),
|
||||
sa.Column("started_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.Column("completed_at", sa.TIMESTAMP(), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=False, server_default=sa.text("'RUNNING'")),
|
||||
sa.Column("executed_by", sa.String(length=100), nullable=True),
|
||||
sa.Column("notes", sa.Text(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
|
||||
if "migration_mappings" not in existing_tables:
|
||||
op.create_table(
|
||||
"migration_mappings",
|
||||
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
|
||||
sa.Column("batch_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("source_system", sa.String(length=50), nullable=False),
|
||||
sa.Column("source_entity_type", sa.String(length=30), nullable=False),
|
||||
sa.Column("source_entity_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("source_parent_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("workspace_id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("ticket_id", sa.BigInteger(), nullable=True),
|
||||
sa.Column("abc_feedback_id", sa.String(length=100), nullable=True),
|
||||
sa.Column("migration_status", sa.String(length=20), nullable=False),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.TIMESTAMP(), nullable=False, server_default=sa.text("CURRENT_TIMESTAMP")),
|
||||
sa.ForeignKeyConstraint(
|
||||
["batch_id"], ["migration_batches.id"], name="fk_migration_mappings_batch"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["workspace_id"], ["workspaces.id"], name="fk_migration_mappings_workspace"
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["ticket_id"], ["support_tickets.id"], name="fk_migration_mappings_ticket"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"source_system",
|
||||
"source_entity_type",
|
||||
"source_entity_id",
|
||||
name="uq_migration_mappings_source_entity",
|
||||
),
|
||||
mysql_engine="InnoDB",
|
||||
)
|
||||
op.create_index(
|
||||
"idx_migration_mappings_batch_status",
|
||||
"migration_mappings",
|
||||
["batch_id", "migration_status"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_migration_mappings_ticket",
|
||||
"migration_mappings",
|
||||
["ticket_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
existing_tables = set(inspect(bind).get_table_names())
|
||||
|
||||
if "migration_mappings" in existing_tables:
|
||||
op.drop_index("idx_migration_mappings_ticket", table_name="migration_mappings")
|
||||
op.drop_index("idx_migration_mappings_batch_status", table_name="migration_mappings")
|
||||
op.drop_table("migration_mappings")
|
||||
if "migration_batches" in existing_tables:
|
||||
op.drop_table("migration_batches")
|
||||
@@ -0,0 +1,54 @@
|
||||
"""add review statuses required by the EGBIM migration
|
||||
|
||||
Revision ID: 0011_egbim_review_statuses
|
||||
Revises: 0010_egbim_migration_tracking
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0011_egbim_review_statuses"
|
||||
down_revision = "0010_egbim_migration_tracking"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Keep this migration idempotent because some staging databases may have
|
||||
# received the status rows through an earlier manual migration.
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO support_status_codes (code, name, sort_order)
|
||||
SELECT 'IN_REVIEW', '검토중', 45
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM support_status_codes WHERE code = 'IN_REVIEW'
|
||||
)
|
||||
"""
|
||||
)
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO support_status_codes (code, name, sort_order)
|
||||
SELECT 'DETAILED_REVIEW', '정밀검토중', 48
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM support_status_codes WHERE code = 'DETAILED_REVIEW'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Do not remove a status that is already referenced by migrated tickets.
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM support_status_codes
|
||||
WHERE code IN ('IN_REVIEW', 'DETAILED_REVIEW')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM support_tickets
|
||||
WHERE support_tickets.status_code = support_status_codes.code
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""add Q&A category codes used by the feedback workspace
|
||||
|
||||
Revision ID: 0012_qna_category_codes
|
||||
Revises: 0011_egbim_review_statuses
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision = "0012_qna_category_codes"
|
||||
down_revision = "0011_egbim_review_statuses"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Some existing staging databases were created before the Q&A category
|
||||
# split was added. Keep this safe to run against both old and new DBs.
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO support_category_codes (code, name, sort_order)
|
||||
VALUES
|
||||
('ERROR_QNA', '오류 문의', 51),
|
||||
('IMPROVEMENT_QNA', '개선 문의', 52)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
sort_order = VALUES(sort_order)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Preserve codes that are already referenced by tickets.
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM support_category_codes
|
||||
WHERE code IN ('ERROR_QNA', 'IMPROVEMENT_QNA')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM support_tickets
|
||||
WHERE support_tickets.category_code = support_category_codes.code
|
||||
)
|
||||
"""
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
"""store SSO tenant memberships on internal support identities"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0013_tenant_memberships"
|
||||
down_revision = "0012_qna_category_codes"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "tenant_ids" in {
|
||||
column["name"] for column in inspect(bind).get_columns("support_users")
|
||||
}:
|
||||
return
|
||||
op.add_column(
|
||||
"support_users",
|
||||
sa.Column("tenant_ids", sa.JSON(), nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "tenant_ids" in {
|
||||
column["name"] for column in inspect(bind).get_columns("support_users")
|
||||
}:
|
||||
op.drop_column("support_users", "tenant_ids")
|
||||
@@ -0,0 +1,54 @@
|
||||
"""store ABC project/channel targets in baron_support"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "0014_abc_workspace_mapping"
|
||||
down_revision = "0013_tenant_memberships"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {
|
||||
column["name"]
|
||||
for column in inspect(bind).get_columns("workspace_channel_mappings")
|
||||
}
|
||||
if "abc_project_id" not in columns:
|
||||
op.add_column(
|
||||
"workspace_channel_mappings",
|
||||
sa.Column("abc_project_id", sa.BigInteger(), nullable=True),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO workspace_channel_mappings
|
||||
(workspace_id, abc_project_id, abc_channel_id, is_active)
|
||||
SELECT w.id, :project_id, :channel_id, 1
|
||||
FROM workspaces w
|
||||
WHERE w.workspace_code = :workspace_code
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM workspace_channel_mappings m
|
||||
WHERE m.workspace_id = w.id
|
||||
AND m.abc_channel_id = :channel_id
|
||||
)
|
||||
""",
|
||||
).bindparams(
|
||||
project_id=1,
|
||||
channel_id="1",
|
||||
workspace_code="Q&A_Platform",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if "abc_project_id" in {
|
||||
column["name"]
|
||||
for column in inspect(bind).get_columns("workspace_channel_mappings")
|
||||
}:
|
||||
op.drop_column("workspace_channel_mappings", "abc_project_id")
|
||||
@@ -0,0 +1,73 @@
|
||||
"""separate feedback handling status from ticket and issue lifecycle status
|
||||
|
||||
Revision ID: 0015_feedback_status_separation
|
||||
Revises: 0014_abc_workspace_mapping
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import inspect, text
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "0015_feedback_status_separation"
|
||||
down_revision = "0014_abc_workspace_mapping"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {
|
||||
column["name"]
|
||||
for column in inspect(bind).get_columns("support_tickets")
|
||||
}
|
||||
|
||||
if "feedback_status" not in columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column(
|
||||
"feedback_status",
|
||||
sa.String(length=20),
|
||||
nullable=False,
|
||||
server_default="NEW",
|
||||
),
|
||||
)
|
||||
|
||||
if "external_issue_status" not in columns:
|
||||
op.add_column(
|
||||
"support_tickets",
|
||||
sa.Column("external_issue_status", sa.String(length=30), nullable=True),
|
||||
)
|
||||
|
||||
# Preserve the existing lifecycle state while giving old records a useful
|
||||
# initial feedback state. Future issue-link changes must not mutate this
|
||||
# field automatically.
|
||||
op.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE support_tickets
|
||||
SET feedback_status = CASE
|
||||
WHEN status_code = 'CLOSED' THEN 'CLOSED'
|
||||
WHEN status_code = 'RESOLVED' THEN 'ANSWERED'
|
||||
WHEN status_code IN ('APPROVED', 'IN_PROGRESS', 'IN_REVIEW', 'DETAILED_REVIEW')
|
||||
THEN 'IN_REVIEW'
|
||||
ELSE 'NEW'
|
||||
END
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
columns = {
|
||||
column["name"]
|
||||
for column in inspect(bind).get_columns("support_tickets")
|
||||
}
|
||||
|
||||
if "external_issue_status" in columns:
|
||||
op.drop_column("support_tickets", "external_issue_status")
|
||||
if "feedback_status" in columns:
|
||||
op.drop_column("support_tickets", "feedback_status")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""align feedback status values with the six issue kanban stages
|
||||
|
||||
Revision ID: 0016_feedback_status_stages
|
||||
Revises: 0015_feedback_status_separation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
revision = "0016_feedback_status_stages"
|
||||
down_revision = "0015_feedback_status_separation"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Keep the lifecycle field independent while normalizing values created by
|
||||
# the previous four-state implementation.
|
||||
op.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE support_tickets
|
||||
SET feedback_status = CASE feedback_status
|
||||
WHEN 'NEW' THEN 'INIT'
|
||||
WHEN 'IN_REVIEW' THEN 'ON_REVIEW'
|
||||
WHEN 'ANSWERED' THEN 'RESOLVED'
|
||||
WHEN 'CLOSED' THEN 'RESOLVED'
|
||||
ELSE feedback_status
|
||||
END
|
||||
WHERE feedback_status IN ('NEW', 'IN_REVIEW', 'ANSWERED', 'CLOSED')
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE support_tickets
|
||||
SET feedback_status = CASE feedback_status
|
||||
WHEN 'INIT' THEN 'NEW'
|
||||
WHEN 'ON_REVIEW' THEN 'IN_REVIEW'
|
||||
WHEN 'DETAILED_REVIEW' THEN 'IN_REVIEW'
|
||||
WHEN 'IN_PROGRESS' THEN 'IN_REVIEW'
|
||||
WHEN 'RESOLVED' THEN 'ANSWERED'
|
||||
WHEN 'PENDING' THEN 'IN_REVIEW'
|
||||
ELSE feedback_status
|
||||
END
|
||||
"""
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user