Status profiles: restrict, never widen

The state machine stays coded; a status profile only removes transitions, never adds any, and can require a minimum role per transition. Simulated before activation, just like a business rule.

The principle

The state machine stays in the code

Every document with a firm-status workflow — purchase requests, quotes, manufacturing orders, nonconformities, and the tables the generic CRUD handler firms (invoices, purchase orders, sales orders, delivery notes, credit notes, receptions, consumption vouchers) — already has a coded state machine: a Go ValidTransitions graph per module (procurement, production, quality…), registered at startup. A status profile never touches that graph. It can only remove a transition the code allows, never add one the code doesn't — the same asymmetry a business rule has toward the write it guards: restrict, never widen.

A single application point enforces it: Autoriser (pkg/statuts), called both by the generic CRUD handler's firming route and by every dedicated status-transition route (manufacturing orders, quotes, purchase requests, nonconformities) — the same shared-guard pattern as pkg/documentsrequis.GardeOF or pkg/sod.Verifier for the segregation-of-duties rule type.

Settings › Status Profiles

The transition matrix

The screen lives under Settings → Status Profiles, one table at a time (GET /api/v1/status-machine/{table}). For a table with a known coded graph, a checkbox matrix shows every coded from → to transition, checked by default — unchecking a box restricts it, and unchecking is the only action the screen allows: it is not possible to add a checkbox for a transition the graph does not contain.

Settings > Status Profiles screen, Purchase requests table, transition matrix with every coded from/to combination

The matrix only shows transitions the code already allows; unchecking restricts, nothing can be added.

Eleven tables carry a profile today:

purchase_requestsquotesmanufacturing_ordersnon_conformitiesconsumption_vouchersinvoicespurchase_orderssales_ordersdelivery_notescredit_notesreceptions

For a table with no known coded graph, the screen falls back to a free list of explicitly allowed transitions instead of a matrix — same restriction principle, different input shape.

A role per transition

Requiring a minimum role

Beyond restricting which transitions exist, a profile can require a minimum role for a specific transition (roles_par_transition, keyed from:to → role) — a purchasing request moving from submitted to approved can be reserved to a purchasing-management role, independently of the amount-based approval a business rule already enforces on the same document. The two checks stack: a business rule can require an approval workflow, and a status profile can additionally require the role that closes it.

Simulate before restricting

Replaying the last 90 days

Exactly like a business rule, a saved profile can be simulated before its assignment is turned on: POST /api/v1/status-profiles/{id}/simulate replays the last 90 days of real transitions on that table and reports how many would have been refused under the restricted profile — evaluated, refused, and a handful of examples with their from → to and date. Nothing is written; the same “observe before hardening” discipline the business-rules pages describe.

Simulation result (excerpt)
POST /api/v1/status-profiles/{id}/simulate  { "jours": 90 }

{
  "table": "purchase_requests",
  "profil": "Purchasing — restricted",
  "jours": 90,
  "disponible": true,
  "total": 86,
  "refusees": 4,
  "exemples": [
    { "record_id": "DA-2026-0031", "de": "submitted", "vers": "approved", "date": "2026-08-12" }
  ]
}

Scope and precedence

Customer, supplier, document type, tenant

A profile only applies once assigned (status_profile_assignments), and an assignment carries a scope: tenant by default, or narrowed to one customer, one supplier, or a document-type classification (e.g. a nonconformity's type) — the last resolved from the document itself, from a bounded single-row read (LireContexteDocumentEnBase), never a second copy of the document's data. When several assignments could match the same document, the most specific one wins: customer, then supplier, then document type, then tenant — the customer and supplier scopes target a third party, document type targets a category, and a third party is treated as more specific than a category.

For the document-type scope, whose source classification is free text in the database (e.g. non_conformities.type), the assignment's scope_id stays a UUID: ScopeIDTypeDocument derives a stable, deterministic UUID (v5) from the chosen label, so the settings screen can compute the exact same identifier without asking a consultant to type one in by hand.

Back to Business Rules and Control
See also : Deployment · Pilotage and risk signals · The copilot