feat(approvals): per-ticket approval requests (PMS-451 phase 1) #334
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
psa-systems/mokosh-server!334
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-451-ticket-approvals"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a
ticket_approvalstable + 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 viaPOST /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/pendingonly 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
5b3d6094a11a17f4340c