Inventory
Stock tracking for products and variants. Single service, JSON column, decremented atomically during order creation, restored only on cancel. Refund does NOT restore stock.
1. Overview
Stock is a JSON blob on both Product.inventory and ProductVariant.inventory, shape { trackQuantity, quantity, lowStockThreshold? }. All mutations go through backend/lib/inventory-service.ts — no direct writes elsewhere. Decrement runs inside the order-create transaction; restore runs on cancel or hard delete. Refund does not restore.
2. Architecture
3. Data model
{ "trackQuantity": true, "quantity": 42, "lowStockThreshold": 5 }Product.inventory at schema.prisma:L329; ProductVariant.inventory at L366. No dedicated table, no ledger.
4. Key flows
4.1 Decrement on order create (atomic)
4.2 Restore on cancel (NOT atomic with status)
Server crash between tx1 and tx2 → order cancelled, stock not restored. Errors log console.warn only.
5. Lifecycle
6. Key files
- backend/lib/inventory-service.ts:
checkInventoryAvailability— read-onlycheckAndReduceInventory— atomic with a passedtxreduceInventory— raw (avoid)restoreInventorygetInventoryLevels- L441-L445 — variant
lowStockThresholdfalls back to parent product's
- backend/routes/orders.ts:L517 — manual order create calls
checkAndReduceInventory(..., tx) - backend/routes/public-catalog.ts:L549 — public checkout, same pattern
- backend/routes/orders.ts:L799-L810 — edit path, passes outer
txcorrectly - backend/routes/orders.ts:L1031 — cancel: calls
restoreInventoryWITHOUT the outer tx - backend/routes/orders.ts:L2378 — hard delete restore
7. Env vars & config
None. Inherits DB. lowStockThreshold is per-product in the JSON.
8. Gotchas & troubleshooting
- Refund does NOT restore stock → Only
cancelledand delete do. Refund without return = stock silently lost. - Cancel restore is not atomic with status flip → Two separate transactions. Real inconsistency risk under failure.
- Variant-bearing products ignore
Product.inventory.quantity→ If a line item hasvariantId, only the variant counter moves. The product-level count is dead weight. reduceInventoryis read-modify-write → Not an atomic SQL decrement. Safety relies on the caller's transaction. Calling it outside a tx = data race.- Oversell is silently clamped at 0 →
Math.max(0, quantity - reduce). Order succeeds even if stock insufficient.checkAndReduceInventoryguards this;reduceInventoryalone does not. - No audit trail → Debugging drift means correlating against Order history manually.
- Low-stock threshold is UI-only metadata → No alert system.
9. Extension points
- Add a ledger → insert
InventoryAdjustmentrows (new model) from inside the service. Non-breaking. - Restore on refund → one-line at the refund handler; watch for double-restore if refund follows cancel.
- Atomic SQL decrement → migrate to
quantity INTcolumn, useUPDATE ... WHERE qty >= :n; removes tx dependency.