Commit Graph

2 Commits

Author SHA1 Message Date
m
e92c56b5f8 feat(t-paliad-154) commit 1.5: extend migration 062 with policy_audit_log
Q8 of locked design: policy CRUD audits to /admin/audit-log only, NOT
to per-project /verlauf. The 4 existing audit sources (project_events,
caldav_sync_log, reminder_log, partner_unit_events) don't fit cleanly:
project_events would surface on /verlauf (rejected by Q8); partner_unit_events
constrains event_type and requires unit_name + a non-null partner_unit_id
which doesn't fit project-scoped policy changes.

Added paliad.policy_audit_log as a fifth audit source — admin-only, scoped
either to a project or a partner unit, snapshots scope_name so post-cascade
rows still render. RLS: select for any authenticated user (route gate is
the actual control); write for global_admin only.

AuditService.ListEntries will union this source in commit 2 of this PR.

Validated insert/select live in BEGIN ... ROLLBACK.
2026-05-08 02:13:58 +02:00
m
f7908f03ad feat(t-paliad-154) commit 1/5: migration 062 — approval_policies unit-defaults + 'none' sentinel + resolver + seed
Schema:
- ALTER paliad.approval_policies: project_id nullable, ADD partner_unit_id
  uuid REFERENCES paliad.partner_units(id) ON DELETE CASCADE.
- XOR check: exactly one of (project_id, partner_unit_id) is set.
- Replace UNIQUE composite with two partial unique indexes (one per scope).
- Extend required_role CHECK with 'none' sentinel.
- approval_role_level('none') already returns 0 via existing ELSE branch
  in 059_profession_vs_responsibility.up.sql:218 — no function update.

Resolver paliad.approval_policy_effective(project, entity_type, lifecycle):
- Step 1: project-specific row wins outright (any value, including 'none').
- Step 2: MAX(approval_role_level) across ancestor rows on project's path
  + unit-default rows for partner units attached to project. Tied levels
  break alphabetically ('ancestor' beats 'unit_default') for stable
  attribution.
- Step 3: zero rows (no candidates) — caller treats as 'no policy applies'.

Returns (required_role, source, source_id) — source ∈ {project, ancestor,
unit_default}; source_id is project_id or partner_unit_id depending.

Seed:
- 8 rows × every existing partner_unit (currently 11): deadline+appointment
  × create/update/delete = associate; complete = none.
- ON CONFLICT (partner_unit_id, entity_type, lifecycle_event)
  WHERE partner_unit_id IS NOT NULL DO NOTHING — idempotent on re-run
  (verified live: 11 units → 88 seed rows, second run is no-op).
- Safe on a DB with 0 partner_units (SELECT returns no rows).

Down migration: reverse-order. Coerces 'none' rows to 'associate' before
restoring CHECK so rollback works without data loss. Drops seeded unit
rows; preserves project rows that pre-date 062.

Validated end-to-end against the live DB inside BEGIN ... ROLLBACK; the
existing project policy (deadline:create=partner) is preserved by the
DO NOTHING clause and the partial-index scope.

Design: docs/design-approval-policy-ui-2026-05-07.md §3.1.

No RAISE EXCEPTION. No bare CSS tokens (no CSS in this commit).
2026-05-08 02:11:23 +02:00