Inventory Management¶
Vectis tracks stock at the variant level per warehouse. The inventory system handles reservations at order time, manual adjustments, and publishes events on every stock change.
Core Concepts¶
Warehouses¶
A warehouse represents any physical or logical stock location. Each warehouse has a name, address, and active/inactive status. You can have one warehouse or dozens.
Stock Levels¶
Stock is tracked per variant per warehouse with these fields:
| Field | Description |
|---|---|
on_hand | Total physical units in the warehouse |
reserved | Units allocated to open orders |
available | Computed: on_hand - reserved |
Note
The available quantity is what the storefront displays and what checkout validates against. A variant with 50 on-hand and 12 reserved shows 38 available.
Stock Adjustments¶
Stock levels change through adjustments. Each adjustment records:
- Variant and warehouse — which stock record to modify.
- Quantity delta — positive (stock received) or negative (stock removed).
- Reason — freeform text for audit (e.g., "Received PO #4421", "Damaged goods write-off").
Manual Adjustments¶
Admins adjust stock directly from the admin panel:
- Navigate to Inventory → Stock Levels.
- Find the variant/warehouse combination.
- Enter the adjustment quantity and reason.
Integration Adjustments¶
External systems (WMS, ERP) adjust stock by publishing events to Redpanda on the vectis.inventory topic. The event consumer processes adjustments and updates stock levels.
Reservations¶
When a buyer completes checkout:
- The system creates a reservation for each line item, incrementing
reservedon the matching stock record. availabledecreases accordingly — other buyers see the reduced availability immediately.- When the order is fulfilled and shipped, the reservation converts to a stock decrease (
on_handreduced,reservedreleased). - If the order is cancelled, the reservation is released —
reserveddecreases andavailablerecovers.
graph LR
CO[Checkout] -->|reserve| R[reserved +5]
R -->|ship| S["on_hand -5, reserved -5"]
R -->|cancel| C["reserved -5 (released)"] Warning
Reservations are tied to the order lifecycle. Manually adjusting reserved without going through the order system can create mismatches.
Backorder Support¶
Variants can be configured to allow orders even when available is zero or negative:
backorder_enabled = true— checkout proceeds regardless of stock. The order is created with a backorder flag.backorder_enabled = false(default) — checkout rejects the line item if quantity exceeds available stock.
Backordered items appear in a dedicated admin filter so fulfillment teams can prioritize restocking.
Inventory Events¶
Every stock change publishes an inventory.adjusted event to Redpanda containing:
| Field | Description |
|---|---|
variant_id | The affected variant |
warehouse_id | The affected warehouse |
adjustment | The quantity delta |
new_on_hand | Updated on-hand quantity |
new_available | Updated available quantity |
reason | Adjustment reason |
External systems (ERP, analytics, alerting) can consume these events to stay in sync.
Tip
Use inventory events to trigger low-stock alerts. Subscribe to vectis.inventory and filter for new_available < threshold.
Multi-Warehouse Fulfillment¶
When multiple warehouses have stock for a variant, the system currently uses the default warehouse for availability checks. Future versions will support warehouse selection strategies (closest warehouse, least-cost routing).
Tracked vs Untracked Products¶
Each product carries a Product.track_inventory boolean flag (nullable; defaults via global setting when unset). When the flag is:
true(default) — reservations andavailableapply as documented above. Cart writes verify stock via the reservation lifecycle.false— Vectis does not maintain stock state for the product. External feeds may writeVariant.external_stock_snapshot(a numeric snapshot column on the DB) for audit only; Vectis never blocks checkout on the snapshot value (the ERP remains the source of truth at fulfillment time).
Reservation Lifecycle (Decided #160)¶
Reservations follow a state machine with TTL expiry:
| State | Meaning |
|---|---|
HELD | Cart wrote a hold; not yet confirmed at place-order |
CONFIRMED | Order placed; ERP/WMS may still be processing |
RELEASED | Order cancelled or refunded before fulfillment |
EXPIRED | TTL elapsed before order placement (cart abandoned) |
FULFILLED | Shipment dispatched; on_hand decremented |
Stale HELD reservations are reaped by ExpireStaleReservationsWorkflow (Temporal). The TTL window lives in the inventory module — see backend/vectis/modules/inventory/services.py for the constant.
Inventory State Version (Decided #234)¶
Product.inventory_state_version is bumped whenever stock policy changes for a product (track_inventory toggle, backorder cap change, external snapshot import). Cart writes capture the current version; place-order re-checks the version inside a serializable transaction and rejects with a state-changed signal if the version moved. This is the race guard that catches "I added these 10 to my cart, then admin disabled tracking, then I checked out" without a stale read.
The clearProductInventoryState(productId): ClearProductInventoryStateResult! mutation bumps the version and releases the relevant HELD reservations — operators use it when re-syncing from an external system.
External Stock Push (Decided #161)¶
ERPs and WMSes feed Vectis with pushExternalStock(input) (API-key scoped). Behaviour depends on track_inventory:
- Tracked product — the push updates
on_hand(with audit log entry) and re-emitsinventory.stock_changed.v1andinventory.restocked.v1events as relevant. - Untracked product — the push only writes
Variant.external_stock_snapshot+external_stock_snapshot_received_at(audit-only).
A push that lowers stock below currently held reservations sets Order.inventory_risk_flag on every affected open order so the operator sees the problem before the buyer does.
Tracking-Enabled-at-Checkout Snapshot (Decided #234)¶
OrderLineItem.tracking_enabled_at_checkout captures whether tracking was enabled on each product at the moment the order was placed. Fulfillment uses this rather than the live Product.track_inventory flag, so toggling tracking after the order is placed doesn't retroactively change pick behavior.
External Fulfillment Handoff (Decided #172)¶
When the order ships from an external system (ERP, 3PL), the receiver calls acknowledgeOrderExternalHandoff(orderId: ID!, externalReference: String). On ack, Vectis releases the internal HELD reservations (the ERP becomes the system of record) and stamps Order.external_handoff_acknowledged_at + Order.external_handoff_reference. If the external system never acknowledges in time, the order surfaces in the Workflow Faults inbox.
MMOQ — Min/Max Per Customer (Decided #236)¶
For tracked products you can declare per-variant caps:
ProductVariant.max_per_customer_per_order— cap per orderProductVariant.max_per_customer_30d— cap across the last 30 daysProductVariant.max_backorder_qty— cap on backordered unitsProductVariant.mmoq_display_unit_id— optional FK to the display package (so the storefront can show "limit 2 cases" instead of "limit 48")
Enforcement happens at the cart aggregate so multiple lines of the same variant can't bypass the cap. Refunds restore quantity to the 30-day window via refunded_quantity_eaches. The window respects the channel timezone (channels.timezone column).
MmoqViolationDetail is returned on cartBulkLookup rows so the storefront can preview violations before commit. clearProductMmoq(productId): Int! lets an admin reset the running 30-day window for the product after a lawful intervention (e.g., legal mediation).
Admin Panel¶
From Inventory in the admin:
- Warehouses — create and manage warehouse locations.
- Stock Levels — view and filter stock by variant, warehouse, or availability status.
- Adjustments — make manual adjustments with audit reasons.
- Backorders — filter orders with backordered items for restocking priority.
- Low Stock — view variants below configurable threshold levels.
- External Snapshots — view the latest
external_stock_snapshotper variant with the receive timestamp. - Inventory Risk — order list with
inventory_risk_flagset; one-click clear once the underlying state is reconciled.