Status lifecycle — validated transitions, post-commit hooks
Every job moves through 5 stable states. Each transition is validated by a state machine, stamped in job_status_history with user + timestamp + comment, and fires post-commit hooks that drive billing, notifications, and downstream automation.
The state machine
Valid transitions
| From | To | Allowed? | Trigger |
|---|---|---|---|
| not_started | in_progress | ✓ | Manual or first task tick (cascade) |
| in_progress | completed | ✓ | Manager: all phases complete + completion gate green |
| completed | archived | ✓ | Auto on archive build, or manual |
| any | cancelled | ✓ with reason | Reason > 50 chars; super_admin/partner only |
| completed | in_progress | ✗ (rejected) | Use unlock-with-reason instead |
| archived | any | ✗ | Read-only forever |
| not_started | completed | ✗ (skip rejected) | Must walk in_progress first |
Rejection at the service layer means the UI dropdown will simply not show the disallowed option. Direct API calls return 422 with explicit reason.
Post-commit hooks
When a transition succeeds, a chain of side-effects fires after the DB commit (so they can't roll back the transition):
Notification to partner + manager + assigned employees per user_notification_preferences.
Stamps actual_start_date if blank. Drives WIP reports.
Auto-drafts a tax invoice using template fee + client T&Cs. Sends notification with deep-link.
Internal notification to partner. If client has portal access, email goes out via M17 (template compliance.expiry_reminder or custom).
Triggers VaultBackupService::buildArchive for the engagement; SHA-256 ZIP placed in storage/archives/.
All open invoices flagged for review. Audit-log records who, when, why.
Completion gate (the green flag)
Manager cannot transition to completed unless these all pass (audit jobs only — non-audit templates skip):
- All 12 workpapers signed through partner (or EQCR if listed)
- 0 open review points
- 0 outstanding mandatory disclosures
- Σ passed_unrecorded AJEs < overall materiality
- Adjusted TB still balances
- FS pack + signed audit report present in
storage/jobs/{id}/05-Audit-Report/
Failed gates show as red blockers on the Audit Completion screen; transition button is disabled until cleared.
On a fresh audit job, try transitioning not_started → completed directly via the dropdown. The system rejects with "Invalid transition: must pass through in_progress." Walk through the proper sequence and watch each hook fire in Activity tab.
Don't cancel a job to "save" effort. Cancellation is a meaningful event — it triggers invoice review, notifies the partner, and leaves a permanent forensic trail. If the work is just paused, use blocked on the affected tasks instead. cancelled is for "we're not doing this engagement at all" — onboarding fell through, client withdrew, conflict discovered post-acceptance.
Three firm-wide settings control auto-invoicing: m11.auto_invoice_on_completion (default ON), m11.require_job_completion_for_invoice (default OFF — flip ON to block manual invoicing on incomplete jobs), m11.notify_on_completion (default ON). Per-engagement overrides live on the job record.