feat(workflows): ticket.created workflow rules with in-line executor (PMS-448 phase 1) #341

Merged
YousifShkara merged 1 commit from feat/PMS-448-ticket-created-rules into main 2026-06-23 11:40:23 +02:00
Owner

Adds the first cut of the workflow engine the ticket asked for. A focused single trigger (ticket.created), structured conditions on the new ticket's well-known dimensions (priority / queue / company / source / type), and structured actions that mutate the same row (assign_to_user_id, assign_to_team_id, set_priority_id, add_tag, add_internal_note). The TicketService's create path runs the matching rules in-line INSIDE the same transaction, so an action that reassigns the ticket commits atomically with the ticket row - there is no observable window in which the ticket exists without its workflow side-effects.

Two tables ship:

  • workflow_rules - the operator-facing rule definitions. Conditions + actions are JSONB so the SPA's rule-builder UI can iterate without a server redeploy; the executor validates them at run time. priority (lower runs first) + tie-break on created_at gives deterministic ordering. Phase 2 will widen trigger_event to other events (ticket.status_changed, ticket.priority_changed, time_entry.created); the data model already supports it.

  • workflow_rule_runs - one row per rule the executor processed against a given entity. Captures the actions blob that was applied + any per-action error so the SPA can render "what auto-routed this ticket" on the ticket detail page. A buggy rule's failure lands here as the error field rather than aborting the create; the new /api/v1/tickets/{id}/workflow-runs endpoint surfaces the timeline.

CRUD endpoints under /api/v1/workflow-rules are admin-only because the rule definitions can reassign tickets and add internal notes - that surface should not be reachable by a typical agent. The RequireAdmin extractor is wrapped with a parallel RequireAuth so the handler can pull the user id out of the auth state without taking the inner tuple of RequireRole.

Conditions evaluation has a deliberate fail-closed posture: an unknown key (typo) returns "miss", not "match", so a bad rule does not silently fire on every ticket. Unit tests under executor.rs pin the matrix (empty=match-all, in-list-match, out-of-list miss, AND across keys, source string match, unknown key miss). The integration test under tests/workflow_rules.rs exercises the full pipeline (real ticket POST, real rule SELECT, real action UPDATE, real workflow_rule_runs INSERT) and confirms a mismatched rule produces NO run row while a matching rule produces exactly one.

Phase 2 of PMS-448, tracked under the same ticket:

  • additional triggers (ticket.status_changed, ticket.priority_changed, time_entry.created);
  • richer condition operators (LIKE, range, NOT IN);
  • richer actions (send email via the notifications dispatcher, create a follow-up ticket, set a custom field);
  • the SPA rule-builder UI.

#PMS-448

Adds the first cut of the workflow engine the ticket asked for. A focused single trigger (`ticket.created`), structured conditions on the new ticket's well-known dimensions (priority / queue / company / source / type), and structured actions that mutate the same row (`assign_to_user_id`, `assign_to_team_id`, `set_priority_id`, `add_tag`, `add_internal_note`). The TicketService's create path runs the matching rules in-line INSIDE the same transaction, so an action that reassigns the ticket commits atomically with the ticket row - there is no observable window in which the ticket exists without its workflow side-effects. Two tables ship: - `workflow_rules` - the operator-facing rule definitions. Conditions + actions are JSONB so the SPA's rule-builder UI can iterate without a server redeploy; the executor validates them at run time. `priority` (lower runs first) + tie-break on `created_at` gives deterministic ordering. Phase 2 will widen `trigger_event` to other events (`ticket.status_changed`, `ticket.priority_changed`, `time_entry.created`); the data model already supports it. - `workflow_rule_runs` - one row per rule the executor processed against a given entity. Captures the actions blob that was applied + any per-action error so the SPA can render "what auto-routed this ticket" on the ticket detail page. A buggy rule's failure lands here as the `error` field rather than aborting the create; the new `/api/v1/tickets/{id}/workflow-runs` endpoint surfaces the timeline. CRUD endpoints under `/api/v1/workflow-rules` are admin-only because the rule definitions can reassign tickets and add internal notes - that surface should not be reachable by a typical agent. The `RequireAdmin` extractor is wrapped with a parallel `RequireAuth` so the handler can pull the user id out of the auth state without taking the inner tuple of `RequireRole`. Conditions evaluation has a deliberate fail-closed posture: an unknown key (typo) returns "miss", not "match", so a bad rule does not silently fire on every ticket. Unit tests under `executor.rs` pin the matrix (empty=match-all, in-list-match, out-of-list miss, AND across keys, source string match, unknown key miss). The integration test under `tests/workflow_rules.rs` exercises the full pipeline (real ticket POST, real rule SELECT, real action UPDATE, real `workflow_rule_runs` INSERT) and confirms a mismatched rule produces NO run row while a matching rule produces exactly one. Phase 2 of PMS-448, tracked under the same ticket: - additional triggers (`ticket.status_changed`, `ticket.priority_changed`, `time_entry.created`); - richer condition operators (LIKE, range, NOT IN); - richer actions (send email via the notifications dispatcher, create a follow-up ticket, set a custom field); - the SPA rule-builder UI. #PMS-448
YousifShkara force-pushed feat/PMS-448-ticket-created-rules from c8b789e97a
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Failing after 19s
E2E / Playwright against staging (pull_request) Successful in 53s
Integration / integration tests (pull_request) Successful in 4m19s
to 7f28f3475b
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 1m5s
Check / fmt + clippy + build + tests (pull_request) Successful in 3m12s
Integration / integration tests (pull_request) Successful in 10m3s
Create release / Create release from merged PR (pull_request) Successful in 3s
2026-06-23 11:16:50 +02:00
Compare
YousifShkara deleted branch feat/PMS-448-ticket-created-rules 2026-06-23 11:40:23 +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!341
No description provided.