SOP: Backorder Resolution
Document ID: WMS-INV-006 Version: 1.0 Effective date: 04/30/2026 Owner: Warehouse Operations Manager Next review: [six months from effective date] Applies to: Managers and staff resolving backordered orders — the pull side of the inventory inflow that WMS-REC-003 and WMS-INV-002 push into
1. Purpose
This procedure governs what happens when an order can't be fully allocated to inventory at order-creation time. The order's status flips to BACKORDERED (or PARTIALLY_ALLOCATED if some lines could be allocated and partial fulfillment is allowed), and the system queues an automatic retry whenever new stock arrives for any of the affected SKUs. Three SOPs upstream cross-reference this one as the destination for their post-approval enqueue:
- WMS-REC-003 §4.2 — receiving session approval enqueues
enqueueCheckBackordersper received variant - WMS-INV-002 §4.8 — cycle-count session approval enqueues the same per variant where variance went up
- WMS-INV-001 §4.2 — lightweight inventory adjustments don't enqueue today (gap noted there)
This SOP is also the manager-facing dashboard documentation — the /backorders page that surfaces the "what's waiting on what" view across the whole open order book, with manual retry and batch-fulfill actions.
2. Scope
In scope:
- The
BACKORDEREDandPARTIALLY_ALLOCATEDorder statuses and the transitions between them - The automatic retry pipeline:
enqueueCheckBackorders→ workerprocessCheckBackorders→checkBackorderedOrders→ batchenqueueAllocateOrder - Stale-backorder escalation to
ON_HOLDafter the 30-day cutoff - Manual retry from the
/backordersUI: per-order Fulfill and Fulfill All - Splitting a backordered order into a shippable child + a backordered child via
splitBackorder - Reading backorder events in
fulfillment_events(order:backordered,order:backorder_resolved)
Out of scope:
- Order creation and the initial allocation pass that decides whether an order goes to
ALLOCATEDvs.BACKORDEREDvs.PARTIALLY_ALLOCATED— out of scope here; the allocation logic is upstream - Picking, packing, shipping the resolved (now
ALLOCATED) order — see WMS-PICK-001, WMS-PACK-001, WMS-SHIP-001 - Order on-hold management beyond the auto-escalation from this SOP —
ON_HOLDorders need manual review per WMS-AUD-002 (when written) - Customer notifications about backorder status — outside the WMS
3. Roles & permissions
API enforcement: The endpoints in this SOP (
GET /orders/backorders,POST /orders/:id/allocate,POST /orders/allocate-batch,POST /orders/:id/split-backorder) are auth-only; no inline role gate.
| Role | View backorders dashboard | Manual retry single order | Batch retry all | Split a backorder |
|---|---|---|---|---|
| READONLY | ✓ | — | — | — |
| STAFF | ✓ | ✓ | ✓ | ✓ |
| MANAGER | ✓ | ✓ | ✓ | ✓ |
| ADMIN | ✓ | ✓ | ✓ | ✓ |
| SUPER_ADMIN | ✓ | ✓ | ✓ | ✓ |
Operational expectations:
- Manual retry is generally not necessary — the automatic retry triggered by inventory inflow handles 90%+ of cases. Reach for the dashboard's Fulfill / Fulfill All when (a) the worker queue is unhealthy, (b) you've moved stock manually via WMS-INV-001 §4.2 (which doesn't enqueue today), or (c) you've just imported stock via a path that bypasses the standard receiving flow.
- Splitting a backordered order is a commercial decision, not a routine operation. Splitting commits the warehouse to shipping the partial today and the rest later — it's only correct if the customer has agreed to the partial shipment. Manager-level discretion.
- An order escalating to
ON_HOLDafter 30 days needs human review, not another retry. Either source the missing stock (special order, vendor PO), substitute a SKU, or cancel and refund. The auto-retry pipeline stops touchingON_HOLDorders by design.
4. Procedures
4.1 How an order ends up backordered
The first allocation pass runs when an order is created (or when it's manually retried per §4.4). Per OrderAllocationService.allocateOrder(), the resulting status is determined by:
| Conditions | New status |
|---|---|
One or more order items have no matching variant in the system (unmatchedItems > 0) and totalAllocated == 0 | ON_HOLD (with holdReason recorded) |
unmatchedItems > 0 but partial allocation succeeded | PARTIALLY_ALLOCATED (with holdReason recorded) |
| All items matched but zero could be allocated (no available inventory at any pickable location) | BACKORDERED |
Some items allocated but not all required quantities, and allowPartial: false | BACKORDERED |
Some items allocated but not all required quantities, and allowPartial: true | PARTIALLY_ALLOCATED |
| All required quantities allocated | ALLOCATED |
What gets written:
- The
Order.statusfield updates to one of the above. - If the transition crosses into
BACKORDEREDfrom any non-backordered status, afulfillment_eventsrow of typeorder:backorderedis created with the order, item counts, and the timestamp. - A real-time
ORDER_BACKORDEREDevent is published outside the transaction (best-effort SSE). - If the transition is out of
BACKORDEREDintoALLOCATED, afulfillment_eventsrow of typeorder:backorder_resolvedis created.
What an order item looks like during backorder:
quantityAllocatedreflects what was successfully allocated (could be zero or partial).- The item's matched flag (set during initial item-to-variant matching) determines whether retry will ever consider it.
- The shortfall is
quantity - quantityAllocatedper item; the order's overall shortfall is the sum across items.
4.2 Automatic retry on inventory inflow
Use when: This is the system's primary backorder-resolution path. You don't initiate it manually — it runs whenever inventory increases for a SKU.
Triggers (callers of enqueueCheckBackorders):
- Receiving approval — per WMS-REC-003 §4.2, after the approve transaction commits, the route enqueues one
checkBackordersjob per unique received variant withtriggerSource: "receiving:{sessionId}". - Cycle-count approval — per WMS-INV-002 §4.8, after the approve transaction commits, the route enqueues one
checkBackordersjob per variant wherevariance > 0(more found than expected) withtriggerSource: "cycle-count:{sessionId}".
Triggers that exist but aren't wired (gaps):
- Customer return restock (per WMS-RET-002) — restocks credit inventory but do not enqueue a backorder check today. This is a real gap: a return adds sellable stock that won't trigger waiting orders to retry. Workaround: manually run §4.4 after a high-volume return restock.
- Lightweight
/inventory/:id/adjust(per WMS-INV-001 §4.2) — adjustments that increase quantity don't enqueue. Same workaround.
What the worker does (processCheckBackorders in apps/worker/src/processors/order.processor.ts:127):
- Receives
{ productVariantId, triggerSource? }job data. - Calls
OrderAllocationService.checkBackorderedOrders(productVariantId)which runs two stages atomically:- Stage 1 — Stale escalation: all orders with
status: BACKORDERED,updatedAt < (now - 30 days), and at least onematched: trueitem for this variant are bulk-updated tostatus: ON_HOLDwithholdReason: "Backordered for more than 30 days — manual review required". They will not be considered for retry. - Stage 2 — Eligible-orders fetch: finds up to 20 orders with
status IN ('BACKORDERED', 'PARTIALLY_ALLOCATED')that have amatched: trueitem for this variant, ordered bypriority ASC, createdAt ASC(EXPRESS before RUSH before STANDARD; FIFO within priority).
- Stage 1 — Stale escalation: all orders with
- For each eligible order, enqueues an
allocateOrderjob withallowPartial: trueand an idempotency key ofbackorder-retry-{orderId}-{timestamp}. - Logs the final count and returns.
What this means in practice:
- A receiving session that brings in 100 of variant X triggers a single
checkBackordersjob. That job kicks off up to 20 follow-onallocateOrderjobs. - Each
allocateOrderjob runs the standard allocation logic. Some will fully allocate (transition toALLOCATED); others will partially allocate (stillPARTIALLY_ALLOCATED); the remainder stayBACKORDERED. - Orders with the same priority are retried in receipt order — earliest-created first.
⚠ The
MAX_RETRY_BATCH = 20cap is per check. If 50 orders are waiting on the same SKU and approval brings in stock for that SKU, only the top 20 (by priority + age) get retried in this pass. The next 30 wait for the next inflow event for that variant. If no further inflow comes, those 30 stay backordered until either a manual retry (§4.4) or the 30-day stale escalation fires.
⚠ Stale escalation runs every time the worker checks, not just on a schedule. The
Stage 1bulk update runs on everycheckBackordersjob for the variant. If the worker queue is healthy, stale orders escalate within minutes of the 30-day threshold. If the queue is backed up or the variant hasn't seen inventory inflow in a long time, escalation is delayed.
4.3 Reading the backorders dashboard
Use when: Manager review of all current backorders — what's stuck, what's been waiting longest, what shortfall by SKU.
Steps:
- Open
/backorders. The page callsGET /orders/backorders. - The page shows three things:
- Stats at the top:
totalBackorderedOrders,totalSkus,oldestDaysPending,totalShortfall - By SKU tab: each SKU with
ordersWaiting,quantityNeeded,quantityAvailable,shortfall, fill-percent bar - Orders tab: each order with
orderNumber, customer, status badge,daysPending, item-level shortfall
- Stats at the top:
What the dashboard counts:
- Orders included:
status IN ('BACKORDERED', 'PARTIALLY_ALLOCATED')— both states are surfaced together. - Available inventory per variant: aggregate across all locations with
status: AVAILABLE. The dashboard does not filter byisPickable: true— so available stock at a non-pickable location (receiving zone, packing station) shows up as "available" here even though allocation won't actually pull it. See callout below. - Days pending:
now - order.createdAt, integer days. Doesn't reset on retries.
⚠ The "Available" column overstates what allocation can use. The dashboard sums
InventoryUnitrows by variant wherestatus: AVAILABLE, but allocation requires bothstatus: AVAILABLEandlocation.isPickable: true. So a SKU showing 50 available with 30 ordered may actually have only 10 in pickable locations and 40 in receiving — the dashboard says "fillable" but a manual retry will still backorder. Either rely on the actual retry result (run Fulfill and watch what happens) or move the receiving-zone stock via WMS-REC-004 (whose UI is unbuilt — see callout) or WMS-INV-001 §4.1 first.
4.4 Manually retrying a single order
Use when: The auto-retry pipeline didn't fire (e.g., inflow was via a path that doesn't enqueue), or you want to confirm an order can now be filled.
Steps:
- From
/backorders→ Orders tab, find the order. - Tap Fulfill next to it.
- The page calls
POST /orders/:id/allocatewith{ allowPartial: true }. - The route runs
OrderAllocationService.allocateOrder(id, true)— the same allocation logic as the auto-retry, but synchronously in-process. - On success, if the order transitions to
ALLOCATED, the route also runsOrderPackageService.recommendAndSave(id)to generate box recommendations. Failures here are logged but don't fail the allocation. - The toast shows
"Allocation attempted — refreshing..."and the dashboard refetches.
Outcomes:
- Order goes to
ALLOCATED→ it will now flow into picking (WMS-PICK-001) per the standard outbound path. - Order goes to
PARTIALLY_ALLOCATED→ some items got stock, others still don't. The order remains on the dashboard. - Order stays
BACKORDERED→ no available pickable stock for any item. The order stays on the dashboard. - Order goes to
ON_HOLD→ there's an unmatched item (no variant match in the system, not just a stock issue). Manual review per §8.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 400 | <varies> from allocateOrder | Could be a state-machine error (e.g., trying to allocate a CANCELLED order). Read the response body. |
| 404 | (depending on order load) | Order ID is wrong. Refresh. |
4.5 Batch-retrying all visible backorders
Use when: You've just brought in a major inventory wave and want to refresh the entire backorder dashboard at once, rather than relying on the per-variant auto-retry to find each order.
Steps:
- From
/backorders→ Orders tab, with the list loaded. - Tap Fulfill All.
- The page collects every order ID currently visible (the current
data.ordersstate) and callsPOST /orders/allocate-batchwith{ orderIds, allowPartial: true }. - The route iterates each order and runs allocation. The toast shows
"Batch complete: {n} fulfilled, {m} still backordered"once done.
Important behavior:
- The batch endpoint runs allocations sequentially in the API process — there's no queue dispatch, no parallelism. Large batches can take noticeable time (seconds-to-minutes for 100+ orders).
- The batch shares the same
MAX_RETRY_BATCHcap of 20 on the dashboard view itself: theGET /orders/backordersendpoint returns up to all current backorders (no take cap), but thetake: MAX_RETRY_BATCHconstraint insidecheckBackorderedOrdersonly applies to the auto-retry path. Manual batch fulfill processes everything you see. If the dashboard shows 200 orders, all 200 are attempted.
⚠ Batch fulfill is non-atomic per order. If allocation for order #87 throws, orders #1–#86 keep their results, order #87 fails for the user, and orders #88+ may or may not be processed depending on whether the route swallows or re-throws. Read the toast message — the count tells you the truth, but if errors bubble, they show up in
app.log.errorrather than the UI.
4.6 Splitting a backordered order
Use when: A PARTIALLY_ALLOCATED or BACKORDERED order has some items ready to ship, the customer has agreed to a split shipment, and you want to release the shippable portion now while keeping the unfilled items waiting on a child order.
Prerequisites:
- Order status is
PARTIALLY_ALLOCATEDorBACKORDERED. - At least one item is fully allocated (otherwise there's nothing shippable).
- Customer has agreed to a partial shipment. This is a commercial decision that the system doesn't validate.
Steps:
- (No UI button mounted on the backorders page today — see §8.) Call
POST /orders/:id/split-backorderdirectly via API. - The route runs
OrderAllocationService.splitBackorder(orderId)in a transaction:- Finds items where
quantityAllocated >= quantity(fully shippable) — these stay on the original order. - Finds items where
quantityAllocated < quantity(partially or unallocated) — these move to a new child order. - Creates the new
Orderrow withstatus: BACKORDERED, the original customer info, and the unfilled items. - Updates the original order's status based on what's left (
ALLOCATEDif everything remaining is fully allocated,READY_TO_PICK, etc.).
- Finds items where
- Returns:
{ originalOrderId, originalOrderNumber, originalStatus, backorderOrderId, backorderOrderNumber, shippableItems, backorderedItems }.
Result:
- The original order is now ready to flow into picking (WMS-PICK-001) for the shippable items.
- The new backorder child order stays on the dashboard with the unfilled items. Its
createdAtis the split time, not the original creation date — which meansdaysPendingon the child resets. Be aware: a customer who waited 14 days, then got a split, then waited another 14 on the child shows up as a 14-day order on the dashboard, not a 28-day one.
Common errors:
| HTTP | API message | What it means |
|---|---|---|
| 400 | Order ... cannot be split (status: ...). Only PARTIALLY_ALLOCATED or BACKORDERED orders can be split. | The order is in a different state. If it's already ALLOCATED, no split is needed; if it's ON_HOLD, resolve the hold first. |
| 404 | Order not found: ... | Order ID is wrong. |
⚠ Split is irreversible. Once split, you cannot merge the child back into the parent through the UI or API. If you split prematurely, the only recovery is to ship the parent normally and let the child sit on the dashboard until its items can be sourced — same as if the customer had placed two separate orders. Confirm the customer agreement before splitting.
5. Reference
5.1 Relevant order statuses
Out of the 13-value OrderStatus enum, this SOP touches:
| Status | Means | This SOP relates to it via |
|---|---|---|
ALLOCATED | Inventory reserved for the order; ready for picking | Successful retry destination |
PARTIALLY_ALLOCATED | Some items reserved, some still need stock | Dashboard included this; eligible for auto-retry |
BACKORDERED | No items have any allocation yet, OR partial isn't allowed | Dashboard included this; eligible for auto-retry |
ON_HOLD | Either unmatched item or stale-backorder escalation | Auto-retry skips these — manual review per §8 |
Everything past ALLOCATED (PICKING, PICKED, PACKING, etc.) is downstream — see WMS-PICK-001 onwards.
5.2 Backorder events
The fulfillment_events table records two event types for backorder transitions:
| Event type | When written |
|---|---|
order:backordered | When allocateOrder transitions from any non-backordered status into BACKORDERED |
order:backorder_resolved | When allocateOrder transitions from BACKORDERED into ALLOCATED |
Each row's payload contains orderId, orderNumber, previousStatus, newStatus, totalItems, allocatedItems, backorderedItems, and timestamp.
The events are insufficient for full audit reconstruction. They don't fire on every retry — only on status transitions. A backorder retried 5 times that stays BACKORDERED produces zero events from the second retry on. To trace retry attempts, look at the worker logs for [Orders] Checking backorders for product variant: ... and [Orders] Found N backordered orders to retry, plus the bullmq job-history table if your queue is configured for retention.
5.3 The 30-day stale-escalation cutoff
The cutoff is a hardcoded MAX_BACKORDER_DAYS = 30 in OrderAllocationService.checkBackorderedOrders() (line 388). Orders that have been BACKORDERED (not just any status) for more than 30 days at the time a retry is attempted are bulk-updated to ON_HOLD with holdReason: "Backordered for more than 30 days — manual review required" and excluded from that retry.
This is a soft escalation:
- It only runs when something triggers
checkBackorderedOrders. If no one is enqueuing checks, the escalation doesn't run. - The 30-day window starts from
Order.updatedAt, notcreatedAt— meaning any update to the order (even a status flip due to an earlier retry) resets the clock. An order that's been backordered for 90 days but had a retry that touched it within the last 30 days will not yet escalate. - An
ON_HOLDorder can be returned toBACKORDEREDvia direct DB update, but no application code does this.
5.4 Related SOPs
- WMS-REC-003 §4.2 — Receiving approval (the primary upstream trigger for auto-retry)
- WMS-INV-002 §4.8 — Cycle-count approval (the secondary upstream trigger)
- WMS-INV-001 §4.2 — Lightweight inventory adjustments (gap: no enqueue today)
- WMS-RET-002 — Customer return restock (gap: no enqueue today)
- WMS-PICK-001 — Picking (the destination for resolved orders)
- WMS-AUD-002 — Shrinkage and on-hold investigation (where ON_HOLD orders go for manual review)
6. Audit & compliance
The backorder lifecycle has partial audit data:
- Backorder transitions —
fulfillment_eventsof typeorder:backorderedandorder:backorder_resolvedcapture entry/exit, but not the in-between retries. - Allocation events — every successful allocation writes its own
fulfillment_eventsrow (per the upstream allocation logic). Joining backorder events to allocation events reconstructs the success path. - Worker job history — if BullMQ is configured for retention, the job history shows every
checkBackordersandallocateOrderinvocation, payload, and outcome. Default retention is short (jobs are usually pruned after completion); extend it for forensics if needed. Order.updatedAtadvances on every status change; combined with the events, this gives a rough timeline.
What's missing:
- No retry-attempt log per order. A backorder retried 50 times before it resolves shows only the entry and exit events.
- No record of which inflow triggered which retry. The
triggerSourcefield inCheckBackordersJobData(e.g.,receiving:{sessionId}) reaches the worker logs but is not persisted to the database. To trace "which receiving session unblocked which order," you need worker logs + timestamp correlation. - No notification log. Customers waiting on backorders get no automated communication from the WMS today.
Manager monthly review:
- Pull
fulfillment_events WHERE type = 'order:backorder_resolved' AND createdAt > (now - 30 days)— what got resolved this month. - Pull
Order WHERE status = 'ON_HOLD' AND holdAt > (now - 30 days)— newly escalated orders. Each one is a manual-review queue item. - Diff
count(BACKORDERED)against last month. Trending up means inflow isn't keeping pace with order volume; trending down is healthy.
Quarterly governance:
- Median time
BACKORDERED → ALLOCATEDfor orders that resolved this quarter. Tightening means upstream improvements are working. - Count of orders that hit
ON_HOLDvia the 30-day rule. High count means either (a) stocking is genuinely chronic for some SKUs, or (b) the retry pipeline is unhealthy. - Top 10 SKUs by total
ordersWaiting * shortfall— the buyer's reorder priority list.
7. Troubleshooting
| Symptom | Cause | Resolution |
|---|---|---|
| Receiving session was approved an hour ago, but my dashboard still shows the same backordered orders waiting on that SKU | Either the worker queue is backed up, or the MAX_RETRY_BATCH = 20 cap was hit and your orders are in the next batch | Check #wms-support for queue health. If healthy, wait — the next inflow for that variant will pick up the rest. If urgent, run Fulfill All (§4.5) manually. |
Fulfill All reports 0 fulfilled, 50 still backordered even though the dashboard shows available stock | Per §4.3 callout, the dashboard "Available" includes non-pickable locations. The available stock is in receiving/staging, not in pickable bins. | Either move the stock to a pickable location (WMS-INV-001 §4.1) or wait for putaway (WMS-REC-004). Then retry. |
An order has been BACKORDERED for 45 days but isn't ON_HOLD | The 30-day cutoff is on updatedAt, not createdAt. Some retry within the last 30 days touched the order. | Check Order.updatedAt. If it's recent, the order is still considered fresh by the retry pipeline. To force escalation, manually update status: ON_HOLD via DB. To prevent the next retry from un-escalating, also set holdAt and holdReason. |
Fulfill returns 200 but the order didn't move from BACKORDERED | Allocation tried but found no pickable inventory for any item | Expected. The order stays on the dashboard. Either source the stock or accept the backorder. |
| Customer-return restock added 50 of a SKU but the backorder for that SKU didn't auto-retry | Per §4.2 gap, return restocks don't enqueue checkBackorders today | Manual Fulfill All for that order, or run a one-off enqueue script per SKU after a return wave. File the return-restock-enqueue gap as engineering work — it's a 3-line addition to the return restock service. |
Lightweight /inventory/:id/adjust increased available stock but waiting backorders didn't trigger | Same as above for the adjust path | Same workaround. File the adjust-enqueue gap. |
| Split was performed accidentally | Split is irreversible per §4.6 | Ship the parent normally; the child stays on the dashboard until its items can be filled. There's no merge. If this happens repeatedly, lock the split CTA behind a confirm modal as engineering work. |
Auto-retry escalated an order to ON_HOLD but the customer just got patient with us | The 30-day rule fired | Manually re-set status: BACKORDERED and holdAt: null via DB. The next retry will treat it normally. Document the override. |
| Orders waiting on an SKU but the SKU isn't in the By SKU tab | The order's items may have matched: false (no variant match in the system at all) | These orders end up ON_HOLD rather than BACKORDERED. They don't show on the SKU tab because there's no SKU to match. Look in the Orders tab for ON_HOLD filtered. |
Worker keeps logging [Orders] Found 0 backordered orders to retry for the same variant | Either no waiting orders for that variant, or the orders' items aren't matched: true | Confirm with Order WHERE status IN ('BACKORDERED', 'PARTIALLY_ALLOCATED') AND items.some({ productVariantId: X, matched: true }). If zero, the worker is correct — there's nothing to retry. |
8. Escalation
- Wire
enqueueCheckBackordersinto the return restock and/inventory/:id/adjustpaths. Both are inflow events that should trigger backorder retry. Returns produce real, sellable stock that today's pipeline misses entirely. The integration is roughly 5 lines per call site (per-unique-variant enqueue withtriggerSource). High operational value, small effort. - Build the split-backorder UI button. The
splitBackorderAPI works; no UI surfaces it. A button on the dashboard's Orders tab that opens a confirm modal explaining the split, then calls the endpoint. Maybe an hour. Without this, splits today require either Postman/Bruno or a developer. - Persist
triggerSourceto the events table. CurrentlytriggerSource: "receiving:{sessionId}"reaches the worker logs but not the DB. Adding atriggerSourcecolumn onfulfillment_eventswould give "which receiving session unblocked which order" as a clean SQL query, instead of timestamp-correlation guesswork. - Customer notification on
order:backorder_resolved. Today the WMS doesn't notify the customer when their long-backordered item finally allocates. A simple email trigger on theorder:backorder_resolvedevent would close the loop. - Sustained ON_HOLD escalation count >5/week. Cross-functional: warehouse manager + buyer review the hold list. Either source missing stock, substitute SKUs (with customer agreement), or cancel and refund.
- Worker queue persistently backed up affecting auto-retry. IT — BullMQ depth and concurrency. Queue health affects every async path, not just backorders.
9. Revision history
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | [DATE] | [NAME] | Initial release. Documents the full backorder resolution lifecycle: status decisions in OrderAllocationService.allocateOrder(), the auto-retry pipeline (enqueueCheckBackorders → processCheckBackorders → checkBackorderedOrders → batched enqueueAllocateOrder), the 30-day stale-escalation rule (MAX_BACKORDER_DAYS = 30, hardcoded), the 20-order cap per check (MAX_RETRY_BATCH = 20), and the priority+FIFO order of retry. Documents the /backorders UI dashboard, including the available-stock overcount callout (the dashboard sums all AVAILABLE units regardless of pickable, so receiving-zone stock inflates the "fillable" view). Documents the manual retry, batch fulfill, and split-backorder operations. Documents two real upstream gaps: customer-return restock (per WMS-RET-002) and lightweight /inventory/:id/adjust (per WMS-INV-001 §4.2) both add inventory but do not enqueue checkBackorders — backorders waiting on those inflows do not auto-retry. Documents one downstream gap: no UI button for splitBackorder. Documents the partial audit story — events on entry/exit only, no retry-attempt log, no triggerSource persistence. Cross-references WMS-REC-003 (primary trigger), WMS-INV-002 (secondary trigger), WMS-INV-001, WMS-RET-002, WMS-PICK-001, WMS-AUD-002. Code references: packages/queue/src/types.ts:541-544, apps/worker/src/processors/order.processor.ts:127-158, packages/domain/src/services/order-allocation.service.ts:270-360, 387-433, 439+, apps/api/src/routes/order.route.ts:283-380, 653-692, 939+, 1017+, apps/web/src/pages/backorders/index.tsx:255-310. |