SKU Cancellation (WorkOrder.canceledSKU)
Covers what sets WorkOrder.canceledSKU, who reads it, and why a Fulfil-side shipment
split leaves MES WorkOrders wrongly flagged as canceled.
Schema
prisma/schema.prisma:153 — three related columns on WorkOrder:
| Column | Type | Notes |
|---|---|---|
canceledSKU | Boolean @default(false) | Indexed on its own (@@index([canceledSKU]), schema.prisma:178) |
cancellationSource | String? @db.VarChar(25) | No live writer in this codebase — read by analytics only |
cancellationDateTime | DateTime? | Set alongside canceledSKU = true by both writers below |
Writers — both live in the print-label flow
canceledSKU is not a user-editable field and is never set by an inbound Fulfil
webhook. It is written only as a side effect of retrieving a shipping label, in
src/services/shippinglabel/shippinglabel.service.ts:
- Per-SKU cancel —
_checkFulfilSkuExist(shippinglabel.service.ts:141-156). For a shipment with ≥2 non-suspended MES WorkOrders, it fetches Fulfil'sinventory_movesfor that shipment, tallies quantity per SKU (skipping moves in statecancel, INFRA-464), then flags every MES WorkOrder whose SKU has no remaining tally:where: { workOrderId: { in: mesWorkOrderIdsShouldCancel }, canceledSKU: { not: true } },
data: { canceledSKU: true, cancellationDateTime: new Date() }, - Whole-shipment cancel —
_changeShipmentWorkOrdersCancel(shippinglabel.service.ts:221-226) flags every non-canceled WorkOrder on the shipment (where: { csShipmentId, canceledSKU: { not: true } }). Reached from three places:_checkFulfilSkuExistwhen Fulfil reports zeroinventory_movesat all (:109-114, which also throws a 400), and_handleEmptyShipping(:202-219) when the Fulfil shipment state iscancel, or isdonewith no tracking number.
Both writers are best-effort: _checkFulfilSkuExist swallows its own errors unless
shouldThrowError is set, so a cancel sweep can happen without surfacing anything to the
caller. Neither writer touches cancellationSource.
Readers — what a stale true breaks
A WorkOrder left at canceledSKU = true is effectively invisible-and-unprintable:
| Reader | Effect |
|---|---|
src/components/admin/ShipStation/ShipStation.tsx:51 + ShipStationCanceledSKUModal.tsx | ShipStation surfaces a blocking/warning modal at print time; hasNonCanceledSKU gates the normal path |
src/components/admin/ShipStation/utils.ts:237-248 | Splits fetched records into canceledSKURecords for that modal |
src/components/common/RecordInList/RecordInList.tsx:151 | Renders the row struck-through/highlighted in the ShipStation list only |
src/models/purchaseorder/purchaseorder.model.ts:94-112 | PO progress/count rollups filter wo."canceledSKU" = false; a separate branch counts = true |
src/models/workOrderShipmentLatency/workOrderShipmentLatency.model.ts:155 | Latency report hard-excludes canceled SKUs |
src/routers/admin/resources/canceledShipment/listActionHandler.ts:111 | The Canceled Shipment resource list is defined as canceledSKU: true |
src/routers/admin/resources/workorder/listActionHandler.ts:133-135 | WorkOrder list filter (item.value === 'true') |
Gotcha — Fulfil shipment splits produce false cancellations
When Fulfil splits an existing two-line shipment after the fact (the recurring data issue
behind INFRA-603 / INFRA-612 / INFRA-624), the moved SKU disappears from the original
shipment's inventory_moves while MES still has both WorkOrders attached to the original
CustomerShipment. The next print-label call on that shipment therefore reads the
moved-away SKU as "not in Fulfil" and flags it via writer #1 above — or, if Fulfil returns
no moves at all for the original shipment, writer #2 flags both WorkOrders.
Consequence for any repair script that re-homes a WorkOrder to the new
CustomerShipment: moving csShipmentId alone is not sufficient. canceledSKU must also
be reset to false with cancellationDateTime cleared (and cancellationSource,
defensively — it should already be NULL given it has no writer), otherwise the correctly
re-homed WorkOrder still won't print and still won't count in PO rollups or the latency
report. Check the sibling WorkOrder left on the original shipment too — writer #2 cancels
the whole shipment, not just the moved line.
INFRA-624 does the reset as a separate step after the move rather than inside the same
transaction: move all rows (unchanged from the INFRA-612 script shape), then a read-only
SELECT over both shipments per row to find which WorkOrders actually carry the flag, then
one scoped UPDATE over that reviewed list. This keeps the move script identical to the
shape already proven in production twice and makes the reset auditable against a known row
count. The trade-off is a window between the two writes during which a correctly re-homed
WorkOrder is still unprintable — run the steps back-to-back.
See also: the Shipping Label / Tracking Capture (print-label flow) section of
.claude/rules/architecture.md for the tracking-number capture side of the same service,
and shipment-create.md for the acknowledge flow that sets
labelStatus/acknowledgedAt (which a manual CustomerShipment INSERT bypasses).