Skip to content

Inbound and Inventory

openwcs-docs-agent edited this page Jun 15, 2026 · 13 revisions

Inbound & Inventory

Real-time stock is a projection of the append-only transaction log. Nothing writes stock.qty directly — movements are events, and the inventory service applies them.

The two working vertical slices

Goods-in → stock

POST /api/txlog/events {GoodsReceived}  ─►  outbox relay  ─►  txlog.stream (Kafka)  ─►  inventory projection  ─►  stock

Line transactions (all order types)

POST /api/orders/{id}/lines/{lineNo}/transactions
   ─► order-management appends GoodsReceived / Picked / StockAdjusted to txlog (via its outbox)
   ─► relay ─► txlog.stream ─► inventory projection moves stock.qty

INBOUND = receipts (+), OUTBOUND = picks (−), COUNT / ADJUSTMENT = signed adjustments. The actor is recorded on every transaction (authenticated when security is on).

txlog — system of record

  • POST /api/txlog/events writes the immutable event and an outbox row in one transaction; a scheduled relay publishes to txlog.stream in global position order.
  • GET /api/txlog/events?afterPosition= is the global replay feed used to (re)build read models — and to stream confirmations back to the host (see Host Integration).
  • UPDATE/DELETE on the event table are blocked by a trigger.

inventory — durable projection

  • Consumes txlog.stream; movement events (GoodsReceived, PutawayCompleted/StockMoved, Picked, StockAdjusted, StockStatusChanged) move stock.qty (per warehouse × SKU × batch × location × HU × status) and advance a projection cursor.
  • Idempotent: every applied event is recorded in a processed_event inbox keyed on eventId, so redelivery/replay is a no-op and the read model can be rebuilt from the log.
  • Stock follows the HU: for HU-bound events (GoodsReceived, Picked, StockAdjusted, StockStatusChanged) the projection books the bucket to the HU's current registered location, not the location stamped in the event. Count lines capture their location at task creation; by the time a station count confirms the tote may have moved. A log line at INFO records when the two diverge. Deliberate from→to moves (PutawayCompleted, StockMoved) keep their explicit locations. When the HU has no registered location the event location is used as-is.
  • Serves SKU-wide and location-scoped availability/reservations (ATP) used by allocation.
  • Per-location occupancy (POST /api/inventory/locations/occupied, body { "locationIds": [...] }{ "occupiedLocationIds": [...] }): the subset of the given locations that physically hold any stock row or handling unit. Slotting put-away uses it to exclude occupied slots from its candidate set before scoring (see Slotting and Replenishment).
  • Batch/lot & serial instances are created at goods-in (ADR 0001).

Handling unit registry

Physical HU instances (barcode, type, location, status) are managed via /api/inventory/handling-units (CRUD). Type and location are set at registration and are not editable through the registry — the PUT /{id} endpoint preserves the existing huTypeId and locationId from the stored record regardless of what is sent in the request body. Changing an HU's type or moving it to a different location can only happen through a controlled process (e.g. a maintenance or QA work cycle). The registry UI enforces this by disabling the Type and Location dropdowns when editing an existing HU.

Transport lifecycle location booking (PUT /api/inventory/handling-units/{id}/location, body { locationId: uuid | null }, 404 for unknown HU): the controlled path the full PUT defers to. Flow-orchestrator calls this endpoint as a best-effort side effect at two points in the induction lifecycle:

  • RETRIEVE completion — books locationId = null (caller does not know the position): the inventory service resolves this to the warehouse's UNKNOWN operational location (FREE_SPACE/QUARANTINE, lazily created in master-data) so no null ever reaches stock.location_id. The HU and its stock rows are visible in the Stock Overview at UNKNOWN while the tote is away from its slot.
  • Return-leg STORE completion — books the source locationId back: the tote is physically back in its slot. On the first booking of an HU into a storage slot (i.e. the first STORE after goods-in), the service also stamps handling_unit.stored_at (V7 migration, nullable) with the current timestamp. The difference between goods-in receipt and stored_at is the dock-to-stock time surfaced in the Inbound dashboard.

The endpoint moves all stock rows riding in the HU to the new location in the same transaction, so the Stock Overview always reflects the tote's current physical position. Rows that would collide at the target location (same SKU, batch, and status, differing only by location — e.g. a counting adjustment that previously landed on a stale slot) are merged into one summed row before the update; the deletes are flushed to the database before the location updates so the unique-bucket constraint (stock_bucket_uniq) is never violated and the booking never rolls back. A UoM mismatch across merged rows logs a WARN. The HU row and its stock rows are never out of sync.

Stock at UNKNOWN (HUs in transit or at a workplace whose exact position is unknown): remains fully visible in the Stock Overview so admins can see and resolve it, but contributes zero to availability / ATP and can never be reserved — the allocation guard excludes it from both warehouse-wide and location-scoped availability checks, and any reservation attempt targeting UNKNOWN is rejected with InsufficientStock. The UNKNOWN location is resolved from master-data (GET /api/master-data/locations/operational?kind=UNKNOWN; openwcs.inventory.master-data-base-url, default http://localhost:8081; id cached per warehouse in memory). If master-data is unreachable, a null booking is rejected with 503; availability reads degrade gracefully (exclusion skipped, UNKNOWN may transiently count as available).

Implemented by InventoryClient in flow-orchestrator (OPENWCS_INVENTORY_BASE_URL, default http://localhost:8082, 2 s connect/read timeouts). A booking failure is logged as a warning and never breaks the transport pipeline.

Stock Overview screen — Mark for counting

The Stock Overview screen lists current stock rows (SKU × location × HU × status). Each row has a Mark for counting button that creates an ad-hoc BLIND count task scoped to that location and SKU without leaving the screen:

  • Calls POST /api/counting/tasks with scopeType: LOCATION, countType: BLIND, tolerance: 1, and a single cells entry { locationId, skuId }.
  • While the request is in-flight the button shows "Adding…" and is disabled for that row.
  • On success a green notice confirms the task was created (e.g. Count task created for SKU-001 at A-01-01); on failure the error is shown in the existing red alert bar.
  • Rows where locationId or skuId is missing (e.g. unallocated stock) have the button disabled.

The created task appears in the stock-counting screen immediately and follows the normal capture → variance → reconcile → StockAdjusted lifecycle (see the counting service row in Services). For ASRS-family locations the task is created with routingStatus = PENDING and the background CountRoutingScheduler routes the tote to an active GTP STOCK_COUNT station within a minute, retrying on failure (see Goods-to-Person Stations).

Observability — counting service

Every counting lifecycle transition is logged at INFO (normal path) or WARN (degraded path) so that a count task can be traced end-to-end from production logs:

Event Level Key fields
Task created INFO task id, scope type/ref, count type, origin, line count, tolerance; recount of task <id> suffix for recounts
Task claimed by operator INFO task id, operator
Task counted (operator submit) INFO task id, operator, line count, count type
Line reconciled — within tolerance (adjusted) INFO task id, line id, variance, tolerance, delta, SKU, location, HU, actor, event id
Line reconciled — exact match (no adjustment) DEBUG task id, line id, SKU, location
Line flagged for recount INFO task id, line id, variance, tolerance, SKU, location
Task reconciled (no recounts) INFO task id, actor, approved and adjusted counts
Task reconciled with recounts INFO task id, actor, out-of-tolerance count, follow-up task id
Station blind-count: first count differs INFO task id, line id, first counted qty (expected qty withheld)
Station blind-count: counts agree, adjusted INFO task id, line id, agreed qty, delta, SKU, location, HU, actor, event id
Station blind-count: third count required INFO task id, line id
Station blind-count: count matches system qty INFO line id, counted qty, SKU, HU
Task auto-reconciled (all lines terminal) INFO task id, line count
Task deleted INFO task id, scope type/ref, origin
Count routing: FAILED WARN task id, scope type/ref, attempt count, reason; stays OPEN for retry
Count routing: success INFO task id, scope type/ref, new status, reason
Count cell presentation requested INFO tote HU code, SKU, qty, station id, task id, line id, flow entry id
Count cell: no HU at location DEBUG location id, SKU id
Schedule emitted task INFO schedule id/name, task id, scope type/ref, count type, next due, cadence

Host-driven adjustments

A host can post a signed stock correction via POST /api/host/inventory/adjustments (Host Integration); it is appended to the txlog as a StockAdjusted event and applied by the projection like any other movement.

Related: Outbound Flow · Architecture.

Clone this wiki locally