Layer 3: Business Logic
Workflows that compose L2 components, enforce business rules, and own the unit of work behind every user action. L3 sits between the public API (L4) and the domain components (L2).
Principles
- One file per workflow. 170 workflows, one per user action. Verbose by design.
- Workflows compose L2 only, never each other. Shared logic revisited later (#360).
- Functions, not classes. One default export per file.
- L3 owns the transaction. Every workflow opens its own
withTransaction, including reads in v1 (#361). - Constraint split: L4 owns request shape (Zod), L3 owns business rules, DB owns data integrity.
- Typed errors (
NotFoundError,InUseError,InvalidTransitionError,ValidationError) extendL3Error; L4 maps each to an HTTP status (#362). - Cost cascade inlined into each calling workflow, not extracted into a shared helper. The current
backend/src/db/cost-cascade.tswill be unwound. - No Anthem-specific data in L3. Branding, addresses, PDF values, per-org config live in
organizations/organization_settingsand are read via L2. - Request-driven in v1. No workers, cron, or queue. Async lands when a workflow needs it.
- Idempotency out of scope for v1. Revisit when duplicate-submission or replay surfaces (#363).
Workflows
Every workflow has a globally unique W### ID assigned in canonical sidebar order. The numbering inside each domain still mirrors the user-actions inventory, so the same position in both docs points to the same action.
See the All workflows page for the full flat table (170 rows), or jump into a domain dropdown in the sidebar.
| Domain | Range |
|---|---|
| Platform | W001–W023 |
| Purchase Orders | W111–W128 |
| Work Orders | W129–W143 |
| Sales Orders | W144–W164 |
| Materials Inventory | W097–W103 |
| Finished Goods Inventory | W104–W110 |
| Customers | W044–W064 |
| Products | W076–W086 |
| Bill of Materials | W087–W091 |
| Vendors | W024–W029 |
| Materials | W065–W075 |
| Locations | W092–W096 |
| Contacts | W030–W043 |
Status to inventory-ledger reconciliation
Sales orders, purchase orders, and work orders drive the inventory ledgers (materials, finished goods) as a function of order status. Every create and every status update runs an idempotent reconciliation projector that makes the ledger match the order's current status:
- Forward. For the current status, the projector writes the set of ledger events that should be net-active (e.g. an SO at
Enteredshould haveDEMAND+ALLOCATE). Events already present are left alone — the write is keyed on the order item, so re-running is a no-op. - Backward / terminal. When a transition makes a previously-written event no longer desired (a regression, or a terminal status such as
Cancelled/Rejectedthat reverses everything), the projector emits the matchingREVERSE_*event rather than deleting history. The ledger stays append-only and fully auditable.
Because the projector is idempotent and computes the desired set from scratch each run, the same logic serves create, single-row update, and bulk import.
What the projector owns vs. what stays receipt-driven:
| Order | Status-driven events (projector) | Receipt-driven events (unchanged) |
|---|---|---|
| SO (FG ledger, per item) | DEMAND from Received+; ALLOCATE from Entered+; CONSUME from Shipped+; Rejected reverses all | — |
PO (materials or FG by target_type, per item) | ORDER for any status except Planning and Cancelled; Cancelled reverses | RECEIVE (W122) |
WO (FG ORDER per output; materials/FG DEMAND/ALLOCATE per BOM-snapshot input) | FG ORDER and DEMAND for any status except Draft and Cancelled; ALLOCATE from Production onwards (In Transit, Partial, Received, Completed); Cancelled reverses | CONSUME + PRODUCE (W137) |
Receipt-driven events stay where they are: PO RECEIVE is written when a receipt is recorded, and WO CONSUME / PRODUCE likewise. The status projector never touches them, so the two paths compose without double-counting.