feat(approvals): per-ticket approval requests (PMS-451 phase 1) #334

Merged
YousifShkara merged 2 commits from feat/PMS-451-ticket-approvals into main 2026-06-23 11:23:59 +02:00
Owner

Adds a ticket_approvals table + HTTP surface for the "this needs sign-off before I close it" loop that PSAs ask for on high-cost work, change-style tickets, and customer-billed escalations. A row is requested by the agent on the ticket and routed to either a named approver or any holder of an assigned role; the chosen approver decides via POST /api/v1/approvals/:id/decision. A pending-queue endpoint backs the top-bar "you have approvals waiting" badge.

Per-row authorisation is enforced at decision time, not on the listing: a caller who hits /approvals/pending only ever sees rows they could decide (named approver or matching role), and the decision endpoint re-checks the same scope so a guessed approval id from another tenant or role cannot be approved.

XOR on (approver_user_id, approver_role) is enforced at both the application layer (so the 422 carries a field-level message) and the database CHECK (so a misuse of the service constructor cannot land a malformed row). Pending queue lookups use partial indexes keyed on (tenant_id, approver_user_id) and (tenant_id, approver_role) respectively so the bag-of-approvals does not grow into a sequential scan as tenants accumulate decided history.

Phase 2 of PMS-451 (polymorphic approvals across change requests / quotes / time entries) is intentionally not in this PR. Ticket-scoped phase 1 keeps the query plan single-index-narrow and lets the SPA ship the per-ticket timeline read in one round trip; the polymorphic surface folds tickets in as a special case once the second consumer is real.

The integration test under tests/ticket_approvals.rs drives the full round trip (create role-assigned -> appear in pending queue -> reject XOR misuse -> approve -> reject second-decide), so the race against double-decide is caught at the route layer.

#PMS-451

Adds a `ticket_approvals` table + HTTP surface for the "this needs sign-off before I close it" loop that PSAs ask for on high-cost work, change-style tickets, and customer-billed escalations. A row is requested by the agent on the ticket and routed to either a named approver or any holder of an assigned role; the chosen approver decides via `POST /api/v1/approvals/:id/decision`. A pending-queue endpoint backs the top-bar "you have approvals waiting" badge. Per-row authorisation is enforced at decision time, not on the listing: a caller who hits `/approvals/pending` only ever sees rows they could decide (named approver or matching role), and the decision endpoint re-checks the same scope so a guessed approval id from another tenant or role cannot be approved. XOR on (approver_user_id, approver_role) is enforced at both the application layer (so the 422 carries a field-level message) and the database CHECK (so a misuse of the service constructor cannot land a malformed row). Pending queue lookups use partial indexes keyed on (tenant_id, approver_user_id) and (tenant_id, approver_role) respectively so the bag-of-approvals does not grow into a sequential scan as tenants accumulate decided history. Phase 2 of PMS-451 (polymorphic approvals across change requests / quotes / time entries) is intentionally not in this PR. Ticket-scoped phase 1 keeps the query plan single-index-narrow and lets the SPA ship the per-ticket timeline read in one round trip; the polymorphic surface folds tickets in as a special case once the second consumer is real. The integration test under tests/ticket_approvals.rs drives the full round trip (create role-assigned -> appear in pending queue -> reject XOR misuse -> approve -> reject second-decide), so the race against double-decide is caught at the route layer. #PMS-451
YousifShkara force-pushed feat/PMS-451-ticket-approvals from 5b3d6094a1
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 38s
Check / fmt + clippy + build + tests (pull_request) Successful in 1m13s
Integration / integration tests (pull_request) Successful in 4m4s
to 1a17f4340c
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 30s
Check / fmt + clippy + build + tests (pull_request) Successful in 1m14s
Integration / integration tests (pull_request) Successful in 4m54s
Create release / Create release from merged PR (pull_request) Successful in 1s
2026-06-23 11:18:55 +02:00
Compare
YousifShkara deleted branch feat/PMS-451-ticket-approvals 2026-06-23 11:23:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!334
No description provided.