Order-to-cash, with the health tail
The NS sales order lifecycle is the canonical revenue flow — from "Driscoll emails an order" to "Driscoll pays the invoice" to "their churn risk score updates". Eleven phases, because we don't stop at payment; the AR aging and customer health signals are part of the same flow.
The platform's job here is mostly the bookends: intake validation and customer-health output. NS owns the middle — pick, pack, ship, invoice, payment. But every step writes events to the ledger, so consumers downstream (customer health watcher, anomaly detector, timeline UI) can react.
Diagram: ns-sales-order-lifecycle.html. One STUB: EDI 850 intake. We have customers (NYC DOE in particular) who can send orders via EDI 850, but the auto-parse isn't wired — the EDI file lands in a mailbox and an operator keys it into NS. The other three channels (manual NS UI, customer portal, email-to-order) are real.
Trigger conditions
- Customer orders via NS UI (most common today — CS desk enters from email/phone).
- Customer self-serves via the customer portal hub page.
- Customer emails an order, the inbound triage workflow extracts line items, stages as draft SO for confirm.
- Customer transmits EDI 850 — STUB — falls back to manual entry by operator.
- Bid award triggers a release order against a bid contract (e.g. B5875 weekly draws).
Auto-approve unless: margin < guardrail OR order_value > auto_threshold OR new customer. Otherwise status='Pending Approval' and a HITL card lands in Mike's queue.
Driscoll weekly draw against B5875
Thursday 14:00. Driscoll (NS customer #2147, 36.4% of revenue) emails the week's draw against the B5875 bid: 14 line items, $18,420. Inbound email triage workflow parses the body, matches lines against bid_lines, stages draft SO. CS desk confirms; SO posts. Customer validation: credit_limit $250K, current AR $42K, not on hold — pass. Inventory check: all 14 lines have sufficient on-hand at the NJ DC. Pricing applies the bid contract; margin rolls up at 18.4% — above guardrail. Order value over $15K threshold → Pending Approval. Mike taps Approve at 14:08 (knows Driscoll, knows the bid). Pick list prints at the warehouse 14:09.
Friday: pack + ship; itemfulfillment closes. CustInvc posted Friday EOD — $18,420 hits AR. Net 30 terms; payment expected June 22. Aging bucket: current. Customer health watcher consumes order.shipped and the 30-day rolling order_velocity stays flat (Driscoll's normal cadence). Health score unchanged.
Contrast scenario: payment doesn't arrive until July 8. AR aging bucket flips to "30-60". ar.bucket_moved event fires. Customer health watcher recomputes: order_velocity stable but DSO trending up; health score nudges from "stable" to "watch". Mike sees the watch flag on Monday's recap.
The eleven beats
-
01
SO intake — four channels
Sales orders arrive via four channels. Manual NS UI is most common; email/portal/EDI are expanding. STUB: EDI 850 auto-parse — today EDI files land in a mailbox and an operator keys them into NS. No EDI X12 parser is wired; no AS2 endpoint is configured.
-
02
Customer validation
Lookup the
customersrow, checkcredit_limit, checkhold_status, enforce service hold rules. If on hold, SO stages withstatus='On Hold'and a HITL card stages for review. -
03
Inventory check
Per
so_lines.item, validatequantityavailableat the fulfillment location. Short-pick flags raised; backorder logic surfaces. A short SO can split into shippable + backordered fragments. -
04
Pricing application
Lookup
pricing_master+customer_programsto apply the right tier and discounts. Compute line margin and roll up to order margin. Bid customers pick up their bid_line price, not the list price. -
05
SO approval gate
If
margin < guardrailORorder_value > auto_thresholdOR new customer,status='Pending Approval'. Otherwise auto-approve. The HITL card carries margin breakdown and customer recap for fast decisions. -
06
Pick
Pick list generated at the warehouse. NS
itemfulfillmentrecord opens withstatus='In Progress'. Pickers walk the list, mark exceptions (damaged, short). -
07
Pack + ship
Fulfillment record closes;
itemfulfillment.status='Shipped'. Inventory decremented per shipped qty. BOL and ASN generated. The carrier-portal handoff is manual today. -
08
Invoice
invoice_linesgenerated fromso_linesusing shipped qty, not ordered qty. NSCustInvcposted; AR balance updates. Invoice mirrored to D1 on the warm tier. -
09
Payment
Customer remits; NS
CustPymtposted.payment_applicationsrow matches the payment to one or more invoices. Short pays and overpays surface for manual reconciliation. -
10
AR aging
v_customer_ar_agingview recomputes from invoice + payment activity. Buckets: current, 30, 60, 90+.ar.bucket_movedevents fire when an invoice ages into a new bucket. -
11
Customer health signal
order_velocity_declinebaseline updates.customer_health_predictormodel recomputes churn risk. Signal surfaces to chat tools + admin-dashboard. Health changes (stable → watch → at-risk) emitcustomer.health_changed.
What's different after the cycle
- Customer received goods; we received payment (eventually).
- AR aging buckets reflect the current state of every invoice.
- Customer health score updated with new velocity + DSO data.
- Events ledger has a full audit trail per SO — replayable for any consumer.
What can go wrong
EDI files land in a mailbox; operator keys into NS. Risk: transcription errors, missed orders, slow turnaround for customers expecting same-day acknowledgment. Mitigation: limited EDI customers today (mostly NYC DOE). Long-term: AS2 endpoint + X12 parser as a Cloudflare Worker.
Pick list says 50 cases, picker finds 48. This is usually where inventory drift surfaces. Recovery: ad-hoc cycle count on the affected SKU; short-ship the SO with customer notification.
Customer pays an unexpected amount. payment_applications can't auto-match. Manual reconciliation queue surfaces these for Marie/Lewis.
Mike doesn't approve in time; warehouse blocks. The CS desk has a fallback: revise the SO to under the auto-threshold by splitting, or escalate. Detection: pick_list_pending dashboard tile.
Adjacent flows + diagrams
Code paths + invariants
| Concern | Where |
|---|---|
| SO mirror table | transactions WHERE recordtype='salesorder' |
| Line mirror | so_lines (1:N from transactions) |
| Pricing source | pricing_master, customer_programs, bid_lines |
| Approval thresholds | guardrails.so_margin_min, so_value_auto |
| AR view | v_customer_ar_aging |
| Health model | customer_health_predictor (R563) |
| Events emitted | order.* , ar.* , customer.health_changed |
| Sync tier | hot (2 min) for SO/invoice/payment |
| STUB — EDI 850 | no AS2 endpoint, no X12 parser |