Appearance
Stock Management
Overview
Stock Management is the behavioural side of inventory: how tracked items are deducted when orders are placed, how stock is gated at checkout, and how out-of-stock items behave on Online Ordering and Kiosk. The underlying data (records, history, the Overview/History screens) is described in Inventory.
Key Purpose: Keep customer-facing availability correct by deducting stock on orders, blocking over-selling, and respecting per-order and out-of-stock rules.
Purpose
This covers what happens when stock changes: automatic deduction on committed orders, the checkout stock check, per-order maximum enforcement, and edge synchronisation.
Key Concepts
- Tracking = a record exists: An item is stock-managed only when it has an inventory record at that location.
Item::getTrackQuantity()returns(bool) getInventory()(RawModels/Item.php:544). There is no stored "track stock" toggle. - Stock = replayed history: The current quantity is recomputed from the inventory history (
Restockadds,Bought/Depletionsubtract) rather than read from a single mutable counter (Services/Inventory/InventoryService.php:56). - Untracked items are unlimited: An item with no inventory record is never blocked;
Item::getStock()returns 99999 in that case (RawModels/Item.php:529). - Max Order Limit: A per-item cap on quantity in a single order, enforced when stock is managed (
InventoryService.php:344,Item::getMaxOrderLimit()RawModels/Item.php:264). - D1 Sync + ReloadMenu: After any stock change the new quantity is pushed to Cloudflare D1 and a
ReloadMenuevent fires so ordering channels reflect availability (InventoryService.php:227). - There is no per-item out-of-stock snooze: no
snooze_time_out_of_stock_itemsfield and noSnoozeOutOfStockItemOptionsenum exist anywhere in the backend. Every snooze in the product is a channel pause: the singleSnoozeOptionsenum (accept / 20 / 40 / 60 min / rest-of-day /dont_accept) drives the Online Ordering pause (SnoozeOnlineOrderingRequest,POST /back-office/online-ordering/{locationId}/snooze) and the Table QR pause (SnoozeQrOrderingRequest,POST /back-office/qr-ordering/{locationId}/snooze), which writeonline_ordering_setting.snoozed_untilandqr_ordering_setting.snoozed_untiland are independent of each other. Neither touches individual out-of-stock items; a tracked item simply becomes unavailable at quantity 0. (Verified:app/Enums/SnoozeOptions.phpis the only snooze enum underapp/Enums/;OnlineOrderingService::snooze,QrOrderingService::snooze;routes/api/backoffice/online-ordering.php:10,routes/api/backoffice/qr-ordering.php:11.)
Actions
Deduct Stock on an Order (automatic)
When a customer order is committed, the system groups ordered quantities per item (including linked items referenced by sub-modifiers), and for each tracked item writes a Bought history entry and recomputes the quantity (InventoryService.php:316). Untracked items are skipped.
Check Stock at Checkout
Before committing, the system checks each tracked item: if the remaining stock would go below zero and selling-without-stock is not allowed, it aborts with HTTP 400 "Not enough remaining stock for :itemName" (InventoryService.php:368).
Enforce Per-Order Maximum
If an item has a maxOrderLimit and the ordered quantity exceeds it, the order is rejected with a 400 stating the maximum allowed (InventoryService.php:344).
Adjust Stock Manually
Saving a new target quantity from the Overview screen derives a Restock or Depletion and recomputes the stock. See Inventory for the screen and payload details.
Location
- Back-office Routes:
/inventory/overviewand/inventory/history(nav targetinventory-overview). There is no bare/inventorypage. - Backend Controller:
app/Http/Controllers/Api/InventoryController.php - Stock service:
app/Services/Inventory/InventoryService.php - Vue Components:
src/views/inventory/Overview.vue,src/views/inventory/History.vue
Stock Operations
Restock (increase)
Saving a higher target quantity derives a Restock entry.
Current quantity: 10
Save target: 30
→ Restock entry of 20, new quantity 30Depletion (decrease)
Saving a lower target quantity derives a Depletion entry.
Current quantity: 30
Save target: 25
→ Depletion entry of 5, new quantity 25Bought (automatic deduction)
A committed order writes a Bought entry per tracked item.
Customer orders 2x Pizza (tracked)
Current quantity: 10
→ Bought entry of 2, new quantity 8Business Logic
Order Stock Flow
Order committed
│
▼
Group ordered quantity per item
(includes linked items via sub-modifiers)
│
▼
For each item:
├── maxOrderLimit set and exceeded? → abort 400 (max allowed)
│
├── item status inactive/unavailable/hidden?
│ ├── pre-commit check → abort 400 "currently unavailable"
│ └── commit phase → log warning, continue
│
└── has inventory record (tracked)?
├── No → skip (unlimited)
└── Yes → remaining = current - ordered
├── remaining < 0 and not sell-without-stock (check) → abort 400
└── commit → write Bought entry, recompute quantity
(logs a warning if it goes negative)Availability on the Storefront
Customer-facing availability is derived from the recomputed stock and expiry. Item::getStock(forCustomerFacing: true) returns 0 for expired stock and the quantity otherwise; an item with no inventory record returns 99999 (always available) (RawModels/Item.php:529). After any change, D1 is updated and ReloadMenu fires so channels refresh.
Storefront-only: exactly how an out-of-stock item is shown or hidden, and any cart re-validation behaviour, is handled by the Online Ordering and Kiosk surfaces and is out of scope for this backend feature.
Customer Impact
Online Ordering & Kiosk
- A tracked item at quantity 0 is treated as out of stock; how each storefront presents it is surface behaviour, not a backend setting.
- An expired record reports 0 stock for customers even if a quantity is stored.
- Untracked items (no inventory record) are always available.
- Stock changes propagate via Cloudflare D1 +
ReloadMenu, so channels reflect availability after a change.
Relations
Depends On
- Inventory: Records and history that hold the quantity (see Inventory)
- Menu Items: Stock is keyed by item;
maxOrderLimitand status come from the item - Locations: Quantities are per location
Affects
- Online Ordering: Item availability and checkout gating
- Kiosk: Item availability
- Transactions: A committed order deducts stock (
Boughthistory) - Cloudflare D1: Edge availability cache
Related Features
- Inventory — data model, history, the Overview/History screens
- Menu Items
- Locations
Business Rules
- Stock is recomputed from inventory history (
Restockadds,Bought/Depletionsubtract); a negative result is rejected with 400 "Not enough remaining stock for :itemName" unless selling-without-stock is allowed (InventoryService.php:56,InventoryService.php:368). - Only items with an inventory record are stock-gated; an item without one is unlimited (
Item::getStock()returns 99999,RawModels/Item.php:529). maxOrderLimit, when set, caps the quantity per single order and returns a 400 with the maximum when exceeded (InventoryService.php:344).- A committed order writes a
Boughthistory entry per tracked item and recomputes the quantity; at commit time a negative result is logged (not aborted) when selling-without-stock applies (InventoryService.php:370). - Every stock change pushes the new quantity to Cloudflare D1 and fires a
ReloadMenuevent (InventoryService.php:227).
FAQs
- "What happens when a customer orders the last unit?" The order writes a
Boughthistory entry and the quantity reaches 0; the item is then out of stock and each storefront decides how to present it. - "Can I sell an item that has no inventory record?" Yes — items without a record are not stock-gated and are treated as unlimited.
- "What is the difference between Depletion and Bought?"
Boughtis the automatic deduction when an order is committed;Depletionis a manual downward adjustment. - "How is a per-order quantity limit enforced?" Via the item's
maxOrderLimit; exceeding it returns a 400 with the maximum allowed. - "How fast do stock changes reach customers?" Each change updates Cloudflare D1 and fires
ReloadMenu, so channels reflect the new availability shortly after. - "Can I snooze a single out-of-stock item?" No. The only snooze controls are the Online Ordering and Table QR channel pauses, which pause the whole channel rather than one item.
Troubleshooting
Problem: Item shows out of stock incorrectly
Causes:
- Quantity is actually 0 (replay the history to confirm).
- The record has expired — expired stock reports 0 for customers.
- Edge cache (D1) not yet refreshed.
Solutions:
- Save a higher target quantity (a
Restock). - Clear or extend the expiry on the record.
- Saving any change re-pushes to D1 and fires
ReloadMenu.
Problem: Stock not deducting on orders
Causes:
- The item has no inventory record, so it is not tracked.
- The item was added under a different location.
Solutions:
- Add an inventory record for the item at the location.
- Confirm the item/location pairing.
Examples
Tracked item (stored record)
json
{
"item_id": "item-123",
"location_id": "location-amsterdam",
"quantity": 25
}A committed order of 2 writes a Bought entry and the quantity becomes 23.