Skip to main content

Pick and pack

Floor routes on fulfillment.controller.ts:

CallPathService
My queueGET /v1/operations/fulfillment-orders/my-queueQuery for the signed-in user
ScanGET /v1/operations/fulfillment-orders/scan/:barcodeLookup by barcode
PickPUT /v1/operations/fulfillment-orders/:id/pickFulfillmentPickPackService.pickItems
PackPUT /v1/operations/fulfillment-orders/:id/packpackItems
StatusPUT /v1/operations/fulfillment-orders/:id/statusFulfillmentWorkflowService.transitionStatus

The floor controller does not declare @RequirePermissions or @TenantScoped. An authenticated user is allowed by the permissions guard. Warehouse confinement still applies when the user is present.

Pick​

pickItems runs in a transaction.

  • The fulfillment order must be OPEN or PICKING, or the call throws fulfillment.error.pickNotAllowed.
  • assertWarehouseFulfillment rejects a dropship worksheet. The comment says there is no stock in the building to pick.
  • The first pick moves OPEN to PICKING and writes history “Started picking items”.
  • Each body line must match a fulfillment line. quantityPicked cannot exceed quantityRequired and cannot be lower than the quantity already picked (fulfillment.error.invalidPickedQuantity, fulfillment.error.invalidPickAllocation).
  • A stocked line (productId set and manageStock true) requires pickedFromBinId. A scanned barcode must match the product barcode, SKU, or code, case-insensitive, or the call throws fulfillment.error.barcodeMismatch.
  • The new units must exist as quantityOnHand in that bin and must fit in ACTIVE reservations for the line. Lots are taken expiry-first. A remainder throws fulfillment.error.pickStockNotFound. The reservation is not tied to a bin. The comment says the reservation is a claim on the site. The pick writes InventoryPick against the stock row the picker scanned.
  • When every line has quantityPicked at least quantityRequired, the worksheet moves to PICKED.

A line with no product, or manageStock false, skips the bin and stock checks and still stores quantityPicked.

A separate PUT .../status to PICKED still throws fulfillment.error.pickIncomplete when any line is short. The pick method moves the status itself when the quantities already qualify.

Pack​

packItems allows only PICKED or PACKING (fulfillment.error.packNotAllowed). The first pack moves PICKED to PACKING.

Each line must have a product (fulfillment.error.lineItemProductUnmapped). quantityPacked cannot exceed quantityRequired or quantityPicked (fulfillment.error.invalidPackedQuantity). A scanned barcode uses the same match as pick. When every line is fully packed, status becomes PACKED and packedById is the user.

transitionStatus to PACKED still throws fulfillment.error.packIncomplete when any line is short. The pack method itself moves the status when the quantities already qualify, so the status route is the gate for a manual jump.

Hold​

PUT /v1/operations/fulfillment-orders/:id/hold is a status transition to ON_HOLD. The workflow stores holdReason from notes when the target is ON_HOLD, and clears it when the target is OPEN.

Leaving ON_HOLD for OPEN calls reserveReplenishedBackorders before the status write.

ON_HOLD can return only to OPEN or CANCELLED in the fulfillment map.

Assign​

batchAssign checks that assignedUserId is a user with deletedAt: null (user.error.userNotFound). For each fulfillment id it sets assignedTo and writes history with the same from and to status. Assignment does not start picking and does not change status. An id outside the caller’s fulfillment scope is fulfillment.error.notFound and is recorded as a failed row. The loop continues. Single PUT .../:id/assign uses the same service method with one id.