feat(workflows): ticket.status_changed + ticket.priority_changed triggers (PMS-448 phase 2) #343

Merged
YousifShkara merged 1 commit from feat/PMS-448-phase2-more-triggers into main 2026-06-23 11:50:18 +02:00
Owner

Widens the workflow executor with the two next-most-asked triggers. The data model from Phase 1 already supported them (the trigger_event column is a VARCHAR with no CHECK and the executor already pulls rules by trigger string), so this is a pure executor + service-allow-list extension; no migration needed.

Two new context shapes and two new public entry-points:

  • TicketStatusChangedContext adds from_status_id and to_status_id as condition keys on top of the create-context surface. Lets a rule say "fire only when the ticket moved INTO closed" (to_status_id: [closed]) or "fire only when it moved OUT of pending" (from_status_id: [pending]).
  • TicketPriorityChangedContext adds from_priority_id and to_priority_id. Same shape.

Posture is LOG-ONLY: matching rules write a workflow_rule_runs row so the operator audit timeline captures the firing, but no mutating actions run on the transition yet. Mutating actions on transitions are scoped for Phase 3 once the SPA rule-builder UI has had time to mature - shipping mutations now would let an operator accidentally create a self-firing loop (a rule on status_changed that mutates status would re-trigger itself indefinitely), and the SPA's only debugging surface today is "look at the audit log".

TicketService::update_ticket calls the two executors at the bottom of the update transaction, AFTER all the individual UPDATEs land but BEFORE commit. That means a rule firing on a status transition commits atomically with the transition itself; rollback drops both. Each trigger only fires when the new value differs from the captured pre-update value, so a PATCH that supplies status_id with the same id it already had does NOT spuriously fire.

The create-rule allow-list (RECOGNISED_TRIGGERS in models.rs) widens to include both new triggers. The integration test under tests/workflow_rules_phase2.rs pins the AND semantics across condition keys (a priority-changed rule with from_priority_id matching but to_priority_id mismatched does NOT fire), confirms the status-changed rule DOES fire on the matching transition, and asserts the create-rule endpoint accepts both new trigger names + still 400s on an unknown one.

Phase 3 follow-ups still on PMS-448:

  • mutating actions on transition triggers (with cycle detection / depth cap so a status-mutating status_changed rule cannot re-trigger itself);
  • additional triggers (time_entry.created, invoice.paid, ...);
  • richer condition operators (LIKE, range, NOT IN);
  • the SPA rule-builder UI.

#PMS-448

Widens the workflow executor with the two next-most-asked triggers. The data model from Phase 1 already supported them (the `trigger_event` column is a VARCHAR with no CHECK and the executor already pulls rules by trigger string), so this is a pure executor + service-allow-list extension; no migration needed. Two new context shapes and two new public entry-points: - `TicketStatusChangedContext` adds `from_status_id` and `to_status_id` as condition keys on top of the create-context surface. Lets a rule say "fire only when the ticket moved INTO closed" (`to_status_id: [closed]`) or "fire only when it moved OUT of pending" (`from_status_id: [pending]`). - `TicketPriorityChangedContext` adds `from_priority_id` and `to_priority_id`. Same shape. Posture is LOG-ONLY: matching rules write a `workflow_rule_runs` row so the operator audit timeline captures the firing, but no mutating actions run on the transition yet. Mutating actions on transitions are scoped for Phase 3 once the SPA rule-builder UI has had time to mature - shipping mutations now would let an operator accidentally create a self-firing loop (a rule on `status_changed` that mutates status would re-trigger itself indefinitely), and the SPA's only debugging surface today is "look at the audit log". `TicketService::update_ticket` calls the two executors at the bottom of the update transaction, AFTER all the individual UPDATEs land but BEFORE commit. That means a rule firing on a status transition commits atomically with the transition itself; rollback drops both. Each trigger only fires when the new value differs from the captured pre-update value, so a PATCH that supplies `status_id` with the same id it already had does NOT spuriously fire. The create-rule allow-list (`RECOGNISED_TRIGGERS` in models.rs) widens to include both new triggers. The integration test under `tests/workflow_rules_phase2.rs` pins the AND semantics across condition keys (a priority-changed rule with `from_priority_id` matching but `to_priority_id` mismatched does NOT fire), confirms the status-changed rule DOES fire on the matching transition, and asserts the create-rule endpoint accepts both new trigger names + still 400s on an unknown one. Phase 3 follow-ups still on PMS-448: - mutating actions on transition triggers (with cycle detection / depth cap so a status-mutating status_changed rule cannot re-trigger itself); - additional triggers (`time_entry.created`, `invoice.paid`, ...); - richer condition operators (LIKE, range, NOT IN); - the SPA rule-builder UI. #PMS-448
feat(workflows): ticket.status_changed + ticket.priority_changed triggers (PMS-448 phase 2)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 1m26s
E2E / Playwright against staging (pull_request) Successful in 1m58s
Integration / integration tests (pull_request) Successful in 4m57s
Create release / Create release from merged PR (pull_request) Successful in 4s
9d2892786c
Widens the workflow executor with the two next-most-asked triggers. The data model from Phase 1 already supported them (the `trigger_event` column is a VARCHAR with no CHECK and the executor already pulls rules by trigger string), so this is a pure executor + service-allow-list extension; no migration needed.

Two new context shapes and two new public entry-points:

  - `TicketStatusChangedContext` adds `from_status_id` and `to_status_id` as condition keys on top of the create-context surface. Lets a rule say "fire only when the ticket moved INTO closed" (`to_status_id: [closed]`) or "fire only when it moved OUT of pending" (`from_status_id: [pending]`).
  - `TicketPriorityChangedContext` adds `from_priority_id` and `to_priority_id`. Same shape.

Posture is LOG-ONLY: matching rules write a `workflow_rule_runs` row so the operator audit timeline captures the firing, but no mutating actions run on the transition yet. Mutating actions on transitions are scoped for Phase 3 once the SPA rule-builder UI has had time to mature - shipping mutations now would let an operator accidentally create a self-firing loop (a rule on `status_changed` that mutates status would re-trigger itself indefinitely), and the SPA's only debugging surface today is "look at the audit log".

`TicketService::update_ticket` calls the two executors at the bottom of the update transaction, AFTER all the individual UPDATEs land but BEFORE commit. That means a rule firing on a status transition commits atomically with the transition itself; rollback drops both. Each trigger only fires when the new value differs from the captured pre-update value, so a PATCH that supplies `status_id` with the same id it already had does NOT spuriously fire.

The create-rule allow-list (`RECOGNISED_TRIGGERS` in models.rs) widens to include both new triggers. The integration test under `tests/workflow_rules_phase2.rs` pins the AND semantics across condition keys (a priority-changed rule with `from_priority_id` matching but `to_priority_id` mismatched does NOT fire), confirms the status-changed rule DOES fire on the matching transition, and asserts the create-rule endpoint accepts both new trigger names + still 400s on an unknown one.

Phase 3 follow-ups still on PMS-448:
- mutating actions on transition triggers (with cycle detection / depth cap so a status-mutating status_changed rule cannot re-trigger itself);
- additional triggers (`time_entry.created`, `invoice.paid`, ...);
- richer condition operators (LIKE, range, NOT IN);
- the SPA rule-builder UI.

#PMS-448
YousifShkara deleted branch feat/PMS-448-phase2-more-triggers 2026-06-23 11:50:18 +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!343
No description provided.