Skip to main content

Order lifecycle — inbound receiving

Inbound receiving is the receive-only path through the Loca.fi order state machine: Open → Receiving → Received → Completed. It applies to the Inbound, Replenishment, and Return order types — none of them has an allocate or dispatch leg. This guide walks that one path end-to-end; the allocate/dispatch side (Outbound, Transfer, Loan) is covered by Order lifecycle — outbound & transfers.

Overview

The happy path is four calls:

  1. Create the order with its expected lines — POST api/orders/createorder (OrderType: "Inbound", a ToPlaceId, OrderSkus with RequiredCount).
  2. Scan arriving stock — POST api/orders/addsnapshot. The first snapshot automatically moves the order Open → Receiving; tags the platform has never seen auto-create items against the matching SKU.
  3. ReceivePOST api/orders/receiveorder. A stage gate checks every line's quantity constraint and that no error tags remain.
  4. CompletePOST api/orders/completeorder. Terminal.

Between scanning and receiving you may need to resolve errors: remove bad scans (a Remove snapshot) or key in receipts by hand (manualfulfillment). Every step writes an OrderTransactionHistory row, so the whole story is reconstructable from getordertransactions.

All order endpoints are gated on the Order module permission (createorder and appendtags need Create; the lifecycle operations need Update).

Prerequisites

  • A token with Order create + update permission — see Getting started.
  • The destination place (ToPlaceId) exists and is visible to the caller — inbound-type orders validate ToPlaceId, not FromPlaceId.
  • Every OrderSkus line references an operational SKU (Draft or Discontinued SKUs are rejected on new lines).
  • Skim Order lifecycle for the full state machine — this guide walks one path through it.

Step-by-step

1. Create the inbound order

curl -sk -X POST "http://<host>/api/orders/createorder" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderType":"Inbound",
"CustomerOrderNumber":"PO-2026-0142",
"ToPlaceId":"<warehouseId>",
"ExpectedArrivalDate":"2026-07-10T00:00:00Z",
"OrderSkus":[
{"SkuId":"<milkSkuId>","RequiredCount":24},
{"SkuId":"<breadSkuId>","RequiredCount":12,
"OrderQtyConstraintAction":"UnderOnly"}]}'

Returns 200 with the new order's OrderDetailDtoOrderState is Open. Capture the Id.

Per line, RequiredCount is measured in outer-SKU units and OrderQtyConstraintAction sets the receive gate for that line: Exact (±0.01), OverOnly, UnderOnly, or All (any quantity). Omit it to use the order-level setting. OrderUniqueItems ({ ItemId } rows) declares specific items expected on the order, alongside or instead of quantity lines.

Retail vs Asset

The pricing fields on each line — UnitPriceSnapshot, UnitCostSnapshot, LineDiscount (must be ≥ 0) — and the order's Channel (InStore/Online/Marketplace) are retail concerns. When the snapshots are omitted the server auto-captures them from the SKU's current pricing at line creation. Asset-management tenants can ignore all four.

2. Scan arriving stock

Feed reader/handheld scans to POST api/orders/addsnapshot:

curl -sk -X POST "http://<host>/api/orders/addsnapshot" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>",
"PlaceId":"<warehouseId>",
"SnapshotType":"Add",
"StartTime":"2026-07-10T09:00:00Z",
"EndTime":"2026-07-10T09:02:30Z",
"Tags":[
{"TagNumber":"3034F8C000000000000001A1","TagType":"PassiveRfid",
"ReadCount":14,"Rssi":-52,"LastReadTime":"2026-07-10T09:01:12Z"}]}'

Returns 200 with an OrderOperationResultDto whose OrderDto carries the updated order. On the first snapshot the order moves Open → Receiving automatically — there is no explicit "start receiving" call.

What each scan does:

  • A tag resolving to a known item marks it received against its SKU line (ReadOrderSkuDto.ReceivedCount and ReceivedItems update on the detail), moves the item to the order's ToPlace, and sets its item state to Received (Restored on Return orders — the only exit from Consumed).
  • An unknown tag auto-creates an item for the matching SKU line — inbound receipts are how new stock enters Loca.fi.
  • A scan that violates a constraint (wrong SKU, quantity overrun, …) records an error tag instead: the order's OrderStatus flips to Errored and the detail's OrderErrors lists each violation.

Repeat with more snapshots as stock arrives — the order stays in Receiving. The addsnapshot/{ignoreExisting} variant controls whether tags already sitting at the snapshot place are skipped (the base route defaults it to true; it applies to receiving scans only and is forced off under a Strict receipt constraint).

3. Check progress and resolve errors

Poll the detail while receiving:

curl -sk "http://<host>/api/orders/getorder/<orderId>" \
-H "Authorization: Token $TOKEN"

Watch OrderSkuList[].ReceivedCount against RequiredCount, and OrderErrors (each row carries the offending SKU/item plus violation flags — IsQtyViolation, IsOrderSkuViolation, IsItemStateViolation, …). OrderStatus: "Errored" blocks Complete, but not the operations that can fix the errors. Two repair tools:

Remove bad scans — post a snapshot with SnapshotType: "Remove" containing the tags to back out (allowed only while the order is in Receiving). Each removed item reverts to the state and place captured when it was scanned:

curl -sk -X POST "http://<host>/api/orders/addsnapshot" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>","PlaceId":"<warehouseId>",
"SnapshotType":"Remove",
"StartTime":"2026-07-10T09:30:00Z","EndTime":"2026-07-10T09:30:10Z",
"Tags":[{"TagNumber":"3034F8C000000000000001A1"}]}'

Manual fulfillment — key in receipts without a physical scan via POST api/orders/manualfulfillment. The server synthesises a snapshot from the tag numbers you supply:

curl -sk -X POST "http://<host>/api/orders/manualfulfillment" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>",
"ManualFulfillmentType":"ManualReceive",
"FulfillmentTags":[
{"TagNumber":"3034F8C000000000000001B2",
"ManualTagProcessingType":"Present"},
{"TagNumber":"3034F8C000000000000001C3",
"ManualTagProcessingType":"Missing"}]}'

Present records a normal receipt; Missing records the receipt and then marks the item Missing — use it to acknowledge a line item that never arrived. Manual fulfillment deliberately does not auto-Receive the order, so you can repeat it as many times as needed before step 4.

4. Receive the order

curl -sk -X POST "http://<host>/api/orders/receiveorder/<orderId>" \
-H "Authorization: Token $TOKEN" -w "\nHTTP:%{http_code}\n"

(Equivalently POST api/orders/receiveorder with a ProcessOrderDto body — {"OrderId":"<orderId>"}.) The stage gate passes only when the order has at least one snapshot, every line's quantity constraint is satisfied, and no error tags remain (error tags are re-evaluated first — items that now pass are promoted). On success the order moves to Received, ReceivedOn is stamped, and any expected-but-unreceived items the order had claimed are swept to Missing.

200 despite failure

receiveorder and completeorder return HTTP 200 even when the transition is refused — the body is the order detail containing the errors. Never trust the status code alone: compare the returned OrderState against what you expected and inspect OrderErrors. A failed gate also logs a ReceiveFailed transaction. See Errors & response conventions.

Received too early, or need more scans? Wind the state back with POST api/orders/revertordertoreceiving/{id} (allowed only from Received), scan again, then re-Receive.

5. Complete the order

curl -sk -X POST "http://<host>/api/orders/completeorder/<orderId>" \
-H "Authorization: Token $TOKEN"

Moves Received → Completed (terminal) and stamps CompletedOn. A still-Errored order is refused up front with a 400 InvalidOperation. To abandon instead, POST api/orders/cancelorder/{id} works from any non-terminal state — received items revert to Present, un-received ones go to Missing.

6. Async variant for large receipts

addsnapshot, appendtags, receiveorder, and completeorder all accept the async pattern: add NotifyOnceComplete: true and set NotificationRecipientId to your own user id (any other id silently runs the request synchronously):

curl -sk -X POST "http://<host>/api/orders/receiveorder" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>",
"NotifyOnceComplete":true,
"NotificationRecipientId":"<yourUserId>"}'

The response is an immediate 200 with OrderOperationResultDto.BackgroundTaskId (and no OrderDto) — it is an acknowledgement, not the outcome. Poll GET api/backgroundtasks/GetBackgroundTask/{id} until the task reaches Complete/Errored, or consume the auto-created completion-event subscriptions. Then fetch the order detail to see the real state.

Ad-hoc receiving with appendtags

When there is no purchase order to receive against, skip the pre-defined lines entirely: create a bare order (step 1 without OrderSkus) and grow it from the scans themselves with POST api/orders/appendtags. The body extends the snapshot DTO with a ProcessAs mode (integer-serialized: 0 = Unique item lines, 1 = OuterSkus, 2 = InnerSkus):

curl -sk -X POST "http://<host>/api/orders/appendtags" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"OrderId":"<orderId>","PlaceId":"<warehouseId>",
"SnapshotType":"Add","ProcessAs":1,
"StartTime":"2026-07-10T10:00:00Z","EndTime":"2026-07-10T10:01:00Z",
"Tags":[{"TagNumber":"3034F8C000000000000001D4"}]}'

Each scan creates or increments the matching order line as it records the receipt (under OuterSkus, an inner-SKU scan accrues 1/InnerQuantity against the outer line). Unknown tags auto-create items here too. Note appendtags requires the Order Create permission, not Update. The Receive/Complete steps are unchanged.

Audit trail

Reconstruct who scanned what, when:

curl -sk "http://<host>/api/orders/getordertransactions/<orderId>" \
-H "Authorization: Token $TOKEN"

Returns every transition — Created, Snapshot, ManualReceive, ReceiveFailed, Received, Completed, … — each with the acting user or agent, timestamp, the snapshot place where relevant, and per-item TransactionRecords (tag number, item, and per-violation flags). For cross-order reporting use the OData-filterable flattened view, GET api/orders/getfilteredordertransactions.

Error handling

All failures follow the platform-wide error and response conventions400 with a JSON array of { ErrorCode, Message, Field } — except where noted.

SituationResponse
Missing/unparseable OrderType on create400 MissingRequiredField / InvalidOperation
ToPlaceId missing or not visible (inbound types)400 InvalidReference, field ToPlaceId
OrderSkus line with an unknown SKU, or a new line on a non-operational (Draft/Discontinued) SKU400
Negative LineDiscount, or Channel not a valid name400 InvalidOperation
Lifecycle call while another operation holds the order's lock, or Complete while Errored400 InvalidOperation with "OrderState => X|OrderStatus => Y"
Snapshot/appendtags in a non-scanning state (Received/Completed/Cancelled)400 OperationFailed (not a clean invalid-state message)
Receive gate fails (quantity constraint / error tags)200 with unchanged OrderState + ReceiveFailed transaction
Complete/Receive on an unknown order id400 NotFound
getorder with an unknown id400 [{"ErrorCode":"NotFound","Field":"Id"}]
getordertransactions with an unknown id200 []
Manual fulfillment when the order's ToPlace cannot be resolved400 "Manual fulfillment requires Order To Place"
Async scheduling failure400 OperationFailed
No token / wrong scheme (Bearer)401; token without the Order permission → 403