Files
paliad/internal/db/migrations/054_approvals.down.sql
m b3401ec8ac feat(t-paliad-138): migration 054 — dual-control approvals schema
Schema-only commit (1 of 8) for the 4-Augen-Prüfung workflow per
docs/design-approvals-2026-05-06.md. No Go code reads these yet —
paliad behaves identically until commit 2 wires ApprovalService into
the mutation paths.

Migration 054 adds:

1. `senior_pa` to paliad.project_teams.role CHECK. Drops both the
   English `project_teams_role_check` and the German-legacy
   `projekt_teams_role_check` (live-DB constraint name carried over
   from migration 018's pre-rename era).

2. `paliad.approval_role_level(text) RETURNS int IMMUTABLE` — strict
   ladder helper: lead(5) > of_counsel(4) > associate(3) > senior_pa(2)
   > pa(1) > [local_counsel/expert/observer = 0 = ineligible]. Mirrors
   the upcoming Go `levelOf()`.

3. `paliad.approval_policies` (project_id, entity_type, lifecycle_event,
   required_role) — UNIQUE composite key gives at most 8 rows per
   project. RLS: SELECT via can_see_project; INSERT/UPDATE/DELETE only
   for global_admin (defence-in-depth; service-role pool bypasses RLS,
   so the actual gate is service-layer).

4. `paliad.approval_requests` — operational pending workflow.
   pre_image jsonb captures revert state; payload echoes the diff;
   required_role snapshots the policy at request time. CHECK
   `decided_by != requested_by` is the second layer of self-approval
   block. RLS = same can_see_project predicate as deadlines /
   appointments — anyone with project visibility sees pending requests.

5. `approval_status` (default 'approved'), `pending_request_id`,
   `approved_by`, `approved_at` columns on both deadlines and
   appointments. `appointments.completed_at` (new) lands the
   appointment:complete lifecycle event.

6. Backfill: every existing deadline + appointment row marked
   approval_status='legacy'. Per Q11, no retroactive approval; the
   next mutation on a legacy row that hits an active policy follows
   the normal flow.

Live-DB dry-run verified end-to-end: 20 deadlines + 5 appointments
backfill, both new tables instantiate cleanly, helper function returns
correct levels, self-approval CHECK fires on invalid INSERT, valid
pending insert succeeds.
2026-05-06 15:13:26 +02:00

43 lines
1.7 KiB
SQL

-- t-paliad-138: rollback dual-control approvals.
--
-- Reverses 054_approvals.up.sql:
-- 1. Drop appointment + deadline approval columns.
-- 2. Drop paliad.approval_requests.
-- 3. Drop paliad.approval_policies.
-- 4. Drop paliad.approval_role_level().
-- 5. Restore project_teams.role CHECK without 'senior_pa'.
--
-- Step 5 will fail loudly if any user has been re-roled to 'senior_pa' —
-- intentional, mirrors the t-paliad-051 down strategy. Operator must
-- migrate those rows to another role before rolling back.
ALTER TABLE paliad.appointments
DROP COLUMN IF EXISTS completed_at,
DROP COLUMN IF EXISTS approved_at,
DROP COLUMN IF EXISTS approved_by,
DROP COLUMN IF EXISTS pending_request_id,
DROP COLUMN IF EXISTS approval_status;
ALTER TABLE paliad.deadlines
DROP COLUMN IF EXISTS approved_at,
DROP COLUMN IF EXISTS approved_by,
DROP COLUMN IF EXISTS pending_request_id,
DROP COLUMN IF EXISTS approval_status;
DROP INDEX IF EXISTS paliad.deadlines_approval_status_pending_idx;
DROP INDEX IF EXISTS paliad.appointments_approval_status_pending_idx;
DROP TABLE IF EXISTS paliad.approval_requests;
DROP TABLE IF EXISTS paliad.approval_policies;
DROP FUNCTION IF EXISTS paliad.approval_role_level(text);
-- Drop by both English and the German-legacy name (see up migration §1).
ALTER TABLE paliad.project_teams DROP CONSTRAINT IF EXISTS projekt_teams_role_check;
ALTER TABLE paliad.project_teams DROP CONSTRAINT IF EXISTS project_teams_role_check;
ALTER TABLE paliad.project_teams ADD CONSTRAINT project_teams_role_check
CHECK (role IN (
'lead', 'associate', 'pa', 'of_counsel',
'local_counsel', 'expert', 'observer'
));