Pick and pack
Floor routes on fulfillment.controller.ts:
| Call | Path | Service |
|---|---|---|
| My queue | GET /v1/operations/fulfillment-orders/my-queue | Query for the signed-in user |
| Scan | GET /v1/operations/fulfillment-orders/scan/:barcode | Lookup by barcode |
| Pick | PUT /v1/operations/fulfillment-orders/:id/pick | FulfillmentPickPackService.pickItems |
| Pack | PUT /v1/operations/fulfillment-orders/:id/pack | packItems |
| Status | PUT /v1/operations/fulfillment-orders/:id/status | FulfillmentWorkflowService.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
OPENorPICKING, or the call throwsfulfillment.error.pickNotAllowed. assertWarehouseFulfillmentrejects a dropship worksheet. The comment says there is no stock in the building to pick.- The first pick moves
OPENtoPICKINGand writes history “Started picking items”. - Each body line must match a fulfillment line.
quantityPickedcannot exceedquantityRequiredand cannot be lower than the quantity already picked (fulfillment.error.invalidPickedQuantity,fulfillment.error.invalidPickAllocation). - A stocked line (
productIdset andmanageStocktrue) requirespickedFromBinId. A scanned barcode must match the product barcode, SKU, or code, case-insensitive, or the call throwsfulfillment.error.barcodeMismatch. - The new units must exist as
quantityOnHandin that bin and must fit inACTIVEreservations for the line. Lots are taken expiry-first. A remainder throwsfulfillment.error.pickStockNotFound. The reservation is not tied to a bin. The comment says the reservation is a claim on the site. The pick writesInventoryPickagainst the stock row the picker scanned. - When every line has
quantityPickedat leastquantityRequired, the worksheet moves toPICKED.
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.