Backorder → Bucket flow
The old shop workflow: cashier rings a bike, product's out of stock, cart won't let them add the line, cashier walks the customer to Purchasing, someone opens a special-order form, the customer waits. The v0.7.190 flow: cashier rings, line lands in the cart red, Charge auto-orders the shortfall. Zero-stock lines are no longer a blocking popup — they're just a line the register handles.
One flow, three modules touching one substrate.
| Shipped | v0.7.190 (ring-through) → v0.7.204 (final polish) |
| Substrate | inventory_order_queue (the Bucket) + core/movements.ts allowNegative flag |
| Retired | /reorder endpoint (v0.7.190) — the manual reorder form is unnecessary now that Charge auto-buckets |
The Sales side
Zero-stock line rings through. The old "can't add — out of stock" popup is gone. The line adds with a red backorder N annotation on the line; the cart totals include it; the tax math includes it. The operator has the customer's attention, not the "out of stock" popup's.
Charge auto-buckets the shortfall. On Charge, for every cart line with qty > on_hand, the shortfall (qty − on_hand) auto-inserts into inventory_order_queue — earmarked to the customer if one's attached, or for Stock if not. The Sales transaction is posted as a Special Order (invoice status Awaiting order); the Sales ledger row will track the Bucket line → PO line → Ordered → Partially received → Ready for pickup lifecycle downstream.
Out-of-stock pill = Adjust actual stock. If the cashier believes there IS stock — miscount, misshelved, moved to the back room — the OOS pill on the cart line is now an inline Adjust actual stock stepper (v0.7.196/.201). Incrementing commits live to inventory.quantity_on_hand via /count (debounced), an "Inventory Adjusted" popup confirms, the cart line's red goes away when on-hand rises above what's needed, and no line ever hits the Bucket. This is the corrective path — the wrong-count case — routed through the cart instead of the operator swapping to Inventory to fix the number.
The Inventory side
The /count endpoint (v0.7.196) writes one immutable inventory_movements row through core/movements.ts — same code path a manual adjust from the Inventory tab uses. Actor / reason are captured; the audit chain narrates it as "Marcus adjusted Trek Domane 4 SL (56) on-hand: 0 → 1."
The allowNegative flag on the movement engine is what makes zero-stock ring-through legal — Sales posts with allowNegative: true on backorder lines so the movement engine doesn't reject the debit. On-hand can go temporarily negative when a sale posts before the reorder receipt (rare — usually the Bucket → PO flow beats the sale, but the invariant holds either way).
Bulk items are real SKUs (v0.7.136, migration 039). The product_variants.is_bulk flag distinguishes bulk items (grease, chain lube, small parts) from bike-serial variants; both live in the same catalog and both route through the same movement engine. The Sales bulk-item palette reads is_bulk = 1.
The Purchases side
"Awaiting order · for <Customer>". The Bucket row (v0.7.191) — the pre-PO staging where reorder-worthy lines accumulate — now shows who each line is for:
- "for Stock" — neutral chip, means the ring didn't have a customer attached
- "for <Customer>" — blue chip, means a customer's earmarked; when the PO's cut and the goods arrive, that customer's the pickup
The Bucket /bucket GET returns customer_id, special_order_transaction_id, and customers.display_name so the chip renders without a second round-trip. Migration 033 (v0.7.86) already carried the special_order transaction → Bucket line → PO line linkage; v0.7.191 surfaces it in the operator UI.
Vocabulary — Bucket = "Awaiting order" (the reorder hasn't been ordered from a vendor yet); PO cut = "Ordered" (vendor has it). The Sales ledger's "Awaiting order" status is the mirror of the Bucket row upstream (v0.7.132).
What the operator sees
A cashier ringing a $2,411 Trek Domane on which the shop has 0 on hand:
- Line adds red with backorder 1
- Cart totals reflect it; no popup
- Cashier tries Adjust actual stock — no, definitely 0
- Attach customer — Joe Customer
- Charge — customer pays $500 layaway; invoice status Awaiting order
- On the Purchases tab (any station, next refresh), the Bucket has a new "Trek Domane 4 SL (56) · Awaiting order · for Joe Customer" row
- The buyer batches it into a Trek vendor PO, cuts the PO; Sales ledger row flips to Ordered
- Trek ships; receiving marks the Bucket line received; Sales ledger flips to Ready for pickup
- Joe comes in, cashier resumes the special order into the Till, takes the balance, invoice closes as Sold
Every state is one row on one substrate — no email chain, no whiteboard, no "did we order that yet?"
Retired: /reorder
Pre-v0.7.190 there was a manual /reorder endpoint — a form where the operator picked SKU + qty to add to the Bucket. It's gone. Every Bucket entry now originates from either a Sales ring-through (auto-bucket at Charge) or the Purchases-tab Reorder dashboard's explicit add (BIKE.L2-0221 was the manual form's designation, retired v0.7.190).
See also
- Sales module — the Charge popup that decides where the line goes
- Purchases module — the Bucket + PO lifecycle downstream
- Inventory module — the
/countlive-commit endpoint +is_bulkflag - Three strata — Kernel · Meta · Modules — the shared
core/movements.tsengine