"""phase51 — add 'external_inspector' role to users.role ENUM Introduces the External Inspector role: an inspector employed by the customer or a third party rather than by the tenant. It has exactly the same capabilities as the internal 'inspector' role and is scoped the same way — through InspectorAssignment rows, resolved by get_inspector_scope(). Every capability/scoping check that used to test `role == 'inspector'` now tests membership of User.INSPECTOR_ROLES (exposed as the `is_inspector` property), so the new role picks up inspector behaviour everywhere without a per-route allowlist. This is a pure ENUM expansion (adds a value, removes and migrates nothing), so the 3-step ENUM protocol does not apply and re-running the same MODIFY is a no-op — safe to re-run. MULTI-TENANT NOTE ----------------- This runs once per tenant database via control/tenant_migrate.py, like every other script in migrations/versions. It is a metadata-only ALTER on a small table, but MySQL still takes an exclusive metadata lock for the duration, so run it during the normal migration window rather than under load. Notification matrix rows for the new 'external_inspector' column are NOT seeded here: MATRIX_DEFAULTS mirrors the Inspector column at runtime and is_enabled() falls back to that default when a row is absent, so an unseeded tenant behaves exactly like the Inspector column until an admin saves the matrix page. """ revision = 'phase51_external_inspector' down_revision = 'phase50_sched_acknowledged' branch_labels = None depends_on = None from alembic import op import sqlalchemy as sa _ENUM_WITH_EXTERNAL = ( "ENUM('admin','director','inspector','project_manager','customer'," "'auditor','external_inspector')" ) _ENUM_WITHOUT_EXTERNAL = ( "ENUM('admin','director','inspector','project_manager','customer','auditor')" ) def upgrade(): # Idempotent: MODIFY to the expanded set is harmless if already applied. op.execute(sa.text( f"ALTER TABLE users MODIFY COLUMN role {_ENUM_WITH_EXTERNAL} NOT NULL" )) def downgrade(): # Reassign any external_inspector rows before contracting the ENUM so no # account is orphaned. They become internal inspectors, which keeps their # InspectorAssignment scoping intact — the same contracts still apply. op.execute(sa.text( "UPDATE users SET role = 'inspector' WHERE role = 'external_inspector'" )) op.execute(sa.text( f"ALTER TABLE users MODIFY COLUMN role {_ENUM_WITHOUT_EXTERNAL} NOT NULL" ))