Skip to content

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 and sent_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_to value 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 page src/pages/online-settings/index.vue; nav entry online-settings.menu-title)
  • Where it lives: Online Settings only. order_capacity is stored under the location's online settings (location.online_settings.order_capacity) and is validated solely by StoreOnlineSettingsRequest. 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 Here dining option when the order is actually placed. For these, getOrderCapacity() returns null and 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 online For Here (eat-in) orders — it runs the same order_capacity checks 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 call OrderCapacityService at 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 with mode="online-settings" from src/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 a mode prop ('settings' | 'online-settings'), but in practice it is only ever mounted with mode="online-settings" on the Online Settings page. There is no in-house order-capacity surface.

Fields

Capacity Enabled (Master Toggle)

PropertyValue
Field IDenabled
LabelEnable Order Capacity Management
TypeToggle (Boolean)
Defaultfalse
Backend Validation`nullable

Description: Master toggle to enable or disable all order capacity management.

Business Logic:

  • When enabled=false: Hard bypassgetOrderCapacity() returns null and all capacity checks are skipped.
  • The available-timeslots path returns every in-hours slot as available, each carrying a capacity_disabled: true flag (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

PropertyValue
Field IDtime_slot_duration_min
LabelTime Slot Duration
TypeSelect (UI)
Options (UI dropdown)5, 10, 15, 20, 30, 45, 60 minutes
Default15
RequiredYes
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_minrequired|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 maxOrdersPerSlot computed property. Entering 0 (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.

PropertyValue
Underlying flaglimit_orders_per_time_slot (boolean)
Value fieldnumber_of_orders_per_time_slot
UI fieldSingle number input (0 = no limit)
Flag defaultfalse
Value defaultnull
Backend Validationlimit_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_date falls inside the pickup window. Status-absent stub transactions are deliberately excluded as well as Unpaid ones (the filter is status: {$nin: [null, "Unpaid"]} — a plain != "Unpaid" would wrongly count stubs that carry an order_date but no status).
  • Only when the incoming order is itself unpaid does the count also include other unpaid orders whose order_date falls 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_timeslots is a throughput concept only; it no longer subdivides the per-slot count.
  • A new order is blocked when current count >= limit (or >= allow_up_to when 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.).

PropertyValue
Field IDenable_flexible_limit_order_per_time_slot
LabelProtect rush capacity
TypeToggle (Boolean)
Defaultfalse
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 (5 for orders, 45 for 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_to as the order limit; otherwise use the standard number_of_orders_per_time_slot limit. (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_to is 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 > 0 and value_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

PropertyValue
Field IDallow_up_to
LabelAllow up to X extra orders
TypeNumber
Defaultnull (empty with placeholder "5")
Backend Validation`nullable
Depends Onenable_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 → effective allow_up_to=2 during that window

Rush Protection: Value Threshold

PropertyValue
Field IDvalue_threshold_per_time_slot
LabelSlot total threshold
TypeCurrency (number)
Defaultnull (empty with placeholder "45")
Backend Validation`nullable
Depends Onenable_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_to limit; if total > threshold → use number_of_orders_per_time_slot limit. (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.

PropertyValue
Underlying flaglimit_items_per_time_slot (boolean)
Value fieldnumber_of_items_per_time_slot
UI fieldSingle number input (0 = no limit)
Flag defaultfalse
Value defaultnull
Backend Validationlimit_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

PropertyValue
Field IDenable_limit_specific_categories
LabelSet limits for specific item categories
TypeToggle (Boolean)
Defaultfalse
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

PropertyValue
Field IDlimit_specific_categories
LabelCategory limits
TypeArray of objects
Backend Validation`nullable
Depends Onenable_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

PropertyValue
Field IDtime_specific_rules
LabelRush-hour rules
TypeArray 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.*):

  • namerequired|string|distinct
  • time_rangerequired|array; time_range.from and time_range.to are each required|string
  • active_daysrequired|array; each value must be one of the seven day names (see Days of Week below)
  • number_of_orders_per_time_slotnullable|numeric|gt:0
  • number_of_items_per_time_slotnullable|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_slot set (numeric > 0) → overrides base and forces the orders limit on; otherwise inherits base.
    • number_of_items_per_time_slot set (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_to is clamped down so it can never exceed that cap.
  • Example: base allow_up_to=5, rule cap=2 → effective allow_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:

  1. Check if order count limit would be exceeded
  2. Check if total items limit would be exceeded
  3. Check if any category-specific limit would be exceeded
  4. If time-specific rule applies, use those limits instead of defaults
  5. 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:

  1. Take the order's requested pickup/delivery time
  2. Subtract preparation time to get order_prep_time
  3. Round order_prep_time down to nearest slot boundary
  4. 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:

  1. Calculate how many items can fit in current slot
  2. If not all items fit, check next available slot
  3. Continue until all items are allocated
  4. 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; returns calculated_order_date and a need_confirmation flag (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, or null when the channel is not Online Ordering, the dining option is For Here, or enabled is 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 writes sent_at / order_prep_time (and split_timeslots if split). Guarded against re-running once sent_at is set.
  • getSentAt() (public) — computes prep time, the slot boundary, and sent_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 with transactionChannel = Online Ordering (OnlineOrderingOrchestrator::getAvailableTimeslots()), for Pickup, Delivery, and For Here (online eat-in). (Verified: app/Services/OrderCapacity/OrderCapacityService.php:1789-1800.)
  • calculateAvailableTimeslots() (private) — the implementation behind getAvailableTimeslots(). For For Here it sets $isEatIn = true and still runs the same order_capacity-based batchCheckTimeslotAvailability() 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 by checkOrderCapacity() and payment capture): still a hard bypass. When diningOption === 'For Here', getOrderCapacity() returns null unconditionally, 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 same order_capacity batch check for For Here as it does for Pickup/Delivery — a slot can be returned as available: false because 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 call OrderCapacityService at 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/false based 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-based available: true/false filtering 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: a For Here order 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

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() returns null for 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 a For Here order 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 and sent_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 to false.
  • 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-35 setDefaultCacheDuration(30); :1789-1800 remember(..., 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-113clearAvailableTimeslotsCache() + 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-267 and :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 to For Here slots as Pickup/Delivery, so a merchant may see eat-in slots shown as full. However, actual order submission for For Here always bypasses capacity (getOrderCapacity() returns null for it), so a For Here order 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: true flag (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_time is the first slot, and split_timeslots records 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_to is 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:

  1. Check current order volume in Transactions
  2. Temporarily increase limits during testing
  3. Review time-specific rules for conflicts
  4. Verify business hours include expected ordering times
  5. 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:

  1. Reduce orders per slot limit
  2. Add category-specific limits for complex items (pizzas, grilled items)
  3. Use shorter time slots (e.g. 5 or 10 min instead of 15)
  4. Add rush-hour (time-specific) rules
  5. 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:

  1. Check which limit is being hit in order logs
  2. Review all active limits and time-specific rules
  3. Consider if all limits are necessary
  4. Check for stuck pending orders

Assistant Guidance

When answering questions about order capacity:

  • Order capacity lives on Online Settings (/online-settings), configured via OrderCapacityManagement.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 Here always bypasses capacity (getOrderCapacity() returns null unconditionally 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 Here and is fully exempt from capacity everywhere — don't conflate the two when a merchant says "dine-in."
  • The single number inputs for "Max orders per slot" and "Max items per slot" have no separate enable toggle in the UI — 0 or 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_capacity settings are per-location (stored under location.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.