Skip to main content

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:

ColumnTypeNotes
canceledSKUBoolean @default(false)Indexed on its own (@@index([canceledSKU]), schema.prisma:178)
cancellationSourceString? @db.VarChar(25)No live writer in this codebase — read by analytics only
cancellationDateTimeDateTime?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:

  1. Per-SKU cancel_checkFulfilSkuExist (shippinglabel.service.ts:141-156). For a shipment with ≥2 non-suspended MES WorkOrders, it fetches Fulfil's inventory_moves for that shipment, tallies quantity per SKU (skipping moves in state cancel, 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() },
  2. 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: _checkFulfilSkuExist when Fulfil reports zero inventory_moves at all (:109-114, which also throws a 400), and _handleEmptyShipping (:202-219) when the Fulfil shipment state is cancel, or is done with 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:

ReaderEffect
src/components/admin/ShipStation/ShipStation.tsx:51 + ShipStationCanceledSKUModal.tsxShipStation surfaces a blocking/warning modal at print time; hasNonCanceledSKU gates the normal path
src/components/admin/ShipStation/utils.ts:237-248Splits fetched records into canceledSKURecords for that modal
src/components/common/RecordInList/RecordInList.tsx:151Renders the row struck-through/highlighted in the ShipStation list only
src/models/purchaseorder/purchaseorder.model.ts:94-112PO progress/count rollups filter wo."canceledSKU" = false; a separate branch counts = true
src/models/workOrderShipmentLatency/workOrderShipmentLatency.model.ts:155Latency report hard-excludes canceled SKUs
src/routers/admin/resources/canceledShipment/listActionHandler.ts:111The Canceled Shipment resource list is defined as canceledSKU: true
src/routers/admin/resources/workorder/listActionHandler.ts:133-135WorkOrder 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).