Concepts, value, and typical clone scenarios — less code.
Concepts, value, and typical clone scenarios — less code.
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Підготовка контенту платформи Ring
Ring tracks quantity with one locked rule: product stock equals available plus reserved. Checkout sells only available. PaymentConductor paid handlers call commitSaleForOrder so fulfill and deduct stay atomic.
Do not treat Nova Poshta /api/shipping/novapost/warehouses as ERP warehouses. ERP default: label zero-warehouse ↔ store id 1 (DEFAULT_INVENTORY_STORE_ID).
store_products.stock === inventory_levels.available + inventory_levels.reserved
sellable === inventory_levels.available| Event | stock | available | reserved |
|---|---|---|---|
| Bootstrap / restock +q | +q | +q | — |
| Reserve q | — | −q | +q |
| Fulfill (paid) q | −q | — | −q |
| Cancel / TTL q | — | +q | −q |
| Refund restore +q | +q | +q | — |
Wave 1 (2026-07-23): digital / instantDelivery / preorder lines skip reserve+deduct (commissions still settle). Authenticated carts soft-hold via cart_${userId} (5 min TTL) then promote at checkout. ProcessConductor inventory-drift asserts the invariant. MCP: ring-stock-get / ring-stock-low / ring-stock-adjust / ring-reservation-list.
After paid deduct, levels re-assert available = stock − reserved so paid-without-hold paths cannot leave stale available.
/admin/store — see totals and pending settlements./admin/store/stock — filter low / critical / out; restock with a reason; read the movement timeline (warehouse column shows zero-warehouse)./vendor/stock — only their products; bulk restock supported.If checkout returns “Insufficient inventory”, sellable (available) was too low — not “the product.stock field looked high.” Reserved units are already held for another open order.
| Surface | Route | Actions |
|---|---|---|
| Hub | /admin/store | Summary cards + tabs |
| Stock | /admin/store/stock | Filters, reason restock, movements |
| Vendor stock | /vendor/stock | Ownership-gated restock + bulk |
features/store/constants/stock.ts — ZERO_WAREHOUSE_ID, DEFAULT_INVENTORY_STORE_ID, STOCK_THRESHOLDS.
Ring tracks quantity with one locked rule: product stock equals available plus reserved. Checkout sells only available. PaymentConductor paid handlers call commitSaleForOrder so fulfill and deduct stay atomic.
Do not treat Nova Poshta /api/shipping/novapost/warehouses as ERP warehouses. ERP default: label zero-warehouse ↔ store id 1 (DEFAULT_INVENTORY_STORE_ID).
store_products.stock === inventory_levels.available + inventory_levels.reserved
sellable === inventory_levels.available| Event | stock | available | reserved |
|---|---|---|---|
| Bootstrap / restock +q | +q | +q | — |
| Reserve q | — | −q | +q |
| Fulfill (paid) q | −q | — | −q |
| Cancel / TTL q | — | +q | −q |
| Refund restore +q | +q | +q | — |
Wave 1 (2026-07-23): digital / instantDelivery / preorder lines skip reserve+deduct (commissions still settle). Authenticated carts soft-hold via cart_${userId} (5 min TTL) then promote at checkout. ProcessConductor inventory-drift asserts the invariant. MCP: ring-stock-get / ring-stock-low / ring-stock-adjust / ring-reservation-list.
After paid deduct, levels re-assert available = stock − reserved so paid-without-hold paths cannot leave stale available.
/admin/store — see totals and pending settlements./admin/store/stock — filter low / critical / out; restock with a reason; read the movement timeline (warehouse column shows zero-warehouse)./vendor/stock — only their products; bulk restock supported.If checkout returns “Insufficient inventory”, sellable (available) was too low — not “the product.stock field looked high.” Reserved units are already held for another open order.
| Surface | Route | Actions |
|---|---|---|
| Hub | /admin/store | Summary cards + tabs |
| Stock | /admin/store/stock | Filters, reason restock, movements |
| Vendor stock | /vendor/stock | Ownership-gated restock + bulk |
features/store/constants/stock.ts — ZERO_WAREHOUSE_ID, DEFAULT_INVENTORY_STORE_ID, STOCK_THRESHOLDS.
Ring tracks quantity with one locked rule: product stock equals available plus reserved. Checkout sells only available. PaymentConductor paid handlers call commitSaleForOrder so fulfill and deduct stay atomic.
Do not treat Nova Poshta /api/shipping/novapost/warehouses as ERP warehouses. ERP default: label zero-warehouse ↔ store id 1 (DEFAULT_INVENTORY_STORE_ID).
store_products.stock === inventory_levels.available + inventory_levels.reserved
sellable === inventory_levels.available| Event | stock | available | reserved |
|---|---|---|---|
| Bootstrap / restock +q | +q | +q | — |
| Reserve q | — | −q | +q |
| Fulfill (paid) q | −q | — | −q |
| Cancel / TTL q | — | +q | −q |
| Refund restore +q | +q | +q | — |
Wave 1 (2026-07-23): digital / instantDelivery / preorder lines skip reserve+deduct (commissions still settle). Authenticated carts soft-hold via cart_${userId} (5 min TTL) then promote at checkout. ProcessConductor inventory-drift asserts the invariant. MCP: ring-stock-get / ring-stock-low / ring-stock-adjust / ring-reservation-list.
After paid deduct, levels re-assert available = stock − reserved so paid-without-hold paths cannot leave stale available.
/admin/store — see totals and pending settlements./admin/store/stock — filter low / critical / out; restock with a reason; read the movement timeline (warehouse column shows zero-warehouse)./vendor/stock — only their products; bulk restock supported.If checkout returns “Insufficient inventory”, sellable (available) was too low — not “the product.stock field looked high.” Reserved units are already held for another open order.
| Surface | Route | Actions |
|---|---|---|
| Hub | /admin/store | Summary cards + tabs |
| Stock | /admin/store/stock | Filters, reason restock, movements |
| Vendor stock | /vendor/stock | Ownership-gated restock + bulk |
features/store/constants/stock.ts — ZERO_WAREHOUSE_ID, DEFAULT_INVENTORY_STORE_ID, STOCK_THRESHOLDS.
inventory-sync.tsBootstrap {available: stock, reserved: 0} when missing |
reserveInventoryForOrder | inventory-sync.ts | Never skips; 409 when insufficient available |
releaseReservation(id, fulfilled) | inventory-sync.ts | Cancel restores available; fulfill decrements reserved only (Flaw E) |
commitSaleForOrder | inventory-sync.ts | Single txn; idempotent if sale movements exist for orderId |
restoreStockForOrder | inventory-sync.ts | Refund/void; idempotent via return movements |
updateStock | erp-stock-service.ts | Transactional product + levels; reject set below reserved |
POST /api/erp/stock/initialize | app/api/erp/stock/initialize/route.ts | Admin bulk init |
commitSaleForOrder)lib/payments/conductor/handlers/store-order.ts (WFP) — also restoreStockForOrder on Refunded/Voidedstore-order-stripe.ts, store-order-paypal.tsapp/api/store/payments/credit/route.ts, …/token/route.tsapp/_actions/admin-orders.ts → unpaid canceled releases holds; paid cancel restores via restoreStockForOrder.
TTL: ProcessConductor cleanup-reservations → cleanupExpiredReservations (cancel path).
WayForPay Refunded/Voided calls restoreStockForOrder. Stripe/PayPal refund event parity is still open — tracked as a FutureFeature on Commissions.
inventory-sync.tsBootstrap {available: stock, reserved: 0} when missing |
reserveInventoryForOrder | inventory-sync.ts | Never skips; 409 when insufficient available |
releaseReservation(id, fulfilled) | inventory-sync.ts | Cancel restores available; fulfill decrements reserved only (Flaw E) |
commitSaleForOrder | inventory-sync.ts | Single txn; idempotent if sale movements exist for orderId |
restoreStockForOrder | inventory-sync.ts | Refund/void; idempotent via return movements |
updateStock | erp-stock-service.ts | Transactional product + levels; reject set below reserved |
POST /api/erp/stock/initialize | app/api/erp/stock/initialize/route.ts | Admin bulk init |
commitSaleForOrder)lib/payments/conductor/handlers/store-order.ts (WFP) — also restoreStockForOrder on Refunded/Voidedstore-order-stripe.ts, store-order-paypal.tsapp/api/store/payments/credit/route.ts, …/token/route.tsapp/_actions/admin-orders.ts → unpaid canceled releases holds; paid cancel restores via restoreStockForOrder.
TTL: ProcessConductor cleanup-reservations → cleanupExpiredReservations (cancel path).
WayForPay Refunded/Voided calls restoreStockForOrder. Stripe/PayPal refund event parity is still open — tracked as a FutureFeature on Commissions.
inventory-sync.tsBootstrap {available: stock, reserved: 0} when missing |
reserveInventoryForOrder | inventory-sync.ts | Never skips; 409 when insufficient available |
releaseReservation(id, fulfilled) | inventory-sync.ts | Cancel restores available; fulfill decrements reserved only (Flaw E) |
commitSaleForOrder | inventory-sync.ts | Single txn; idempotent if sale movements exist for orderId |
restoreStockForOrder | inventory-sync.ts | Refund/void; idempotent via return movements |
updateStock | erp-stock-service.ts | Transactional product + levels; reject set below reserved |
POST /api/erp/stock/initialize | app/api/erp/stock/initialize/route.ts | Admin bulk init |
commitSaleForOrder)lib/payments/conductor/handlers/store-order.ts (WFP) — also restoreStockForOrder on Refunded/Voidedstore-order-stripe.ts, store-order-paypal.tsapp/api/store/payments/credit/route.ts, …/token/route.tsapp/_actions/admin-orders.ts → unpaid canceled releases holds; paid cancel restores via restoreStockForOrder.
TTL: ProcessConductor cleanup-reservations → cleanupExpiredReservations (cancel path).
WayForPay Refunded/Voided calls restoreStockForOrder. Stripe/PayPal refund event parity is still open — tracked as a FutureFeature on Commissions.