"""Update ``laptop_replacement`` Feishu approval external_code. Revision ID: 183 Revises: 182 Create Date: 2026-05-19 NOTE (2026-05-20 alembic chain hotfix): PR #408 briefly renamed this file's revision to ``183_laptop`` to disambiguate from PR #406's also-revision-"183" office-held migration. PR #409 then renumbered the office-held chain to 186/187/188 anchored on ``down_revision = "183"`` — i.e. expecting THIS file to remain revision="183". The PR #408 rename + PR #409 renumber landed on main in different commits and the residual mismatch left the chain orphaned at line `186 → 183 (missing)`. This restores the historical ``revision = "183"`` so the chain is linear again and production's ``alembic_version='183'`` stays valid without manual SQL fixup. PR #408's chain (now 189-191 after hotfix renumber) anchors off ``188_backfill_asset_holder_kind``. Why this exists --------------- The Feishu 电脑更换 (laptop_replacement) approval definition was rebuilt on the Feishu open-platform side with a new form layout (conditional visibility on additional widgets, refreshed required-set). The new definition lives under a fresh approval code; the old code keeps the legacy schema and will diverge. This migration repoints ``integration_approval_flows.external_code`` for ``flow_key='laptop_replacement'`` (on the primary chat_approval/feishu integration) at the new UUID, and invalidates ``platform_settings['approval.asset_template_cache']`` so the next ``/api/v1/approval/asset/templates`` read refetches the schema from Feishu instead of serving the stale parsed widgets that were cached under the old code. Idempotency ----------- * The UPDATE is keyed by ``(integration_id, flow_key)`` via the existing unique constraint; re-running is a no-op. * The cache delete is unconditional — the next list call repopulates it. Safe even if the row was already absent. * If the laptop_replacement row doesn't exist (greenfield deploy where 026/027 never seeded it because the legacy env var wasn't set), this migration is a no-op for the row UPDATE. The cache delete still runs harmlessly. Operators on a greenfield install will create the row via the admin Integrations UI with the new code directly. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "183" down_revision = "182" branch_labels = None depends_on = None _FLOW_KEY = "laptop_replacement" _OLD_CODE = "3DF39DB6-7F1D-4CCB-9121-C9C73A852246" _NEW_CODE = "7694D6BE-7FEE-4E49-96B5-A106FC4623DD" _CACHE_KEY = "approval.asset_template_cache" 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 LIMIT 1" ), {"n": name}, ).scalar() is not None def upgrade() -> None: bind = op.get_bind() if _table_exists(bind, "integration_approval_flows"): bind.execute( sa.text( "UPDATE integration_approval_flows " "SET external_code = :new_code " "WHERE flow_key = :flow_key " "AND external_code = :old_code" ), { "new_code": _NEW_CODE, "old_code": _OLD_CODE, "flow_key": _FLOW_KEY, }, ) if _table_exists(bind, "platform_settings"): bind.execute( sa.text("DELETE FROM platform_settings WHERE `key` = :k"), {"k": _CACHE_KEY}, ) def downgrade() -> None: bind = op.get_bind() if _table_exists(bind, "integration_approval_flows"): bind.execute( sa.text( "UPDATE integration_approval_flows " "SET external_code = :old_code " "WHERE flow_key = :flow_key " "AND external_code = :new_code" ), { "new_code": _NEW_CODE, "old_code": _OLD_CODE, "flow_key": _FLOW_KEY, }, ) if _table_exists(bind, "platform_settings"): bind.execute( sa.text("DELETE FROM platform_settings WHERE `key` = :k"), {"k": _CACHE_KEY}, )