The system of three — Mike's framing
Three NetSuite transaction lifecycles run the business. Together they are the operating system; separately they are easy to misunderstand.
- Sales Order — the customer side of the transaction. Starts with the customer order and ends with customer fulfillment, invoicing, payment, and order closure.
- Purchase Order — the vendor side of the transaction. Used when the company needs to buy product, ingredients, packaging, components, finished goods, or drop-ship items. Ends with Item Receipt, vendor bill entry, vendor payment, and PO closure.
- Work Order — the production side of the transaction. Used when the company needs to assemble or build finished goods. Ends when Assembly Build is completed and finished goods are added back into inventory.
The three flows interconnect through two cross-lane bridges and one universal convergence. SO Path 2 (assembly) creates a Work Order from the SO — WO.createdfrom = SO.id preserves the link. SO Path 3 (dropship) creates a Purchase Order from the SO — PO.createdfrom = SO.id preserves the link. WO Path 1 (from-SO) is the consuming side of SO Path 2; PO Path 1 (SO-connected) is the consuming side of SO Path 3. All three converge at the Finance handoff post-fulfillment / post-receipt. Finance reconciles SO + PO + WO at monthly close.
Diagram: sales-purchase-work-order-integration.html. Lane masters: SO master · WO lifecycle. Cross-lane bridges live in SO Path 2 and SO Path 3.
Decision logic — when does an order spawn each combination?
A single Sales Order routinely spans multiple lifecycles because lines often differ in item type. The decision happens at SO entry — today operator-driven, tomorrow auto-classified.
| If the SO contains… | It spawns… | Bridge field |
|---|---|---|
| Only inventory items (already on hand) | SO only. No WO, no PO. Reserve → pick → pack → ship. | none — SO Path 1 only |
| One or more assembly items (need to build) | SO + WO chain. WO created from SO. Build then fulfill. | WO.createdfrom = SO.id |
| One or more dropship items (vendor ships direct) | SO + PO chain. PO created from SO. Vendor ships, Item Receipt posts, SO fulfills. | PO.createdfrom = SO.id |
| Mixed lines (most common at GFS) | SO + WO + PO. Single SO fans out into BOTH a WO and a PO, plus inventory pulls for any in-stock lines. | both createdfrom bridges |
After SO entry, each line is classified by item type. Inventory items stay in Lane 1 (SO Path 1). Assembly items spawn into Lane 3 (WO). Dropship / special-order items spawn into Lane 2 (PO). One SO can do all three.
All three lifecycles fire a Finance alert at their post-fulfillment / post-receipt moment. SO fires on Item Fulfillment. PO fires on Item Receipt. WO fires on close (with cost variance). Finance reconciles all three at monthly close.
Driscoll orders 50 cases of Right Start RS-12
Thursday 14:00. Driscoll (NS customer #2147, 36.4% of revenue) emails an order to orders@globalfoodsolutions.co: 50 cases of Right Start RS-12 plus 1 line for a specialty vendor item. Customer PO# is 72622.
Lane 1 SO entry (14:08): Team keys the SO into NetSuite, captures otherrefnum = "72622", NS-assigns tranid = "1217". Lines classified: 30 cases pull from existing RS-12 inventory (Path 1), 20 cases need to be built (Path 2 spawns WO), 1 specialty line drop-ships from vendor (Path 3 spawns PO).
Lane 3 WO spawn: 20-case Work Order created with WO.createdfrom = SO.id. WO entered, released, components reserved against the BOM. Production crew completes the Assembly Build Friday morning — HITL approval at /assembly-build.html, Mike taps Approve. 20 cases of RS-12 land back in inventory.
Lane 2 PO spawn: Purchase Order created with PO.createdfrom = SO.id, memo = "72622". PO transmitted to vendor Friday morning. Vendor ships direct to Driscoll's location Friday afternoon. Item Receipt posted against the PO — PO line closed.
Lane 1 Item Fulfillment converges (Friday 15:30): All 50 cases plus the specialty line ship to Driscoll. Single Item Fulfillment closes. otherrefnum = "72622" threads through. ★ Finance alert fires (SO side).
Lane 4 Finance reconciles: Finance reviews fulfillment, posts CustInvc for $18,420. Net 30 terms. Meanwhile, PO side: ★ Finance alert fires (PO side) on the Item Receipt, queuing 3-way match for the vendor bill (entered the following week, paid per vendor terms). WO side: ★ Finance roll-up fires on the WO close, booking the build variance into COGS.
Three lifecycles, one customer order, one Finance reconcile. June 22 (Net 30): Driscoll pays, events.order.closed fires. The customer PO# 72622 traces across SO · PO · IF · IR · Invoice · Vendor Bill via the universal thread.
Four scenarios — from simplest to most complex
Same SO entry, four different fan-out shapes. The path classification at step 04 decides which lifecycles fire.
Scenario 1 — Simple inventory order (SO only)
-
01
SO entered with all-inventory lines
All lines fall under
inventory_item. No assembly, no dropship. Path 1 only. -
02
Lane 1 SO Path 1 runs
Reserve qty → pick → pack → stage → Item Fulfillment closes. Inventory decrements.
-
03
★ Finance handoff fires once
SO side only. Invoice posted, AR open, payment received, SO closed. PO and WO lanes never engaged.
Scenario 2 — Assembly order (SO + WO chain)
-
01
SO entered with assembly lines
One or more lines are
assembly_item. SO Path 2 dispatches. -
02
Lane 3 WO spawned from SO
WO.createdfrom = SO.id. WO entered → released → components reserved against BOM. -
03
Assembly Build completes
HITL at
/assembly-build.html. BOM consumed, FG credited back to inventory. WO closes. -
04
Lane 1 SO fulfills from new FG
Now that the assembly is on hand, the SO ships. Item Fulfillment closes.
-
05
★ Finance handoff fires twice
WO close fires the WO-side roll-up (COGS + variance). SO fulfillment fires the SO-side alert (invoice posting).
Scenario 3 — Dropship order (SO + PO chain)
-
01
SO entered with dropship lines
One or more lines are
dropship_itemor special-order. SO Path 3 dispatches. -
02
Lane 2 PO spawned from SO
PO.createdfrom = SO.id.PO.memo = customer PO#. PO transmitted to vendor. -
03
Vendor ships · Item Receipt posted
Vendor ships direct to customer (or to GFS crossdock).
IR.createdfrom = PO.id. PO line closes. -
04
Lane 1 SO fulfills
Item Fulfillment closes for the dropship line.
-
05
★ Finance handoff fires twice
PO side: Item Receipt queues 3-way match for vendor bill entry. SO side: fulfillment fires invoice posting. Both alerts are by design — not duplicates.
-
06
Vendor bill cycle runs in parallel
Finance enters
VendBillagainst PO. Paid per vendor terms (separate cycle from customer-side invoice).
Scenario 4 — Mixed order (SO + WO + PO, most common)
-
01
SO entered with multi-line, multi-type
SO has lines that fall into ALL three buckets — some inventory, some assembly, some dropship. This is the common case at GFS.
-
02
The SO fans out into BOTH a WO AND a PO from the same SO
Path 2 spawns Lane 3 WO with
WO.createdfrom = SO.id. Path 3 spawns Lane 2 PO withPO.createdfrom = SO.id. Path 1 lines pull from existing inventory in parallel. All three threads run concurrently. -
03
Production + vendor + inventory all converge
WO closes → FG in inventory. PO ships → Item Receipt posts. Inventory lines stay reserved. Once all three threads are ready, Lane 1 Item Fulfillment closes for the whole SO.
-
04
★ Finance handoff fires THREE TIMES
SO-side alert (fulfillment), PO-side alert (Item Receipt), WO-side alert (close + variance). Finance reconciles all three at monthly close. The customer PO# threads through every record.
What's different after the cycle
- Customer received the entire mixed order in a single shipment (or coordinated drops).
- FG inventory reflects new assembly builds; raw inventory reflects component consumption.
- AR open for customer-side invoice; AP open for vendor-side bill; COGS adjusted for build variance.
- Customer PO# threads through every record — single grep lights up the whole chain.
- Events ledger:
so.created,workorder.created,purchaseorder.created,itemfulfillment.completed,so.finance_alert_fired,po.finance_alert_fired,wo.closed,order.closed,po.closed.
What can go wrong — cross-lane edition
Production finished the assembly, FG credited, but the SO still shows "Pending Fulfillment". Most likely WO.createdfrom got dropped or D1 mirror is stale. See runbook scenario below.
Vendor confirmed shipment, customer received goods, but no Item Receipt posted against the PO. Customer-side SO is blocked from fulfilling. Manual reconcile required.
SO carries otherrefnum = "72622" but the PO created from it has blank memo, or the Vendor Bill is missing the thread. Trace via createdfrom chain to find the broken link.
Expected for mixed / Path 3 orders. SO fulfillment alert + PO Item Receipt alert (+ WO close alert if Path 2). Design intent, not a bug. Finance UI should visually de-duplicate per customer/SO.
SO carries otherrefnum; PO carries memo; WO has no canonical place yet. Expected pattern is bodyFields.memo, but Mike's review is pending. Until then, the WO step in the universal thread is implicit (via createdfrom) not explicit.
Adjacent docs + diagrams
Code paths + invariants
| Concern | Where |
|---|---|
| SO master worker route | src/index.ts — /api/so/create + /api/salesorder/:id |
| Inbound email pipeline | src/email.ts |
| SO → WO bridge | workorder.createdfrom = transactions.id (SO) |
| SO → PO bridge | purchaseorder.createdfrom = transactions.id (SO) |
| SO → IF inherit | itemfulfillment.createdfrom = so.id · inherits otherrefnum |
| PO → IR inherit | itemreceipt.createdfrom = po.id · inherits memo |
| PO → Bill inherit | vendorbill.createdfrom = po.id · inherits memo / tranid |
| Convergence (SO side) | itemfulfillment.status = 'Shipped' fires SO finance alert |
| Convergence (PO side) | itemreceipt.posted fires PO finance alert |
| Convergence (WO side) | events.wo.closed + events.assembly.built fire WO finance roll-up |
| Sync tier | hot (2 min) for SO, WO, PO, fulfillment, receipt, invoice, bill, payment |
| STUB — WO PO# field | TBD — Mike review pending · expected bodyFields.memo |
| STUB — Finance NS automation | planned R594 (workflow build to deliver alerts into Finance review queue) |
| STUB — auto-classification | operator decides today; no /api/so/create classifier wired |
Dated trail · spot stale claims
Dated trail of when this doc was last touched, what changed, and what to look at if it feels stale.
| Date | Round | Change | Touched by |
|---|---|---|---|
2026-05-26 | R593 | First shipped — canonical integration view tying SO + PO + WO into one operating system. 4-lane swimlane diagram with cross-lane bridges + Finance convergence. 12-section wiki per R586 structure. Driscoll RS-12 mixed-order scenario walks all 3 lifecycles firing in coordination. | Mike + Claude |
The machine-readable spec
Canonical fields, table names, cross-record linkage. What code should match, what tests should assert.
Cross-record linkage (the bridge table)
| Cross-record linkage | Field | Carries |
|---|---|---|
| SO → WO | WO.createdfrom = SO.id | preserves the SO ↔ WO link |
| SO → PO | PO.createdfrom = SO.id | preserves the SO ↔ PO link |
| SO → Item Fulfillment | IF.createdfrom = SO.id | inherits otherrefnum |
| PO → Item Receipt | IR.createdfrom = PO.id | inherits memo |
| PO → Vendor Bill | Bill.createdfrom = PO.id | inherits memo, tranid |
| Customer PO# thread (universal) | otherrefnum (SO, Inv) ↔ memo (PO, Bill) ↔ memo (WO — TBD) | universal trace thread "72622" |
D1 tables involved (across the 3 lifecycles)
| Table | Lane | Purpose |
|---|---|---|
transactions (SalesOrd / PurchOrd / WorkOrd) | all | all three lifecycles live in one D1 table, differentiated by type |
so_lines / transaction_lines | Lane 1 | SO line detail; parent_so_id tracking |
work_orders | Lane 3 | WO records with createdfrom bridge to SO |
assembly_builds | Lane 3 | build completion records |
purchase_orders | Lane 2 | PO records with createdfrom bridge to SO |
item_fulfillments | Lane 1 converge | SO-side close point; createdfrom = so.id |
item_receipts | Lane 2 converge | PO-side close point; createdfrom = po.id |
vendor_bills | Lane 4 PO | AP open; createdfrom = po.id |
customer_invoices | Lane 4 SO | CustInvc; carries otherrefnum |
customer_payments / vendor_payments | Lane 4 | cash cycle records |
v_customer_ar_aging | Lane 4 | dunning trigger source |
inventory_balance | all | state shared across all 3 lanes — the actual ledger of stock |
events | all | so.*, po.*, wo.*, finance.* |
Events fired across the 3 lifecycles
| event_type | Lane | When |
|---|---|---|
so.created | Lane 1 | SO entered + mirrored |
workorder.created | Lane 3 | Path 2: WO spawned from SO |
purchaseorder.created | Lane 2 | Path 3: PO spawned from SO (or stock replen) |
itemfulfillment.completed | Lane 1 | SO ships — fires SO finance alert |
itemreceipt.posted | Lane 2 | Vendor ships — fires PO finance alert |
assembly.built | Lane 3 | Build complete (with variance) — fires WO finance roll-up |
so.finance_alert_fired | Lane 4 | SO → Finance handoff |
po.finance_alert_fired | Lane 4 | PO → Finance handoff |
wo.closed | Lane 3 | WO complete — fires WO finance roll-up |
order.closed | Lane 1 | SO Closed after payment apply |
po.closed | Lane 2 | PO Closed after vendor pay |
It broke at 2am — what now
Cross-lane incidents. Different from a single-lane failure — here the linkage between lifecycles is what broke.
Scenario · WO completed but SO not fulfilled (WO ↔ SO link broken)
Production finished the assembly build, FG credited to inventory, but the SO still shows "Pending Fulfillment". The cross-lane bridge dropped somewhere.
- Verify the bridge:
SELECT id, createdfrom, status FROM transactions WHERE type='WorkOrd' AND id=?—createdfromshould equal the originating SO id. - If createdfrom is NULL: the link is broken. Either rebuild via
UPDATE transactions SET createdfrom=? WHERE id=?or open the WO in NS UI and re-link to the SO. - Force SO resync:
POST /api/sync/run?tier=hot&table=transactions&id=<so_id> - Trigger fulfillment manually in NS: from the SO record, click "Fulfill" with the assembly now on hand.
- Verify the WO event fired:
SELECT * FROM events WHERE event_type='wo.closed' AND payload LIKE '%<wo_id>%'— if missing, re-emit.
Scenario · PO received but Item Receipt missing (vendor-side disconnect)
Vendor confirmed shipment; customer received goods (Path 3 dropship) or warehouse confirmed receipt. But no Item Receipt posted against the PO, so Lane 1 SO is blocked from fulfilling.
- Check PO state:
SELECT id, createdfrom, status, trandate FROM transactions WHERE type='PurchOrd' AND id=? - Check for orphan vendor bill:
SELECT * FROM vendor_bills WHERE po_id=?— if a bill exists without an IR, that's a sign of premature billing. Pause and reconcile. - Post Item Receipt manually: in NS, open the PO → click "Receive" with the received qty.
- Force resync:
POST /api/sync/run?tier=hot&table=transactionsto pull the new IR into D1. - Verify Lane 1 fulfills: after IR posts, the SO Item Fulfillment can complete. Trigger from the SO.
Scenario · Customer PO# missing on a downstream record
SO carries otherrefnum = "72622" but the PO created from it has blank memo, or the Vendor Bill is missing the thread. The universal trace is broken at some hop.
- Trace the chain:
SELECT type, id, createdfrom, otherrefnum, memo FROM transactions WHERE id IN (so_id, po_id, if_id, ir_id, bill_id)— find the first row with blank PO# in the thread. - Patch the field at NS: open the record in NS, fill in
memo(PO/Bill) orotherrefnum(SO/Invoice). - Resync D1: hot-tier sync the affected table.
- Verify the universal grep:
grep "72622"acrosstransactions.otherrefnum+purchase_orders.memo+customer_invoices.otherrefnum+vendor_bills.memo+vendor_bills.tranidshould now light up every record.
Scenario · Finance alert fires twice (once for receipt, once for fulfillment)
Finance reports they got two notifications for the same customer order on a Path 3 dropship. Or three for a mixed order with Path 2 + Path 3.
- This is the design intent — not a bug. Path 3 fires both a PO-side alert (on IR) and an SO-side alert (on IF) for the same line. Mixed orders also fire a WO-side roll-up.
- De-duplicate visually: Finance review UI should group alerts by customer/SO id so the operator sees one row with three sub-events.
- If Finance is over-paged: rate-limit the alert delivery (batch by 5 min window) but keep the events distinct in the ledger.
- Don't suppress alerts at source: each lifecycle's alert is independent — PO bill cycle is a separate cash cycle from SO invoice cycle. Suppressing one breaks AP reconcile.
Logs to check
events· cross-lifecycle event trail — trace byso_idorcustomer_idsync_log· D1 mirror freshness for transactions tablens_pending_pushes· write-backs queued but not yet drainedworkflow_run_log· assembly_build_review, ar_aging_action_plannpx wrangler tail· live Worker logs for /api/so/create and /api/sync/run
Kill switch · emergency stop
kill:ns_writes· stops every NS push platform-wide (halts all 3 lifecycles)kill:so_fanout· stops the SO → WO/PO fan-out (lifecycles still work independently)kill:finance_alerts· suppresses the ★ handoff (Finance fails closed, manual reconcile)
See kill-switches-state-machine.html for the full state machine + recovery procedure.
Escalation
Primary: Mike Levine (single-admin) · mikelevine@globalfoodsolutions.co. For prolonged cross-lane outage, notify warehouse lead (Lane 1 + 3) and accounting lead (Lane 2 + 4) so they can defer dependent work.
What's not done · what's uncertain
Open punch list captured so it survives context switches.
-
TBD
WO customer PO# field
WO has no canonical place for the customer PO# yet. Expected pattern is
bodyFields.memo. Pending Mike's review — once decided, the universal thread becomes complete across all 3 lifecycles. -
STUB
Finance alert NS automation
SO/PO/WO finance alerts fire as platform events today. The NS workflow build that delivers them into Finance's review queue is planned for R594. Until then Finance gets the events ledger view, not a queued task.
-
STUB
Auto-classification of which path/combination applies
Today operators classify each line by item type mentally. A classifier on intake would surface mixed-path orders earlier and auto-stage WO + PO from the SO via a single fan-out endpoint.
-
DECISION
Monthly close reconciliation as a 27th v2 workflow contract
The Finance step that ties SO + PO + WO together at month-end is Finance-manual today. Promoting it to a workflow contract would automate the variance roll-up + cross-lane reconcile. Pending Mike's call.
-
OPEN
De-duplicate Finance alerts in the review UI
Mixed orders fire 2-3 alerts (SO + PO + WO). Each is meaningful and shouldn't be suppressed at source, but Finance UI should group them visually by customer/SO id.
-
DEFER
Real-time cross-lane visibility dashboard
One screen that shows, for a given customer order, the live state of all 3 lifecycles + Finance posture. Defer until the NS workflow alerts land (R594) so we have something real to surface.