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) | Set to warehouseReassign by the canceled-warehouse writer only (INFRA-671); the two legacy writers leave it NULL |
cancellationDateTime | DateTime? | Set alongside canceledSKU = true by all three writers below |
Writers — all 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:151-219). 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 (updateManyat:210-215):where: { workOrderId: { in: mesWorkOrderIdsShouldCancel }, canceledSKU: { not: true } },
data: { canceledSKU: true, cancellationDateTime: new Date() }, - Whole-shipment cancel —
_changeShipmentWorkOrdersCancel(shippinglabel.service.ts:280-285) flags every non-canceled WorkOrder on the shipment (where: { csShipmentId, canceledSKU: { not: true } }). Reached from three places:_checkFulfilSkuExistwhen Fulfil reports zeroinventory_movesat all (:165-169, which also throws a 400), and_handleEmptyShipping(:261-278) when the Fulfil shipment state iscancel, or isdonewith no tracking number. - Canceled-warehouse (KY2) cancel —
_cancelForCanceledWarehouse(shippinglabel.service.ts:116-144) flags every non-canceled WorkOrder on the shipment (where: { csShipmentId, canceledSKU: { not: true } }) and — unlike the two legacy writers — setscancellationSource: CANCELLATION_SOURCE.canceledWarehouse(:135-142). See Canceled-warehouse (KY2) flow below.
Writers #1 and #2 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 of them touches cancellationSource; only writer #3 does.
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') |
Canceled-warehouse (KY2) flow
The Ship Station "Download shipping label" click can, as a side effect, cancel the whole
shipment's WorkOrders — but only for one specific Fulfil warehouse (KY2), and only when the
shipment is already done there. This is writer #3 above, added by INFRA-671.
- Trigger: when
getShippingLabelFromFulfilends up with no label (typeof labelRes.data?.[1] !== 'string',shippinglabel.service.ts:80-83), it calls_cancelForCanceledWarehousebefore the legacy_handleEmptyShipping/_checkFulfilSkuExistpath. Other warehouses and states are unaffected and fall through unchanged. - Config: the warehouse to cancel for is
FULFIL_CANCELED_WAREHOUSE_ID(src/utils/envConfig.ts; KY2 =284). An empty value disables the whole check. - Condition:
_cancelForCanceledWarehousereads the shipment's Fulfilstate+warehouseviafulfilGetShipmentsWarehouseState(src/clients/fulfilClient.ts:391-408) and cancels only whenString(warehouse) === FULFIL_CANCELED_WAREHOUSE_IDandstate === 'done'(shippinglabel.service.ts:131-133). - Fail-safe direction: empty env, missing row, non-matching warehouse, non-
donestate, or a warehouse-lookup error all returnfalse(cancel nothing); the lookup error is logged, not thrown (:124-127). The cancellation DB write is deliberately outside that catch — a DB failure during the destructive cancel surfaces instead of being silently swallowed (:135-142). - Response → frontend: on success the service returns
{ …, canceledWarehouse: true }, the controller echoeswarehouseReassign, anddownloadShippingLabelHandler.ts(_makeCanceledWarehouseRes) returns a canceled-marker record so the frontend skips thepdfNotFounderror; thefinally-siderefreshWorkOrders()re-fetch then opens the existing canceled-SKU modal (src/components/admin/ShipStation/utils.ts:60-64). - Auth gotcha:
fulfilGetShipmentsWarehouseStateauthenticates with the global Admin-API credential (_adminApiHeaders()— OAuth Bearer or legacyX-API-KEY), not the per-factory_makeToken3PL token. The 3PL token is scoped per warehouse and would fail to read shipments reassigned away from the factory's own warehouse (INFRA-633 trap). No batching inside the client — callers chunk withchunkArray+env.FULFIL_BATCH_SIZE.
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 cleared —
NULL for the two legacy writers, but warehouseReassign for writer #3), 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).