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.

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.
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
