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)Set to warehouseReassign by the canceled-warehouse writer only (INFRA-671); the two legacy writers leave it NULL
cancellationDateTimeDateTime?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:

  1. Per-SKU cancel — _checkFulfilSkuExist (shippinglabel.service.ts:151-219). 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 (updateMany at :210-215):
    where: { workOrderId: { in: mesWorkOrderIdsShouldCancel }, canceledSKU: { not: true } },
    data: { canceledSKU: true, cancellationDateTime: new Date() },
  2. 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: _checkFulfilSkuExist when Fulfil reports zero inventory_moves at all (:165-169, which also throws a 400), and _handleEmptyShipping (:261-278) when the Fulfil shipment state is cancel, or is done with no tracking number.
  3. 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 — sets cancellationSource: 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:

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')

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 getShippingLabelFromFulfil ends up with no label (typeof labelRes.data?.[1] !== 'string', shippinglabel.service.ts:80-83), it calls _cancelForCanceledWarehouse before the legacy _handleEmptyShipping/_checkFulfilSkuExist path. 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: _cancelForCanceledWarehouse reads the shipment's Fulfil state + warehouse via fulfilGetShipmentsWarehouseState (src/clients/fulfilClient.ts:391-408) and cancels only when String(warehouse) === FULFIL_CANCELED_WAREHOUSE_ID and state === 'done' (shippinglabel.service.ts:131-133).
  • Fail-safe direction: empty env, missing row, non-matching warehouse, non-done state, or a warehouse-lookup error all return false (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 echoes warehouseReassign, and downloadShippingLabelHandler.ts (_makeCanceledWarehouseRes) returns a canceled-marker record so the frontend skips the pdfNotFound error; the finally-side refreshWorkOrders() re-fetch then opens the existing canceled-SKU modal (src/components/admin/ShipStation/utils.ts:60-64).
  • Auth gotcha: fulfilGetShipmentsWarehouseState authenticates with the global Admin-API credential (_adminApiHeaders() — OAuth Bearer or legacy X-API-KEY), not the per-factory _makeToken 3PL 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 with chunkArray + 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).