Files
paliad/internal
m 3a41aa9209 feat(approvals/t-paliad-160 slice1+2): split policy + 409 handler
m's locked redesign (2026-05-08 16:40): replace `required_role` (with
'none' sentinel) with two columns — `requires_approval boolean` (the
gate) + `min_role text` (the seniority threshold). Cleanly separates
"approval applies at all" from "who's allowed to approve".

M1 phase: additive migration 064 adds the columns, backfills from the
legacy required_role ('none' → false/NULL; else → true/role), and
rewrites paliad.approval_policy_effective() to most-strict-wins:
  - requires_approval := bool_or across project + ancestor + unit_default
  - min_role          := MAX(approval_role_level) among requires_approval=true
The legacy required_role column survives this slice as a dual-read
mirror (resolver returns it too) so any caller that hasn't cut over
keeps working. M2 will drop required_role.

Service layer (approval_service.go): LookupPolicy + GetEffectivePolicyOne
read the new columns; UpsertProjectPolicySplit / UpsertUnitPolicySplit
accept the new shape directly; legacy UpsertProjectPolicy /
UpsertUnitPolicy stay as thin shims that map required_role through
splitFromLegacy(). ApplyMatrixToDescendants writes both columns.

Handler 409 mapping (§B): writeServiceError now consults a shared
mapApprovalError() helper before falling through to the generic 500.
ErrConcurrentPending → HTTP 409 with body
{code: "awaiting_approval", message, request_id?, required_role?}.
PendingApprovalError wraps ErrConcurrentPending with the in-flight
request id + role so the UI knows which request to point a withdraw
button at. ErrNoQualifiedApprover, ErrSelfApproval, ErrNotApprover,
ErrRequestNotPending all mapped consistently. writeApprovalError
now defers to the same helper for shape consistency.

Models: ApprovalPolicy + EffectivePolicy gain RequiresApproval/MinRole
fields. RequiredRole stays as a dual-read mirror until M2.

Tests: TestMapApprovalError_* covers the four 409/403 branches and the
"no match — fall through" case. Existing approval service tests pass
unchanged.

Defers per task spec to follow-up slices:
  - A3 (admin UI 2-control flip)
  - C+E (badge + withdraw button on detail pages)
  - D   (/inbox Meine Anfragen visibility fix)
  - M2  (drop required_role column)
2026-05-08 16:54:45 +02:00
..