Skip to content

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:

  1. Navigate to Inventory → Stock Levels.
  2. Find the variant/warehouse combination.
  3. 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:

  1. The system creates a reservation for each line item, incrementing reserved on the matching stock record.
  2. available decreases accordingly — other buyers see the reduced availability immediately.
  3. When the order is fulfilled and shipped, the reservation converts to a stock decrease (on_hand reduced, reserved released).
  4. If the order is cancelled, the reservation is released — reserved decreases and available recovers.
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 and available apply as documented above. Cart writes verify stock via the reservation lifecycle.
  • false — Vectis does not maintain stock state for the product. External feeds may write Variant.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-emits inventory.stock_changed.v1 and inventory.restocked.v1 events 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 order
  • ProductVariant.max_per_customer_30d — cap across the last 30 days
  • ProductVariant.max_backorder_qty — cap on backordered units
  • ProductVariant.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_snapshot per variant with the receive timestamp.
  • Inventory Risk — order list with inventory_risk_flag set; one-click clear once the underlying state is reconciled.