Appearance
Order Capacity Management
Overview
Order Capacity Management controls how many orders and items your kitchen can handle per time slot. This prevents kitchen overload by limiting orders during busy periods.
Key Purpose: Ensure kitchen can fulfill all orders on time by limiting incoming orders based on capacity.
Purpose
This page lets you configure order and item limits per time slot so your kitchen is never overwhelmed, with support for rush protection, category-specific caps, and time-specific rules.
Key Concepts
- Time Slot: A fixed-duration window (e.g., 15 or 30 minutes) used to group orders. An order still gets a kitchen slot (
order_prep_time= requested time minus preparation time), which drives business hours, blocked periods, the closing cut-off andsent_at. Capacity, however, is counted on the customer pickup slot (order_date= kitchen slot + preparation time). Rush-hour/time-specific rules are matched on the pickup slot too, so a rule written in pickup time applies to the slot the customer actually collects in. - Rush Protection: A flexible limit system that allows extra low-value orders when the slot's total order value is below a configured threshold, preventing one large order from blocking the entire slot.
- Category-Specific Limits: Independent item caps per menu category (e.g., max 6 pizzas per slot for oven capacity), checked alongside global order and item limits.
- Time-Specific Rules: Override rules that apply different capacity limits during specific days and time ranges (e.g., reduced limits during lunch rush), with null fields inheriting from base settings.
- Clamp Behavior: During time-specific windows, rush protection's
allow_up_tovalue is automatically clamped to never exceed the time-specific order cap, ensuring time-specific rules are always the hard ceiling.
Actions
Enable/Disable Capacity Management
Toggle the master capacity switch. When disabled, all capacity checks are fully bypassed and all time slots within business hours are shown as available regardless of configured limits.
Configure Order and Item Limits
Set the maximum number of orders and/or total items allowed per time slot. Both limits are checked simultaneously and an order is rejected if any limit would be exceeded.
Set Up Rush Protection
Enable flexible ordering to allow extra low-value orders when a slot's total value is below the threshold. Configure the expanded order limit (allow_up_to) and the value threshold.
Add Time-Specific Rules
Create named rules that override default limits during specific days and time ranges (e.g., "Lunch Rush" Mon-Fri 12:00-13:30 with reduced order cap). Empty fields inherit from base settings.
Configure Category Limits
Set per-category item caps for slow-prep categories like pizzas or grilled items, ensuring equipment-constrained items do not bottleneck the kitchen.
Location
- Backoffice Route:
/online-settings(file-based pagesrc/pages/online-settings/index.vue; nav entryonline-settings.menu-title) - Where it lives: Online Settings only.
order_capacityis stored under the location's online settings (location.online_settings.order_capacity) and is validated solely byStoreOnlineSettingsRequest. It is NOT part of in-house settings. - Applies to: Online Ordering channel — Pickup, Delivery, and (for the customer-facing time-slot preview only — see below) online eat-in (
For Here). - Does NOT apply to order submission for: any other channel (Kiosk, POS, Table QR Ordering, Uber Eats, Takeaway, Shopify) and the
For Heredining option when the order is actually placed. For these,getOrderCapacity()returnsnulland the order is never capacity-checked or slotted at submission time. - Exception: the customer-facing available-timeslots preview (
getAvailableTimeslots()/POST /available-timeslots) DOES apply capacity filtering to onlineFor Here(eat-in) orders — it runs the sameorder_capacitychecks as Pickup/Delivery, only skipping cut-offs and blocked periods for eat-in. See Eat-In (For Here) and Capacity below for the full breakdown. Table QR Ordering and Kiosk dine-in use separate channels that never callOrderCapacityServiceat all, so they remain fully exempt at every step. - Backend Service:
app/Services/OrderCapacity/OrderCapacityService.php - Vue Component:
src/views/in-house/components/forms/OrderCapacityManagement.vue(rendered withmode="online-settings"fromsrc/views/online-settings/OnlineSettingsForm.vue) - Time-Specific Rule dialog:
src/views/in-house/components/settings/TimeSpecificRuleFormDialog.vue - Category Limit dialog:
src/views/in-house/components/settings/CategoryLimitFormDialog.vue
Component naming note: The Vue component physically lives under
views/in-house/...and accepts amodeprop ('settings' | 'online-settings'), but in practice it is only ever mounted withmode="online-settings"on the Online Settings page. There is no in-house order-capacity surface.
Fields
Capacity Enabled (Master Toggle)
| Property | Value |
|---|---|
| Field ID | enabled |
| Label | Enable Order Capacity Management |
| Type | Toggle (Boolean) |
| Default | false |
| Backend Validation | `nullable |
Description: Master toggle to enable or disable all order capacity management.
Business Logic:
- When
enabled=false: Hard bypass —getOrderCapacity()returnsnulland all capacity checks are skipped. - The available-timeslots path returns every in-hours slot as available, each carrying a
capacity_disabled: trueflag (per-slot informational metadata, not a top-level response field). - Stale configuration values are ignored, preventing accidental enforcement.
Customer Impact:
- Online Ordering: All in-hours slots are bookable. (storefront rendering not verified here)
Why this matters: Prevents the scenario where a merchant disables capacity management but old configured values accidentally block orders.
Time Slot Duration
| Property | Value |
|---|---|
| Field ID | time_slot_duration_min |
| Label | Time Slot Duration |
| Type | Select (UI) |
| Options (UI dropdown) | 5, 10, 15, 20, 30, 45, 60 minutes |
| Default | 15 |
| Required | Yes |
| Backend Validation | `required |
Description: The length of each time slot for capacity calculations. Orders are grouped into these time windows for capacity checking.
Validation note: The 5/10/15/20/30/45/60 list is enforced by the UI dropdown only (timeSlotDurationOptions in the Vue component). The backend only requires a positive number (time_slot_duration_min → required|numeric|gt:0). At runtime the service additionally clamps the value defensively: values ≤ 0 fall back to 30 minutes, and values above 1440 (24 hours) are capped at 1440.
Business Logic:
- Orders are grouped into time slots of this duration
- Capacity limits are checked per slot
- Shorter slots = more precise control but harder to manage
- Longer slots = simpler but less granular control
Customer Impact:
- Online Ordering: Customers see available pickup/delivery times rounded to these intervals. With 15-minute slots, times show as 12:00, 12:15, 12:30, etc. (storefront rendering not verified here)
Examples:
- Fast food (high volume, quick prep): 15 minutes
- Standard restaurant: 30 minutes
- Catering/complex orders: 60 minutes
Maximum Orders Per Time Slot
UI note: The form does NOT expose a separate "enable" toggle for the orders limit. It shows a single number field ("Max orders per slot") backed by the
maxOrdersPerSlotcomputed property. Entering0(or leaving it blank) means "no order limit"; entering a positive number turns the limit on. Under the hood this sets both the boolean flag and the value.
| Property | Value |
|---|---|
| Underlying flag | limit_orders_per_time_slot (boolean) |
| Value field | number_of_orders_per_time_slot |
| UI field | Single number input (0 = no limit) |
| Flag default | false |
| Value default | null |
| Backend Validation | limit_orders_per_time_slot: `required |
Description: The maximum number of orders allowed in each time slot. When set, the system counts existing orders in the slot and blocks a new order once the count reaches the limit.
Business Logic:
- The slot count includes every settled order whose
order_datefalls inside the pickup window. Status-absent stub transactions are deliberately excluded as well asUnpaidones (the filter isstatus: {$nin: [null, "Unpaid"]}— a plain!= "Unpaid"would wrongly count stubs that carry anorder_datebut no status). - Only when the incoming order is itself unpaid does the count also include other unpaid orders whose
order_datefalls in the slot and that were created within the slot reservation TTL (ONLINE_ORDERING_SLOT_RESERVATION_TTL_MINUTES, default 15 minutes). A paying customer is never blocked by not-yet-paid carts. split_timeslotsis a throughput concept only; it no longer subdivides the per-slot count.- A new order is blocked when
current count >= limit(or>= allow_up_towhen rush protection is in effect — see below). - Unpaid duplicates that share an idempotency key are de-duplicated so a double-submit counts as one order.
Customer Impact:
- Online Ordering: When a slot reaches its order limit, that time is reported as unavailable and the storefront offers the next available slot. (storefront UI wording not verified here)
Examples:
- Small kitchen: 3-5 orders per slot
- Medium kitchen: 8-12 orders per slot
- Large kitchen: 15-25 orders per slot
Related Fields: enable_flexible_limit_order_per_time_slot, allow_up_to, value_threshold_per_time_slot
Protect Rush Capacity (Rush Protection)
Currency note: All monetary examples in this section use numbers only. The actual currency depends on your store configuration (€, $, etc.).
| Property | Value |
|---|---|
| Field ID | enable_flexible_limit_order_per_time_slot |
| Label | Protect rush capacity |
| Type | Toggle (Boolean) |
| Default | false |
| Backend Validation | `required |
| Depends On (UI) | The toggle is disabled unless the max-orders field is greater than 0 (maxOrdersPerSlot === 0 disables it). |
Description: Allow extra low-value orders when the slot's total value is still low. Prevents one large order from blocking your entire rush capacity.
UI Behavior (Configure → Summary pattern):
- Toggle ON + not yet configured → shows the inline "Accept up to [X] orders while slot total stays under [€ amount]" input fields
- Toggle ON + configured → shows a green "enabled" badge and summary with an Edit button
- Fields use placeholders (
5for orders,45for the threshold) instead of pre-filled defaults; the threshold input is prefixed with€in the UI
Server-side guardrail: When the request is prepared for validation, if rush protection is toggled on but value_threshold_per_time_slot or allow_up_to is missing/invalid (not a positive number), the backend silently turns rush protection back off before saving. The same guardrail runs per time-specific rule.
Business Logic:
- If the slot's total value <= threshold → use
allow_up_toas the order limit; otherwise use the standardnumber_of_orders_per_time_slotlimit. (The available-timeslots batch check uses a strict<comparison against the threshold; the per-order check uses<=.) - Clamp behavior: When a time-specific rule supplies its own order cap,
allow_up_tois clamped down to that cap if it would exceed it, so a time-specific rule is always the hard ceiling. - Rush protection only takes effect when it is enabled AND fully configured (both
allow_up_to > 0andvalue_threshold_per_time_slot > 0).
Customer Impact:
- Online Ordering: Small orders (like just drinks) may still be accepted even when the slot would otherwise be "full" for larger orders.
Merchant Explanation: "Rush protection lets you accept a few extra low-value orders, as long as the total value of the time slot stays manageable for your kitchen."
Related Fields: allow_up_to, value_threshold_per_time_slot
Rush Protection: Allow Up To
| Property | Value |
|---|---|
| Field ID | allow_up_to |
| Label | Allow up to X extra orders |
| Type | Number |
| Default | null (empty with placeholder "5") |
| Backend Validation | `nullable |
| Depends On | enable_flexible_limit_order_per_time_slot must be true |
Description: Maximum orders allowed when the time slot's total value is below the threshold.
Business Logic:
- This is the "expanded" limit for low-value slots
- Clamp: During time-specific windows, this value is automatically reduced to match the time-specific order cap if it would exceed it
- Example: Base
allow_up_to=5, time-specific cap=2 → effectiveallow_up_to=2during that window
Rush Protection: Value Threshold
| Property | Value |
|---|---|
| Field ID | value_threshold_per_time_slot |
| Label | Slot total threshold |
| Type | Currency (number) |
| Default | null (empty with placeholder "45") |
| Backend Validation | `nullable |
| Depends On | enable_flexible_limit_order_per_time_slot must be true |
Description: If the total order value in a time slot is at or below this amount, the rush protection limit (allow_up_to) applies instead of the standard limit.
Note: The threshold is stored as a plain number and compared in your store's base currency; no currency conversion is applied. The backoffice input hard-codes a € prefix as a display affix.
Business Logic:
- The slot's running total value (existing orders + the order being placed) is compared to this threshold.
- If total <= threshold → use
allow_up_tolimit; if total > threshold → usenumber_of_orders_per_time_slotlimit. (The available-timeslots preview path uses strict<.)
Examples (amounts in your store's currency):
- Set to 45: If slot has 30 in orders, rush protection allows extra orders
- Set to 100: More room for small orders before standard limit kicks in
Merchant Explanation: "Accept up to [X] extra orders, as long as the slot total stays under [your configured amount]."
Maximum Items Per Time Slot
UI note: As with orders, there is no separate "enable" toggle. The form shows a single number field ("Max items per slot") backed by
maxItemsPerSlot;0/blank means "no item limit", a positive number turns it on. This sets the boolean flag and value together behind the scenes.
| Property | Value |
|---|---|
| Underlying flag | limit_items_per_time_slot (boolean) |
| Value field | number_of_items_per_time_slot |
| UI field | Single number input (0 = no limit) |
| Flag default | false |
| Value default | null |
| Backend Validation | limit_items_per_time_slot: `required |
Description: The maximum total quantity of items allowed across all orders in each time slot.
Business Logic:
- The check uses the order's raw quantity (
getRawQty); quantity matters (3x Pizza = 3 items). - The order being placed is rejected if its own quantity exceeds the item limit, or if the slot's existing item total has already reached the limit.
Customer Impact:
- Online Ordering: A single large order may be rejected for a slot or split across later slots. (storefront behavior not verified here)
Examples:
- If limit is 15: Order A (5 items) + Order B (8 items) = 13 items
- Order C (3 items) would be rejected (13+3=16 > 15)
Enable Category-Specific Limits
| Property | Value |
|---|---|
| Field ID | enable_limit_specific_categories |
| Label | Set limits for specific item categories |
| Type | Toggle (Boolean) |
| Default | false |
| Backend Validation | `required |
| Depends On (UI) | The toggle is disabled unless either the max-orders or max-items field is greater than 0. |
Server-side guardrail: If this toggle is on but the category list is empty, the backend turns it back off before saving.
Description: Enable to set different capacity limits for specific menu categories. Useful when some items take longer to prepare.
Business Logic:
- Each category can have its own item limit per slot
- All category limits are checked independently
- An order is rejected if ANY category limit would be exceeded
Customer Impact:
- Online Ordering: If pizza limit is reached but burger limit is not, customer can still order burgers but not pizzas.
Related Fields: limit_specific_categories
Category-Specific Limits
| Property | Value |
|---|---|
| Field ID | limit_specific_categories |
| Label | Category limits |
| Type | Array of objects |
| Backend Validation | `nullable |
| Depends On | enable_limit_specific_categories must be true |
Item Schema (stored):
json
{
"category_id": "string (category id)",
"category_name": "string",
"number_of_items_per_time_slot": "number"
}Field notes:
category_id— required, distinct, string. Validated to exist in the merchant's categories.number_of_items_per_time_slot— required,numeric|gt:0.category_name— added by the category-limit dialog at save time for display only (it is the human-readable label, e.g. "Pizzas" or "Food → Pizzas" for a sub-category). The backend validation rules do not require it.
Description: Define maximum items per time slot for specific categories. The dialog supports selecting a top-level category or a sub-category.
Business Logic:
- For each configured category, the per-category quantity in the slot is compared against its limit.
- Each category limit is checked independently; the order is blocked if any category limit is met or exceeded.
Examples:
- Pizzas: 5 per slot (oven capacity)
- Fries: 10 per slot (fryer capacity)
- Drinks: 20 per slot (easy to prepare)
Time-Specific Rules
| Property | Value |
|---|---|
| Field ID | time_specific_rules |
| Label | Rush-hour rules |
| Type | Array of objects |
| Backend Validation | `nullable |
Item Schema (as written by the rule dialog):
json
{
"name": "string (required, distinct, e.g. 'Lunch Rush')",
"time_range": {
"from": "string (HH:mm)",
"to": "string (HH:mm)"
},
"active_days": ["Monday", "Tuesday"],
"number_of_orders_per_time_slot": "number | null",
"number_of_items_per_time_slot": "number | null"
}Per-rule field validation (order_capacity.time_specific_rules.*):
name—required|string|distincttime_range—required|array;time_range.fromandtime_range.toare eachrequired|stringactive_days—required|array; each value must be one of the seven day names (see Days of Week below)number_of_orders_per_time_slot—nullable|numeric|gt:0number_of_items_per_time_slot—nullable|numeric|gt:0
Backend-only fields: The validation schema and the stored default template also recognise per-rule enable_flexible_limit_order_per_time_slot, value_threshold_per_time_slot, allow_up_to, and limit_specific_categories. However, the rush-hour rule dialog (TimeSpecificRuleFormDialog.vue) does NOT expose these — it only edits name, time range, active days, and the two limit overrides. At runtime the merge logic deliberately ignores per-rule rush protection and category limits and always inherits them from the base settings.
Description: Override the base order/item limits during specific day-and-time windows. Useful for managing rush hours.
UI Behavior:
- The dialog only edits order/item limit overrides (simplified UI).
- "Leave empty to use default": a blank/null override inherits the base value.
- Category limits and rush protection are always inherited from base settings (not editable per rule).
Business Logic:
- A slot is matched to a rule by day-of-week and by
from <= time <= to. When multiple rules could match, the first matching rule wins. - The matched rule is merged onto the base settings:
number_of_orders_per_time_slotset (numeric > 0) → overrides base and forces the orders limit on; otherwise inherits base.number_of_items_per_time_slotset (numeric > 0) → overrides base and forces the items limit on; otherwise inherits base.- Rush protection and category limits → always inherited from base.
Clamp Behavior:
- When a rule supplies an order cap, the base
allow_up_tois clamped down so it can never exceed that cap. - Example: base
allow_up_to=5, rule cap=2 → effectiveallow_up_to=2. - Merchant explanation: "Rush protection can help within a rush-hour window, but it will never exceed the rush-hour limit you set."
Examples:
- Lunch Rush (11:30-13:30, Mon-Fri): reduce to 3 orders/slot
- Weekend Dinner (18:00-21:00, Sat-Sun): reduce to 5 orders/slot
Enums
Days of Week (active_days)
The day values are full English day names, validated server-side against Laravel's Carbon::getDays():
Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
The backoffice rule dialog presents these as chips (displayed truncated to three letters, e.g. "Mon"), but the stored/validated value is the full name. The same day-name set is used for the separate delivery/pickup availability day pickers on the Online Settings page (delivery_available_days / pickup_available_days).
Business Logic
How Limits Work Together
All enabled limits are checked simultaneously. An order is rejected if ANY limit would be exceeded.
Enforcement Order:
- Check if order count limit would be exceeded
- Check if total items limit would be exceeded
- Check if any category-specific limit would be exceeded
- If time-specific rule applies, use those limits instead of defaults
- If flexible ordering enabled and slot value < threshold, use flexible limit
Time Slot Calculation
How the system determines which time slot an order belongs to:
- Take the order's requested pickup/delivery time
- Subtract preparation time to get
order_prep_time - Round
order_prep_timedown to nearest slot boundary - That slot is where the order's capacity is counted
Example:
- Customer requests pickup at 12:45
- Preparation time is 20 minutes
order_prep_time= 12:25- With 15-minute slots, order counts toward 12:15-12:30 slot
The closing-time slot
Slot generation emits an extra bucket that starts exactly at each business-hours window's closing time — a [to, to + duration) range, subject to the same past-today guard as every other slot.
It exists because the storefront offers an inclusive closing slot (a store closing at 21:00 offers 21:00). Backend slot generation used to stop one slot short of closing, so that storefront slot matched no backend range, hit the "allow" fallback, and bypassed capacity limits entirely. Emitting it gives it a real, distinct capacity range key so it is counted and limited like any other slot.
The closing cut-off applies to it. The past-close guard matches on a half-open from <= time < to range, which by definition never matches a slot sitting exactly on to — so the closing slot initially skipped the guard and a store closing at 21:00 could be offered a 21:25 pickup. A slot sitting on a window's to is now matched to that window, unless another window opens on the same boundary: back-to-back windows share that instant, and the window that opens on it owns the slot.
Split Order Handling
When an order is too large for one slot, it can be split across multiple slots:
- Calculate how many items can fit in current slot
- If not all items fit, check next available slot
- Continue until all items are allocated
- Order is assigned to first slot, but capacity counted in all slots
Backend Implementation
Key methods in OrderCapacityService.php:
checkOrderCapacity()(public) — runs the capacity flow for a transaction; returnscalculated_order_dateand aneed_confirmationflag (set when the calculated dispatch time is delayed by at least one slot duration vs. the requested time).getOrderCapacity()(public) — used by order submission / payment capture. Returns the location's capacity settings, ornullwhen the channel is not Online Ordering, the dining option isFor Here, orenabledis false (hard bypass). This is a submission-time bypass only — see Eat-In (For Here) and Capacity for how it differs from the time-slot preview.setTransactionSentAtByOrderCapacity()(public) — assigns the slot and writessent_at/order_prep_time(andsplit_timeslotsif split). Guarded against re-running oncesent_atis set.getSentAt()(public) — computes prep time, the slot boundary, andsent_at.getAvailableTimeslots()(public) — cached for 60 seconds, keyed per location/day/channel/dining option plus the prospective cart's max item prep, value, quantity, and category mix (a low-value cart's result is never served to a high-value cart); used by the storefront availability endpoint. Always called withtransactionChannel = Online Ordering(OnlineOrderingOrchestrator::getAvailableTimeslots()), for Pickup, Delivery, andFor Here(online eat-in). (Verified:app/Services/OrderCapacity/OrderCapacityService.php:1789-1800.)calculateAvailableTimeslots()(private) — the implementation behindgetAvailableTimeslots(). ForFor Hereit sets$isEatIn = trueand still runs the sameorder_capacity-basedbatchCheckTimeslotAvailability()as Pickup/Delivery, but uses the location's business-hours schedule (not the online-ordering pickup/delivery schedule) and skips cut-off and blocked-period checks.calculateSplitTimeslots()(private) — allocates a large order across consecutive available slots.getTimeslotAvailableCapacity()/isTimeSlotAvailableAndUsable()/isTimeSlotAvailable()(private) — per-slot capacity evaluation under a DB lock with retries.isWithinBusinessHours()(private) — delegates to the location's business-hours / online-ordering schedule.
The customer-facing available-slots endpoint is POST /available-timeslots (Api\OnlineOrderingController::getAvailableTimeslots), which delegates to OnlineOrderingOrchestrator. Capacity assignment also runs during payment capture (PaymentCaptureService).
Storefront pre-submit slot re-check
Two storefront behaviours keep the time picker honest about what order placement will actually accept.
1. The timeslot request is cart-aware. Alongside dining_option and day, the online-ordering storefront sends:
item_ids— the cart's item ids, so the backend builds the grid with the same effective prep time it uses at placement.cart_value— the projected order grand total in decimal currency: items net of discounts, plus tax, tip, and the delivery / pickup transaction fee. It deliberately mirrors the backend's order total rather than the bare shopping-bag subtotal, so a slot sitting near the rush-protection value threshold reads as taken in the picker exactly when checkout would reject it.cart_quantity— total item quantity.category_quantities— a map of category id to quantity, for the category-specific caps.
Availability is re-fetched whenever the cart's capacity signature changes — item ids, total quantity, or total value — not only when the item set changes, since bumping an existing item's quantity moves the projected value without changing which ids are in the cart.
2. The committed slot is re-validated just before submit. When the customer presses pay, checkout fetches a fresh cart-aware listing and checks the committed time is still offered. If it is not, the order is not sent: the committed time is kept, the Order Details modal reopens with an inline red notice reading "Your selected time is no longer available for this order. We've suggested the nearest available time — please review and confirm.", and the picker auto-corrects to the nearest still-available slot by minute distance (a tie resolves to the earlier slot).
The re-check deliberately reports "allowed" — no block — in two cases, leaving the backend as the final backstop:
- Nothing is scheduled (table-QR / ASAP flows with no slot).
- The listing comes back empty (capacity disabled, or the fetch failed).
Eat-In (For Here) and Capacity
Online eat-in — the For Here dining option placed through the Online Ordering channel (e.g. a customer scanning a QR code that opens the online storefront and choosing to eat in) — is treated differently depending on which part of the flow is involved:
- Order submission (
getOrderCapacity(), used bycheckOrderCapacity()and payment capture): still a hard bypass. WhendiningOption === 'For Here',getOrderCapacity()returnsnullunconditionally, so the actual order is never blocked or slotted by capacity, regardless of configured limits (app/Services/OrderCapacity/OrderCapacityService.php:308-318). - Customer-facing time-slot preview (
getAvailableTimeslots()/POST /available-timeslots): online eat-in IS included in capacity-based slot filtering.calculateAvailableTimeslots()runs the sameorder_capacitybatch check forFor Hereas it does for Pickup/Delivery — a slot can be returned asavailable: falsebecause of order/item/category/rush-protection limits, exactly as for Pickup/Delivery (app/Services/OrderCapacity/OrderCapacityService.php:1844-1919). Two things still differ from Pickup/Delivery for eat-in slots:- It uses the location's regular business hours schedule, not the online-ordering pickup/delivery schedule.
- It ignores same-day/next-day cut-offs and merchant-defined blocked periods — those checks are skipped entirely for eat-in.
- Table QR Ordering / Kiosk dine-in: these are separate channels (
Table Qr Ordering,Kiosk) that never callOrderCapacityServiceat all — not for a time-slot preview, not for order submission. They remain fully exempt from capacity at every step.
Practical effect: a merchant can see eat-in time slots on the online storefront marked unavailable due to capacity limits, but capacity never actually blocks or delays a For Here order at submission — the enforcement gap is real and specific to getOrderCapacity()'s unconditional null for For Here.
Customer Impact
Online Ordering
The backend getAvailableTimeslots returns, per slot, an available boolean plus start/end times and prep-time-adjusted order-date ranges. How the storefront presents this (hiding full slots, exact messaging, etc.) is not verified here — that lives in the customer-facing app, outside the repos audited for this doc. Backend-confirmed behavior:
- Each slot is marked
available: true/falsebased on the effective limits. - For Pickup/Delivery, slots respect the online-ordering business-hours schedule and same-day / next-day cut-offs.
- For online
For Here(eat-in), the same capacity-basedavailable: true/falsefiltering applies, but slots are generated from the location's regular business hours (not the online-ordering pickup/delivery schedule) and are NOT subject to cut-offs or blocked periods — see Eat-In (For Here) and Capacity. Note that this preview filtering does not carry through to order submission: aFor Hereorder is never actually blocked by capacity when placed. - When capacity is disabled, all in-hours slots are returned as available (each carrying
capacity_disabled: true).
Kitchen / KDS
(Not verified here.) Slot grouping and KDS rendering are outside the scope of OrderCapacityService; the service writes sent_at, order_prep_time, and split_timeslots on the transaction, which downstream systems consume.
Relations
Depends On
- Business Hours: Capacity only applies within business hours
- Locations: Each location has its own capacity settings
- Menu Categories: Category limits reference menu categories
Affects
- Online Ordering: Determines available time slots for pickup/delivery
- Transactions: Orders tagged with time slot info
Related Features
Business Rules
- Order capacity submission checks only apply to the Online Ordering channel with Pickup or Delivery dining options. Any other channel (Kiosk, POS, Table QR Ordering, Uber Eats, Takeaway, Shopify) and the For Here dining option bypass capacity entirely at order-placement time —
getOrderCapacity()returnsnullfor them. - Exception: the customer-facing time-slot preview (
getAvailableTimeslots()) DOES apply capacity filtering to online eat-in (For Here) — a slot can show as unavailable due to capacity limits even though aFor Hereorder placed for that slot would never actually be rejected by capacity. See Eat-In (For Here) and Capacity. - When capacity management is disabled (
enabled=false), it is a hard bypass: stale configuration values are ignored, no capacity checks run, and all in-hours slots are returned as available. - All enabled limits (order count, item count, category-specific) are evaluated together; the order is blocked if any single limit is met or exceeded.
- Capacity is counted on the customer pickup slot (
order_date), and rush-hour / time-specific rules are matched on that same pickup slot. The kitchen slot (order_prep_time) still drives business hours, blocked periods, the closing cut-off andsent_at. - An abandoned unpaid cart holds its pickup slot for the reservation TTL (default 15 minutes) and then self-expires — there is no unpaid-order cleanup job.
- The default time-slot duration is 15 minutes; the orders/items limit values default to
null(no limit) and all the limit/flexible/category toggles default tofalse. - Server-side guardrails normalize invalid states before saving: rush protection toggled on without a valid threshold + allow-up-to is turned off, and the category toggle with an empty list is turned off (applied to both base settings and each time-specific rule).
- Slot capacity evaluation runs under database-level locks with retries to reduce race conditions during concurrent order placement; if a lock cannot be acquired after retries, the request aborts with a 503 ("processing many orders, try again in a moment").
- Two short-lived caches back the timeslot grid: per-slot stats (30-second TTL) and the available-timeslots result (60-second TTL). (Verified:
app/Services/OrderCapacity/OrderCapacityService.php:32-35setDefaultCacheDuration(30);:1789-1800remember(..., 60).) - Both caches are cleared for the whole location whenever Online Settings is saved, so a capacity change (limits, slot duration, rush protection, category limits, time-specific rules) takes effect on the storefront immediately rather than after the TTL. (Verified:
app/Services/BackOffice/OnlineSettingsService.php:106-113—clearAvailableTimeslotsCache()+clearAllTimeSlotStatsCacheForLocation().) - They are also cleared when a transaction takes a slot: eagerly as soon as an Unpaid transaction occupies one, and again for the affected slot(s) once the transaction is assigned. (Verified:
app/Services/OrderCapacity/OrderCapacityService.php:264-267and:454-490.) - Scheduling settings saved on the separate Online Ordering page (blocked times, custom availability, days-in-advance, last pickup slot) do not clear these caches, so those edits can take up to 60 seconds to appear. (Verified:
app/Services/BackOffice/OnlineOrderingService.php::update()lines 223-280 — no cache-clear call.)
FAQs
- Does order capacity affect kiosk or POS orders? No. Order-placement capacity checks only apply to Online Ordering Pickup/Delivery. Kiosk, POS, Table QR Ordering, and other channels — plus the online For Here dining option, at submission time — are never subject to capacity limits when the order is placed.
- Does capacity apply to eat-in / dine-in (
For Here) orders? It depends where you look. The customer-facing time-slot preview on the online storefront DOES apply the same capacity limits toFor Hereslots as Pickup/Delivery, so a merchant may see eat-in slots shown as full. However, actual order submission forFor Herealways bypasses capacity (getOrderCapacity()returnsnullfor it), so aFor Hereorder is never blocked or delayed by capacity once placed. Table QR Ordering and Kiosk dine-in are on separate channels and never touch capacity at all, at any step. - What happens when I disable capacity management? It is a hard bypass: old configured values are ignored, no checks run, and all in-hours slots are returned as available. Each returned slot carries an informational
capacity_disabled: trueflag (per-slot metadata, not a top-level response field — treat it as optional debugging info). - How does the system handle concurrent orders? Slot evaluation runs under DB locks with retries to keep two orders from claiming the last spot in a slot. If a lock cannot be acquired after retries, the order request aborts with a 503 and the customer is asked to try again shortly.
- Can a large order be split across multiple time slots? Yes. When an order's quantity cannot fit one slot, the service allocates as much as fits and continues into subsequent available in-hours slots (up to a safety cap; it will not split across different days once it has already started splitting). The order's primary
order_prep_timeis the first slot, andsplit_timeslotsrecords the per-slot quantities. - I changed my capacity limits — how long until customers see the new slots? Immediately. Saving Online Settings clears both the slot-stats cache and the available-timeslots cache for the location, so the next storefront request recalculates from the new limits. (Verified:
app/Services/BackOffice/OnlineSettingsService.php:106-113.) The one exception is the scheduling settings on the separate Online Ordering page (blocked times, custom availability, days-in-advance, last pickup slot) — those do not clear the cache and can take up to 60 seconds. - Why did my customer get sent back to the time picker when they pressed pay? Their chosen slot filled up (or their cart grew past a capacity limit) after they picked it. The storefront re-checks the slot right before payment and, rather than failing the order, reopens the time picker with the nearest still-available time pre-selected for them to confirm.
- What is the difference between rush-hour (time-specific) rules and rush protection? Time-specific rules override the base order/item limits for specific days and time ranges. Rush protection allows extra low-value orders within whatever limit is in effect. A time-specific rule's order cap always wins — rush protection's
allow_up_tois clamped to never exceed it.
Troubleshooting
Problem: Customers report no available time slots
Possible Causes:
- Order limits set too low for actual demand
- Time-specific rule reducing capacity during peak hours
- Business hours not configured correctly
- All slots filled with existing orders
Solutions:
- Check current order volume in Transactions
- Temporarily increase limits during testing
- Review time-specific rules for conflicts
- Verify business hours include expected ordering times
- Check if there's a backlog of pending orders
Problem: Kitchen is overwhelmed despite capacity limits
Possible Causes:
- Limits set too high
- Category limits not configured for slow-prep items
- Time slot duration too long
- Large orders not being split properly
Solutions:
- Reduce orders per slot limit
- Add category-specific limits for complex items (pizzas, grilled items)
- Use shorter time slots (e.g. 5 or 10 min instead of 15)
- Add rush-hour (time-specific) rules
- Review the location's Customer prep time and the channel delays (per-item prep times have no effect — see Menu Items)
Problem: Orders being rejected unexpectedly
Possible Causes:
- Multiple limits active and one is being hit
- Time-specific rule with stricter limits
- Category limit reached for specific item
- Pending orders counting toward capacity
Solutions:
- Check which limit is being hit in order logs
- Review all active limits and time-specific rules
- Consider if all limits are necessary
- Check for stuck pending orders
Assistant Guidance
When answering questions about order capacity:
- Order capacity lives on Online Settings (
/online-settings), configured viaOrderCapacityManagement.vue— there is no separate in-house/kiosk order-capacity page. - Kiosk, POS, Table QR Ordering, Uber Eats, Takeaway, and Shopify never call
OrderCapacityService— capacity has zero effect on those channels, at any step. Don't tell a merchant to "increase capacity" to fix a kiosk/POS ordering issue. - Be precise about
For Here(eat-in) — it behaves differently depending on what the merchant is asking about:- If asked "does capacity block eat-in orders?" → No, order submission for
For Herealways bypasses capacity (getOrderCapacity()returnsnullunconditionally for it). - If asked "why do eat-in time slots show as full/unavailable on the online site?" → capacity DOES filter the customer-facing time-slot preview for online
For Here, using the same limits as Pickup/Delivery (just without cut-offs/blocked periods, and using regular business hours instead of the online-ordering schedule). This is the one place eat-in and capacity actually interact. - Table QR Ordering / kiosk dine-in is a different channel from online
For Hereand is fully exempt from capacity everywhere — don't conflate the two when a merchant says "dine-in."
- If asked "does capacity block eat-in orders?" → No, order submission for
- The single number inputs for "Max orders per slot" and "Max items per slot" have no separate enable toggle in the UI —
0or blank means no limit; entering a positive number both sets the value and turns the underlying flag on. - Rush protection (
enable_flexible_limit_order_per_time_slot) only does anything on top of an order limit — it cannot be used standalone, and it can never exceed a time-specific rule's order cap (the cap always wins via clamping). - If a merchant asks to configure different limits per location, note that
order_capacitysettings are per-location (stored underlocation.online_settings.order_capacity), so this is already supported — just repeat the setup on each location.
Examples
Small Pizza Shop
json
{
"enabled": true,
"time_slot_duration_min": 15,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 4,
"limit_items_per_time_slot": false,
"enable_limit_specific_categories": true,
"limit_specific_categories": [
{
"category_id": "<category id>",
"category_name": "Pizzas",
"number_of_items_per_time_slot": 6
}
]
}Explanation: Max 4 orders per 15 minutes, but never more than 6 pizzas. This accounts for oven capacity (typically 2-3 pizzas at a time, ~5 min each). Note: capacity is only enforced when enabled is true, and a category entry must carry a valid category_id (the category_name is a display label added by the dialog).
Busy Lunch Restaurant
json
{
"enabled": true,
"time_slot_duration_min": 30,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 15,
"time_specific_rules": [
{
"name": "Lunch Rush",
"time_range": { "from": "12:00", "to": "13:30" },
"active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
"number_of_orders_per_time_slot": 8
}
]
}Explanation: Normal capacity is 15 orders/slot, but during the lunch-rush window it drops to 8 to maintain quality and prevent delays.
Coffee Shop with Flexible Ordering
json
{
"enabled": true,
"time_slot_duration_min": 15,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 10,
"enable_flexible_limit_order_per_time_slot": true,
"allow_up_to": 15,
"value_threshold_per_time_slot": 50
}Explanation: Normally max 10 orders per slot. But while the slot's total value stays at or under 50 (in your store's base currency), allow up to 15 orders. This maximizes throughput for quick drinks while protecting against too many complex food orders.
Common Merchant Questions
1. "Why are orders still coming in even though I set a limit?"
Order capacity, at order-placement time, only applies to the Online Ordering channel with pickup or delivery. It does not block any other channel (kiosk, POS, Table QR Ordering, Uber Eats, Takeaway, Shopify) or an online For Here (eat-in) order — those are never rejected by capacity when submitted, even though eat-in slots can still show as "full" on the storefront's time-slot picker (see next question).
2. "Why does the online ordering site show eat-in (For Here) time slots as full, but customers can still place them?"
The online storefront's time-slot preview applies the same order/item/category capacity limits to For Here (eat-in) as it does to Pickup/Delivery, so a slot can legitimately show as unavailable. However, the actual order-submission check (getOrderCapacity()) always bypasses capacity for For Here — it is a deliberate hard exemption in the backend. So the slot picker can be more restrictive than what's actually enforced when the order goes through.
3. "I turned order capacity off — are my old limits still active?"
No. When order capacity is disabled, all limits are fully bypassed. Old values are ignored and no capacity checks are applied.
4. "What's the difference between a time-specific rule and rush protection?"
- Time-specific rules set hard limits for specific days and times (e.g. Friday 18:00–20:00).
- Rush protection allows a few extra low-value orders within those limits when the kitchen can still handle them.
5. "Which one wins if they overlap?"
Time-specific rules always win. Rush protection can never exceed the limit set by a time-specific rule.
6. "If I leave a limit field empty, what happens?"
In the main orders/items fields, leaving the field blank (or entering 0) means "no limit" — the limit is simply off. Inside a rush-hour rule, a blank override means "use the base setting for this window" rather than zero. Limits are only enforced when a positive value is set.
7. "Why does rush protection use a value threshold instead of item count?"
Because value is a good proxy for kitchen workload. Many small, low-value orders are often faster to prepare than a few large, complex ones. The threshold is always evaluated in your store's currency.
8. "Does rush protection apply all the time?"
No. It only applies:
- When enabled
- When properly configured
- And only within the active order capacity limits
9. "Can rush protection increase my limits during busy hours?"
No. Rush protection can help during quieter moments, but it never increases limits during busy or restricted time windows.
10. "Why do I see time slots available even when capacity is off?"
When capacity is disabled, all available business hours are shown as open. This is expected behavior.
11. "What happens if I set limits that are too strict?"
Full slots are reported as unavailable, so customers have fewer (or no) bookable times. The backend also flags need_confirmation when the calculated dispatch time is pushed at least one slot duration later than requested. Exactly how the storefront surfaces this (e.g. prompting to pick a later time or confirm a delayed order) is not verified here.
12. "Is the value threshold per order or per time slot?"
It's per time slot, not per order. The total value of all orders in the slot is evaluated together.
13. "Does this affect POS / phone orders?"
No. Order capacity only affects online orders placed through your ordering website or app. Orders entered directly in the POS are not blocked.
14. "How are full time slots presented to customers?"
The backend returns each slot with an available: true/false flag rather than removing full slots from the response. Whether the storefront hides unavailable slots or shows them greyed-out is a front-end decision not verified here.
15. "What happens if my preparation time changes?"
Capacity is calculated using the location's Customer prep time (average_prep_time, Settings → Locations) plus the channel delay. If you increase it, orders may shift into earlier time slots for capacity counting. This can affect availability during busy periods. Changing a menu item's prep time does nothing here — item prep time is gated off in every environment (see Menu Items).
16. "Why does a small order get accepted when a larger one is rejected?"
Because rush protection allows extra low-value orders when the slot total is still under the threshold. Larger orders push the slot value over the threshold faster.
17. "Can I use rush protection without limiting orders per slot?"
No. Rush protection only works on top of an order limit. It adjusts how the limit behaves; it does not replace it.