Inventory

Inventory is managed per location, with dynamic threshold rules, sales velocity & stockout forecasting, auto-reorder, and backorders/pre-orders.

Multi-location

inventory_levels/update webhooks record per-location stock at /content/commerce/inventory/levels/{inventory_item_id}.json (newest wins).

{
  "inventory_item_id": "123456",
  "locations": { "loc_1": { "available": 15, "updatedAt": "..." } }
}
  • aggregates and per-location breakdowns come from commerce.Locations
  • allocation planning (commerce.Allocation) supports most_stock / priority strategies
  • config: /etc/commerce/config/locations.yml (strategy / priorityOrder / defaultSafetyStock)
  • read: GET …/endpoints/inventory-locations.groovy?productId=123[&variantId=456&qty=10]

Threshold rules

The effective threshold is resolved by precedence: manual override → rule match → default → none.

/etc/commerce/config/inventory-rules.yml:

default: 5
rules:
  - name: "Perishable"
    match: { productType: ["Food", "Beverage"] }
    threshold: 20
  - name: "High velocity"
    match: { minVelocityPerDay: 5 }
    threshold: 25

Match criteria: productType / vendor / tags (any match) / season (MM-DD window, wraps year-end) / minVelocityPerDay. Rules are evaluated top-down; the first rule whose criteria all hold wins.

Sales velocity & stockout forecast

A timer (every 6h by default) computes per-variant velocity (units/day) from order history and caches it at /content/commerce/analytics/velocity.json.

daysToStockout = current_stock / perDay

Variants predicted to run out within stockout.warnDays are alerted (debounced via commerce.Alerts). Config: /etc/commerce/config/velocity.yml (windowDays / stockout.warnDays / cooldownMinutes). Read: GET …/endpoints/forecast.groovy?warnDays=7.

Auto-reorder

A daily timer proposes purchase orders from velocity and stock.

need = velocity * (leadTimeDays + targetCoverDays) - currentStock
qty  = roundUp(max(ceil(need), minOrderQty), roundTo)

Proposals are recorded at /content/commerce/purchase-orders/{yyyy}/{MM}/po_*.json (review_pending) and routed to an approval BPMN flow. On approval, the order is sent per supplier.delivery (none / email / webhook). Config: /etc/commerce/config/reorder.yml. Status: review_pending → approved/rejected → ordered/order_failed.

Backorders / pre-orders

At order time, a shortfall (shortfall) or a pre-order tag (preorder) is detected and recorded.

backordered → ready → released   (normal)
backordered → cancelled          (on refund)
  • detection: detectBackorders.groovy in the order route (idempotent by order + line)
  • release: when stock arrives, oldest-first (FIFO) Release tasks are raised for what stock covers
  • cancellation: on refund, the order's un-released backorders are cancelled
  • records: /content/commerce/backorders/{yyyy}/{MM}/backorder_{orderId}_{lineItemId}.json
  • config: /etc/commerce/config/backorder.yml (preorderTags / notify.onCreated / notify.onReady)

The alert tool flow

A product webhook starts the inventory-alert workflow → on first run, if no threshold is set, a "Set Inventory Threshold" task is raised → when stock < threshold an "Inventory Review" task is raised → a notification is sent. If a workflow is already running for the same product, the new webhook is skipped. Handle tasks in the Webtop Tasks app.