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 (5–60 minutes) on one wall-clock grid per capacity block — local midnight + k ×
time_slot_duration_min. 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 collection slot (order_date): the kitchen slot plus preparation time, rounded up onto the same grid, so every collection time is itself a grid cell (kitchen 07:45 + 10 min prep is ready at 07:55 and offered as 08:00). Rush-hour/time-specific rules and per-slot overrides are matched on that collection slot too, so the time a merchant configures is the time the customer picks. - Capacity split (
order_capacity_split, off by default): one switch — Set capacity separately for pickup and delivery — that gives pickup and delivery each a complete capacity block of their own (order_capacity_by_option.pickup/.delivery: master toggle, slot duration, limits, rush protection, category limits, rush-hour rules and per-slot grid), each counting only its own orders. Off, the singleorder_capacityblock governs pickup, delivery and online eat-in together, exactly as before. On,order_capacitykeeps governing online eat-in only. - Per-slot capacity grid (
slot_overrides): a grid of the block's actual slots per weekday where a merchant can give one slot its own order cap or close it for online orders. A slot with no override inherits the defaults above the grid. Grid edits are orders only — items, value ceiling, category limits and rush protection keep coming from the defaults and rush-hour rules. - 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.
- Max order value per slot: A hard euro ceiling on the total value of all orders collected in one pickup slot, for kitchens whose load tracks basket value rather than order or item count (six €8 sandwiches and one €400 catering order look the same to the order cap, but not to the kitchen). Not the same thing as rush protection's value threshold, which runs the other way — it admits extra orders while a slot is still cheap.
- 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.
- Rush Hour Rules (on screen) / time-specific rules (field name): 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. The section header says Rush Hour Rules; the dialog it opens is titled Add Time-Specific Rule. Always give a merchant the section name.
- 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. A per-slot override goes one step further: it zeroesallow_up_tofor that slot, so the number typed in the grid is the slot's hard cap.
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, Item and Value Limits
Set the maximum number of orders, total items and/or total order value allowed per time slot. All configured 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 Rush Hour Rules
Expand Rush Hour Rules and click + Add New Rule to 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.
Set Capacity Separately for Pickup and Delivery
Turn on Set capacity separately for pickup and delivery at the top of the card. Tabs Pickup, Delivery (and Eat in, only when online eat-in is enabled on the Online Ordering page) appear; each tab is a full capacity block of its own. The first time the switch is turned on, pickup and delivery are seeded as copies of the current shared settings, so nothing changes until the merchant edits a tab. Turning it off again goes back to the single shared block; the per-option blocks are kept on the location but ignored.
Adjust or Close Individual Time Slots
Under Per-slot capacity, pick a day, then click a slot (or drag across several) and either type a per-slot max orders value, Close it for online orders, or Reset to default. Bulk actions can set every slot of the day, copy the day to other days, or clear the day. Changing the Time Slot Duration locks the grid with a notice until the settings are saved, because the grid is drawn from the saved duration.
Location
- Backoffice Route:
/online-settings(file-based pagesrc/pages/online-settings/index.vue; nav entryonline-settings.menu-title) - Where it lives: Online Settings only. Everything is stored under the location's online settings and validated solely by
StoreOnlineSettingsRequest:location.online_settings.order_capacity— the shared block (governs everything while split is off; online eat-in only while split is on).location.online_settings.order_capacity_split— boolean, defaultfalse.location.online_settings.order_capacity_by_option.pickup/.delivery— the two per-option blocks, same shape asorder_capacity(defaults: disabled blocks).- Each block carries its own
slot_overridesgrid. 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. Those orders also do not count toward a slot's occupancy any more — only Online Ordering orders do (see Time Slot Calculation). - Exception: the customer-facing available-timeslots preview (
getAvailableTimeslots()/POST /available-timeslots) DOES apply capacity filtering to onlineFor Here(eat-in) orders — it runs the same checks as Pickup/Delivery against the shared block, 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(resolveCapacityBlock()/ staticcapacityBlockIn()pick the block for a dining option;resolveSlotOverride()reads the grid). - Grid endpoint:
GET /api/back-office/online-settings/{locationId}/capacity-grid?day=<Monday…Sunday>&channel=<pickup|delivery|shared>(Api\BackOffice\OnlineSettingsController::capacityGrid→OnlineSettingsService::capacityGrid()), which draws the merchant's grid with the engine's own slot math. - Vue Components:
src/views/in-house/components/forms/OrderCapacityManagement.vueis the shell (split switch, channel tabs, which block is being edited);src/views/in-house/components/forms/OrderCapacityBlockForm.vuerenders ONE block (master toggle, duration, limits, Advanced Options, Rush Hour Rules, the per-slot gridCapacitySlotGrid.vue). Mounted 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 components physically live under
views/in-house/...for historical reasons, but they are only mounted on the Online Settings page (the shell's only prop isreadonly; the oldmodeprop is gone). There is no in-house order-capacity surface.
UI Labels (exact on-screen wording)
Emily and any merchant-facing answer must use these strings. They are the only wording a merchant can actually see; the field names (time_specific_rules, enable_flexible_limit_order_per_time_slot, …) appear nowhere in the interface.
Verified against upvendo-backoffice origin/production: src/plugins/i18n/locales/modules/{en,nl}/online-settings.ts → settings.form.sections.order_capacity_management.*, rendered by src/views/in-house/components/forms/OrderCapacityManagement.vue and src/views/in-house/components/settings/TimeSpecificRuleFormDialog.vue.
| Where | English (i18n key) | Dutch |
|---|---|---|
| Page (nav) | Settings, under Online Channels — page header Online Settings (nav-title) | Instellingen / Instellingen voor Online Bestellingen |
| Section title + master toggle | Order Capacity Management (title) — sub-label "Limit how many orders your kitchen can handle per time slot" (master_toggle.description) | Order Capaciteit Beheer |
| Slot length | Time Slot Duration (time_slot_settings.label) | Tijdsslot Duur |
| Order limit | Max orders per slot (simplified.max_orders_label), hint 0 = no limit | Max bestellingen per slot |
| Item limit | Max items per slot (simplified.max_items_label), hint 0 = no limit | Max artikelen per slot |
| Value limit | Max order value per slot (simplified.max_value_label), hint 0 = no limit | Max orderwaarde per slot |
| Collapsible #1 | Advanced Options (simplified.advanced_options) | Geavanceerde Opties |
| — inside Advanced Options | Protect rush capacity (capacity_limits.flexible_ordering.title) | Bescherm spitscapaciteit |
| — inside Advanced Options | Set limits for specific item categories (capacity_limits.set_limits_specific_item.title) → Add Category Limit | Stel limieten in voor specifieke artikelcategorieën |
| Collapsible #2 | Rush Hour Rules (simplified.rush_hour_rules) | Spitsuur Regels |
| — inside Rush Hour Rules | + Add New Rule (time_specific_rules.add_new_rule) | Nieuwe Regel Toevoegen |
| — rule dialog title | Add Time-Specific Rule / Edit Time-Specific Rule (time_specific_rules.form.title_add / title_edit) | Tijdsspecifieke Regel Toevoegen |
| — rule dialog fields | Rule Name, Time Range (Start Time / End Time), Active Days, Capacity Overrides, Override max orders per slot, Override max items per slot, Override max order value per slot, hint Leave empty to use default | Regelnaam, Tijdsbereik, Actieve Dagen, … |
| Split switch (top of the card) | Set capacity separately for pickup and delivery (capacity_split.label) — hint "Each option gets its own limits, rules and time slots. Orders for one do not use up the other's capacity." | Stel de capaciteit apart in voor afhaling en bezorging |
| Channel tabs (only while split is on) | Pickup / Delivery / Eat in (capacity_split.tab_pickup / tab_delivery / tab_eat_in; Eat in only when online eat-in is enabled) — Eat in tab shows "These settings apply to online eat-in orders only." | Afhaling / Bezorging / Ter plaatse eten |
| Master toggle sub-label while split is on | "Limit how many pickup orders your kitchen can handle per time slot" (master_toggle.description_for, option name filled in) | Beperk hoeveel bestellingen voor afhaling je keuken per tijdsslot aankan |
| Per-slot grid section | Per-slot capacity (slot_grid.title) — "Adjust or close individual time slots. These are the times customers pick, and closing one only removes it from online ordering — POS and kiosk orders are unaffected. Slots without their own value use the defaults above." Link Edit hours | Capaciteit per tijdslot / Openingsuren bewerken |
| — grid cell states | Closed for online orders, Outside hours, No limit; legend Uses the default / Override | Gesloten voor online bestellingen, Buiten openingsuren, Geen limiet; Gebruikt de standaard / Eigen waarde |
| — grid actions | Set to, Close / Reopen, Reset to default; Bulk actions → Set all slots to, Copy this day to, Clear this day | Zet op, Sluiten / Heropenen, Terug naar standaard; Bulkacties → Zet alle sloten op, Kopieer deze dag naar, Deze dag wissen |
| — grid locked notice | "This grid reflects your last saved settings (15-minute slots). Save your changes to redraw it on 10-minute slots before adjusting individual slots." (slot_grid.stale_notice) | Dit rooster toont uw laatst opgeslagen instellingen … |
| — duration change warning | "Changing the slot duration will clear N slot override(s) that no longer fit the new grid." with Apply / Cancel (slot_grid.duration_prune_warning) | Het wijzigen van de tijdslotduur verwijdert … |
| — save blocked by an incomplete tab | "Order capacity for Delivery is incomplete — set its time slot duration before saving." (capacity_split.invalid_tab; the form switches to that tab) | De bestelcapaciteit voor … is onvolledig |
Navigation path to give a merchant
Online Settings → Order Capacity Management → Rush Hour Rules → + Add New Rule
With split on, add the tab: Online Settings → Order Capacity Management → Pickup (or Delivery) tab → …. For a single time slot: … → Per-slot capacity → pick the day → click the slot → Set to / Close.
Known naming inconsistencies — do not repeat them
- "Time-specific rules" is not a visible label anywhere. It is the field name (
time_specific_rules) and the dialog title ("Add Time-Specific Rule"). The section a merchant clicks is Rush Hour Rules. Leading with "time-specific rules" sends merchants hunting for a section that does not exist. - Rush Hour Rules is NOT inside Advanced Options. They are two sibling collapsibles separated by a divider (
OrderCapacityManagement.vue— Advanced Options block,VDivider, then the Rush Hour Rules block). - "Category limits" is not a visible label either. The toggle reads Set limits for specific item categories; the table columns are Category / Max Items Per Time Slot; the button is Add Category Limit.
- The older headings Time Slot Settings / Capacity Limits / Time-Specific Rules still exist as i18n keys but are not rendered by the current simplified form — do not quote them to a merchant.
- "Split" is not a visible word. The switch reads Set capacity separately for pickup and delivery; the tabs read Pickup / Delivery / Eat in. There is no "shared" tab — the shared block is simply what the card shows while the switch is off, and the Eat in tab while it is on.
- "Slot overrides" is the field name (
slot_overrides). On screen the section is Per-slot capacity and a customised cell is an Override in the legend.
Fields
Capacity Enabled (Master Toggle)
| Property | Value |
|---|---|
| Field ID | enabled |
| Label | Order Capacity Management (section title + master toggle; sub-label "Limit how many orders your kitchen can handle per time slot") |
| 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 of the capacity rules —getOrderCapacity()returnsnulland every capacity check (order counts, item counts, category caps, rush protection, per-slot overrides, the value ceiling) is skipped. - The available-timeslots path returns in-hours slots as available, each carrying a
capacity_disabled: trueflag (per-slot informational metadata, not a top-level response field). - It is not a bypass of the closing cut-off or of blocked periods. Both are properties of the location, not of order capacity, so they still apply on this path. A slot whose customer pickup time would land after the cut-off is returned
available: false, and blocked windows are still marked unavailable afterwards for Pickup/Delivery. (The closing guard only bites when a customer prep time is configured — with a prep time of0the pickup is the kitchen slot itself and nothing is withheld.) - Stale configuration values are ignored, preventing accidental enforcement.
Customer Impact:
- Online Ordering: Every in-hours slot a customer can actually be served in is bookable; a slot whose pickup would fall past the closing cut-off, or inside a blocked window, is not offered. (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, 60 minutes (a block already saved with another value, e.g. the retired 45, keeps showing that value until it is changed) |
| 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/60 list is enforced by the UI dropdown only (timeSlotDurationOptions in OrderCapacityBlockForm.vue). 45 was dropped because it does not tile the hour on a midnight-anchored grid (00:45, 01:30, 02:15 … — a 07:00 opening would get 07:30 as its first slot). 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.
Per block: with split on, pickup and delivery each have their own duration (a 15-minute pickup grid next to a 30-minute delivery grid is valid). The storefront receives them as timeslot_intervals ({Pickup, Delivery, 'For Here'}) on the public ordering constant, next to the legacy scalar timeslot_interval which always carries the shared block's value; the picker uses the interval of the dining option the customer selected.
Changing it after slots were customised: the grid is drawn from the saved duration. Picking a new duration (a) shows a warning with the number of per-slot overrides that no longer land on the new grid, which are cleared when the merchant presses Apply, and (b) locks the grid and bulk menu with the notice "This grid reflects your last saved settings…" until the settings are saved and the grid is redrawn. Flow: change duration → Save → adjust slots → Save.
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 (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 | Inline sentence: "Accept up to [n] extra orders, as long as slot total stays under [amount]" — this field is the [n] |
| 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
Not to be confused with Max order value per slot (
max_value_per_time_slot), which is a hard ceiling on a slot's total value. This threshold does the opposite: it admits extra orders while the slot is still cheap. See Maximum Order Value Per Time Slot. | Property | Value | |----------|-------| | Field ID |value_threshold_per_time_slot| | Label | Inline sentence: "Accept up to [n] extra orders, as long as slot total stays under [amount]" — this field is the [amount] | | Type | Currency (number) | | Default |null(empty with placeholder "45") | | Backend Validation |nullable|numeric|gt:0by default; becomesrequired|numeric|gt:0when rush protection is enabled | | Depends On |enable_flexible_limit_order_per_time_slotmust 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.
- An order is never spread across slots to get around this limit: its full quantity counts in its one collection slot. A slot the order would push over the limit is skipped, and the whole order is offered the next slot that can take it. This holds whether or not Set capacity separately for pickup and delivery is on — that switch only chooses which block's limit applies.
- An order whose own quantity is above every item limit the location can apply (the base limit, and no rush-hour rule raises it) fits no slot at all. It is rejected before payment ("This order has more than the maximum of 10 items per time slot. Please place a smaller order, or contact the restaurant for large orders."), translated into the customer's language.
Customer Impact:
- Online Ordering: the picker hides every slot the cart would overflow. A slot that fills while the customer is at checkout moves the whole order to the next slot that can take it, and the customer is asked to confirm the new time. An order bigger than the item limit cannot be placed online; the customer has to split it into smaller orders themselves or contact the restaurant.
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)
Maximum Order Value Per Time Slot
UI note: Same pattern as orders and items — a single number field ("Max order value per slot") backed by
maxValuePerSlot;0/blank means "no value limit", a positive amount (in the location's currency) turns it on and sets the boolean flag and value together.
| Property | Value |
|---|---|
| Underlying flag | limit_value_per_time_slot (boolean) |
| Value field | max_value_per_time_slot (decimal currency amount, e.g. 200 = €200.00) |
| UI field | Single number input (0 = no limit) |
| Flag default | false |
| Value default | null |
| Backend Validation | limit_value_per_time_slot: `nullable |
Description: A hard ceiling on the total value of all orders collected in one pickup slot. It is the lever for a kitchen whose load tracks basket value rather than order or item count.
What counts as "value": the order's grand total as stored on the transaction — items net of discounts, plus tax, tip and the delivery / pickup transaction fee. A €20 tip or a delivery fee therefore counts against the slot's ceiling, exactly as it does for rush protection's value threshold. All comparisons are made in whole cents.
Business Logic:
- A slot whose booked value is already at or over the ceiling is full — even a fully discounted €0 order is turned away from it.
- An order is rejected for a slot when the slot's existing value plus the order would exceed the ceiling. There is no partial fill: the customer is offered the next slot, exactly as the item limit behaves.
- An order that exactly fills the ceiling is accepted (€150 booked + €50 order into a €200 cap); only the next order is turned away.
- The storefront picker, order placement, and the reschedule/split search all apply the same check, so a slot the picker offers is one checkout will accept, and a slot placement turns the order away from is never re-used by the fallback search.
- An order worth more than every ceiling the location can apply (the base cap, and no rush-hour rule raises it) fits no slot at all. It is rejected before payment with a clear error ("This order exceeds the maximum order value of 200.00 per time slot."), rather than being paid for and then failing to find a slot.
- Rush-hour rules can override the ceiling per window (Override max order value per slot); an empty override inherits the base ceiling, and a rule with no base ceiling behind it leaves slots outside its window uncapped.
Customer Impact:
- Online Ordering: slots whose remaining value headroom is smaller than the customer's cart read as unavailable in the picker; a cart larger than the ceiling itself cannot be checked out at all and shows the error above.
Examples:
- Ceiling €200: Order A (€120) + Order B (€60) = €180 booked. Order C (€50) is offered the next slot (180 + 50 > 200); Order D (€20) still fits (exactly €200).
- Ceiling €200 with a €450 catering order: no slot can ever take it — the customer sees the "exceeds the maximum order value" error at checkout.
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 | Table under Set limits for specific item categories; columns Category and Max Items Per Time Slot; button Add Category Limit |
| 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)
Rush Hour Rules (stored as time_specific_rules)
| Property | Value |
|---|---|
| Field ID | time_specific_rules |
| Label | Rush Hour Rules (section) → + Add New Rule → dialog Add Time-Specific Rule / Edit Time-Specific Rule |
| 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",
"max_value_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:0max_value_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 three limit overrides (orders, items, order value). 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/value limits during specific day-and-time windows. Useful for managing rush hours.
UI Behavior:
- The dialog only edits order/item/value limit overrides (simplified UI). The + Add New Rule button (and the category-limit switch) stay disabled until at least one basic limit — orders, items or order value — is set.
- "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 the half-open window
from <= time < to(a slot starting exactly at a rule's end time belongs to the next window, matching how overlap validation reads ranges). 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.max_value_per_time_slotset (numeric > 0) → overrides base and forces the value ceiling 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
Capacity Split
| Property | Value |
|---|---|
| Field ID | order_capacity_split (top-level online setting, sibling of order_capacity) |
| Label | Set capacity separately for pickup and delivery |
| Type | Toggle (Boolean) |
| Default | false |
| Backend Validation | `nullable |
Description: Off (default): the single order_capacity block governs pickup, delivery and online eat-in, and all of those orders share one counting pool per slot. On: pickup orders are governed and counted by order_capacity_by_option.pickup, delivery orders by order_capacity_by_option.delivery, and order_capacity governs and counts online eat-in only.
Business Logic:
- Turning the switch on seeds a per-option block that has never been configured with a deep copy of the current shared block (client-side in
OrderCapacityManagement.vue; the server repeats the same rule on save from the stored document inOnlineSettingsService::seedCapacityBlocks(): sent block → previously authored stored block → copy of shared). So the first save after splitting changes nothing about what customers can book. - A block that was already authored is never overwritten by the seed, so a merchant can split, un-split and split again without losing per-option settings.
- With split on and a per-option block missing from the stored document (hand-edited data, a save that raced the toggle), the engine falls back to the shared block — the safe direction, never "no capacity".
- Blocked times on the Online Ordering page stay ONE list that applies to pickup and delivery alike; the split does not duplicate them.
- "Split" means splitting the capacity between pickup and delivery. It does not split a large order across several time slots, and it does not let an order above a slot's item or value limit through: those orders are still rejected before payment, against the limit of the block that governs them.
Customer Impact:
- Online Ordering: pickup and delivery each get their own slot list (own duration, own caps, own closures), and a full pickup slot no longer makes the same delivery time unavailable, or vice versa.
Per-Option Capacity Blocks
| Property | Value |
|---|---|
| Field ID | order_capacity_by_option.pickup, order_capacity_by_option.delivery |
| Label | The Pickup and Delivery tabs |
| Type | Object, same shape and same field rules as order_capacity (see every field section above), each with its own slot_overrides |
| Default | Two disabled blocks (enabled: false, 15-minute duration, no limits) |
| Backend Validation | Container order_capacity_by_option and each channel key: `nullable |
Description: A per-option block is a block, not a patch: it is normalised against the block defaults, not against the shared block, and nothing in it is inherited from order_capacity.
Per-Slot Capacity Grid (stored as slot_overrides)
| Property | Value |
|---|---|
| Field ID | slot_overrides (inside each block: order_capacity.slot_overrides, order_capacity_by_option.pickup.slot_overrides, …) |
| Label | Per-slot capacity section; cells Override / Closed for online orders |
| Type | Object: { "<Day>": { "HH:MM": { "orders": <int ≥ 0> } } } |
| Default | {} |
| Backend Validation | `bail |
Description: One entry per customised slot of a weekday, keyed by the slot's collection start time exactly as the grid shows it. orders: n caps that slot at n online orders; orders: 0 closes the slot for online ordering. Slots not present inherit the block's defaults and rush-hour rules.
Business Logic:
- Resolution order for a slot is override → matching rush-hour rule → base defaults (
applySlotOverrideToRule()runs afterretrieveTimeSpecificRule()). An override replacesnumber_of_orders_per_time_slotfor that slot, forces the order limit on, and setsallow_up_toto 0, so rush protection cannot lift a slot above the number the merchant typed. - Overrides are orders only. The item cap, the value ceiling, category limits and rush protection keep resolving from base + rush-hour rule for that slot.
- A closed slot (
orders: 0) is a hard "customers cannot submit an online order for this time": it readsavailable: falsein the storefront preview and is refused at placement. POS and kiosk are unaffected (they have no capacity settings and are never gated). - Overrides are matched on the slot the customer picks (collection time), the same key the picker shows. Closing one cell always removes exactly one bookable time for every cart.
- The reschedule/split search for an already-paid order deliberately ignores overrides so a paid order can never end in "no slot found". (The search before payment does apply them — a closed slot is never proposed at checkout.)
- Orphans are kept, never pruned on save. A stored key that no longer lands on the day's grid (duration or hours changed) is inert for the engine; the grid endpoint returns it with
in_hours: falseand the grid draws it dashed so the merchant can see and clear it. The only pruning is the explicit, confirmed one in the duration-change warning. - In the grid, typing
0in a cell deletes the override (defaults row semantics: 0 = no limit); closing is a dedicated Close action that stores{orders: 0}.
Grid endpoint contract (GET …/capacity-grid?day=&channel=): { slots: [{ slot, label, orders, source, closed, in_hours }], reference_prep_minutes: {…}, time_slot_duration_min }. slot is the storage key the UI writes back and label the customer-facing time (they are equal now that collection times are grid-aligned); source is base / rule / override so an inherited value renders as a placeholder rather than a stored one; time_slot_duration_min is the duration the grid was drawn on (30 when the master toggle is off), which the backoffice compares to the unsaved form value to lock a stale grid. The grid is drawn for the location's reference prep (average prep + the option's delay); a slower cart shifts which kitchen slot lands in a cell, never how many bookable times a cell holds.
Examples:
- Close Friday 18:30 and 18:45 for delivery while leaving pickup open: split on → Delivery tab → Per-slot capacity → Friday → select 18:30 and 18:45 → Close.
- Allow only 1 online order in the 12:00 slot on weekdays but keep the default 4 elsewhere: select 12:00 on each weekday → Set to 1 (or set Monday and Copy this day to the others).
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:
- Pick the capacity block for the order's dining option (shared block, or the pickup/delivery block when split is on)
- Resolve the slot's effective rule: base defaults → matching rush-hour rule → per-slot override (orders only)
- Check if the order count limit would be exceeded (a closed slot,
orders: 0, always fails here) - Check if the total items limit would be exceeded
- Check if the per-slot order value ceiling would be exceeded
- Check if any category-specific limit would be exceeded
- If flexible ordering is enabled and the slot value is under the threshold, use the flexible limit — never above a rush-hour cap, and never on a slot with an override
Time Slot Calculation
One slot grid per capacity block. Slots sit on the wall clock — local midnight + k × time_slot_duration_min (00:00, 00:30, 01:00, … for 30-minute slots) — and that grid is shared by the location's business hours, the online-ordering custom-availability schedule, the QR schedule and every range of a day. A schedule only decides which of those slots are offered (its open/close window); it never moves the grid. With split off there is one grid for every dining option; with split on, pickup and delivery each draw their own grid from their own block's duration, and online eat-in stays on the shared block's grid.
Consequences:
- A range's first slot is its opening time rounded up onto the grid. Opening at 11:50 with 30-minute slots offers 12:00 first; the 11:50–12:00 lead-in is not orderable. Openings that are a multiple of the slot duration (the usual :00/:15/:30 cases) are unaffected.
- Eat-in (business hours) and pickup/delivery (online-ordering schedule) offer the same slots even when their opening times differ, so their capacity buckets coincide while split is off, and one order is counted in exactly one bucket rather than against overlapping windows on two grids.
- What counts as occupancy: only orders placed through the Online Ordering channel (settled orders, plus in-checkout unpaid orders for the reservation window). Kiosk, POS and other channels never consumed online capacity as gating and, as of this release, no longer count as load in a slot either. With split off, pickup, delivery and online eat-in orders share one pool per slot; with split on, the pickup block counts Pickup orders only, the delivery block Delivery orders only, and the shared block counts online eat-in (
For Here) only. - Collection time is a grid cell. The customer's collection time is kitchen slot + preparation time rounded up onto the grid (
pickupSlotStart()), so a 10-minute prep on a 15-minute grid puts the 07:45 kitchen slot at 08:00 for the customer. The merchant's grid, the storefront picker and the per-slot override key are the same set of times. The price is that a customer may wait up to one slot duration longer than the raw ready time. - Order placement (round down), the reschedule/split search (round up) and the listing all snap onto the same grid, and the storefront picker builds its times on it too (per dining option, via
timeslot_intervals), so a slot the picker offers is one checkout will accept.
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 the nearest wall-clock slot boundary; a time inside a range's opening lead-in takes the range's first slot — this is the kitchen slot - Add preparation time back and round up onto the grid — this is the collection slot, where the order's capacity is counted and where rush-hour rules and per-slot overrides are matched
Example:
- Customer picks 12:45 from the picker (15-minute slots, 20-minute prep)
order_prep_time= 12:25 → kitchen slot 12:15–12:30- Kitchen 12:15 + 20 min = 12:35 → rounded up → collection slot 12:45, which is the time the customer picked; the order counts toward that 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 the customer's own slot cannot take the order, a forward search looks for another. It behaves differently before and after payment:
- Before payment (checkout): every candidate slot is judged by the same check as a slot the customer picks (item, value and category limits, and per-slot closures), and it takes the whole order or nothing. The order is offered the first slot that passes, and the customer confirms the new time. An order is never spread across slots at checkout. An order above every item limit or every value ceiling is rejected before the search runs; if no slot passes within 7 days, checkout is refused with "No available time slot could be found in the next 7 days. Please choose a different time."
- After payment (capture): a paid order must always land, so the search uses a lenient kitchen budget. It may spread the order's items over consecutive kitchen slots (written to
split_timeslots), and it ignores per-slot closures.
Either way, capacity is counted once, in the order's collection slot, with its full quantity and value. split_timeslots is kitchen-throughput information only; nothing reads it for capacity.
No setting turns splitting on or off. In particular, order_capacity_split (Set capacity separately for pickup and delivery) only chooses which capacity block governs an order — see Capacity Split.
Backend Implementation
Key methods in OrderCapacityService.php:
checkOrderCapacity()(public) — runs the capacity flow for a transaction; returnscalculated_order_dateand aneed_confirmationflag (set whenever the calculated pickup time differs from the requested one, by any amount). Before searching it rejects, with a 422, an order worth more than every order-value ceiling the location can apply (highestValueCap()), and an order with more items than every item limit the location can apply (highestItemCap()).getOrderCapacity()(public) — used by order submission / payment capture. Resolves the capacity block for the order's dining option viaresolveCapacityBlock()and returns it, ornullwhen the channel is not Online Ordering, the dining option isFor Here, or the block'senabledis 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.resolveCapacityBlock()/ staticcapacityBlockIn(array $onlineSettings, string $diningOption)— the single place that decides which block governs:order_capacitywhen split is off or the option is not Pickup/Delivery (capacityChannelFor()maps Delivery→delivery, Pickup→pickup, everything else→shared);order_capacity_by_option.<channel>when split is on, falling back to the shared block if that block is missing. A non-array shared block (corrupt document) is coerced tonull.resolveSlotOverride()(public) /applySlotOverrideToRule()— readslot_overrides[day]["HH:MM"]for the collection slot and stamp it onto the already-resolved rule (orders only,allow_up_tozeroed).pickupSlotStart()(public) — kitchen slot + prep, rounded up onto the grid; shared by the engine and the grid endpoint so both speak the same times.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).calculateAvailableTimeslots()(private) — the implementation behindgetAvailableTimeslots(). It resolves the block for the requested dining option (so pickup and delivery can come back on different grids under split). ForFor Hereit sets$isEatIn = trueand still runs the samebatchCheckTimeslotAvailability()as Pickup/Delivery against the shared block, but uses the location's business-hours schedule (not the online-ordering pickup/delivery schedule) and skips cut-off and blocked-period checks. Both the listing and the placement check build their occupancy filter from the sameoccupancyPoolClauses()(Online Ordering only; plus the block's own dining-option pool under split).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). The merchant-facing grid is OnlineSettingsService::capacityGrid() (see Per-Slot Capacity Grid).
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 or the per-slot order-value ceiling 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.
The picker generates its candidate times on the grid of the selected dining option: the public ordering constant carries timeslot_intervals ({ "Pickup": 15, "Delivery": 30, "For Here": 15 }, each block's saved duration) next to the legacy scalar timeslot_interval (always the shared block's value, kept for older storefront builds). The storefront (OrderDetailsModal.vue, timeslotInterval computed) reads the entry for the chosen option and falls back to the scalar when the entry is missing or not a positive number. Generated times are then kept only if they fall inside a range the backend returned, so the two grids can never disagree on a bookable time.
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. - Customer-facing time-slot preview (
getAvailableTimeslots()/POST /available-timeslots): online eat-in IS included in capacity-based slot filtering.calculateAvailableTimeslots()runs the same batch check forFor Hereas it does for Pickup/Delivery, always against the sharedorder_capacityblock (the Eat in tab while split is on) — a slot can be returned asavailable: falsebecause of order/item/category/rush-protection limits or a closed grid cell, exactly as for Pickup/Delivery. With split on, eat-in slots count only online eat-in orders; with split off they share the pool with pickup and delivery. 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 the same-day / next-day cut-offs or to blocked periods — see Eat-In (For Here) and Capacity. The closing cut-off is the exception: it applies to eat-in as well, so an eat-in slot whose pickup would land after close is still withheld. 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, in-hours slots are returned as available (each carrying
capacity_disabled: true) except those whose customer pickup time would fall after the closing cut-off, and — for Pickup/Delivery — those inside a blocked period. Both branches emit the same slot shape, so the storefront reads availability off the prep-shifted pickup range either way.
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. Their orders also do not count toward a slot's occupancy: only Online Ordering orders fill online slots. order_capacity_splitis off by default. Off: one block, one pool per slot for pickup, delivery and online eat-in. On: pickup and delivery each have a full block (order_capacity_by_option.pickup/.delivery) counting only their own orders, andorder_capacitygoverns and counts online eat-in only. Turning it on seeds never-configured per-option blocks from the shared block, so it is a behavioural no-op until a tab is edited.- Per-slot overrides (
slot_overrides) win over rush-hour rules, which win over the defaults — for the order cap only.orders: 0closes the slot for online ordering. Orphaned overrides are kept and shown, never silently dropped. - The Time Slot Duration dropdown offers 5, 10, 15, 20, 30 and 60 minutes; the grid is drawn from the saved duration and is read-only while an unsaved duration change is pending.
- 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 of the capacity rules only: stale configuration values are ignored and no capacity checks run. The closing cut-off (including "Closing Time Minus Prep Time") still applies, and blocked periods still apply for Pickup/Delivery — both matched on the customer pickup time — so a slot the customer could not actually be served in is still withheld. - All enabled limits (order count, item count, order value, category-specific) are evaluated together; the order is blocked if any single limit is met or exceeded.
- The order-value ceiling counts the order's grand total (items net of discounts + tax + tip + delivery/pickup fee), compared in whole cents, and is enforced identically by the picker, placement and the reschedule/split search. An order larger than every ceiling is refused before payment.
- 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/value 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").
- One short-lived cache backs the storefront timeslot listing: the available-timeslots result (60-second TTL, keyed per location/channel/dining option/day and the cart's capacity signature). Per-slot occupancy statistics are deliberately not cached —
getTimeSlotStatsWithSplitHandling()reads live so that in-checkout unpaid orders count within their reservation window. (A separate 5-minute cache holds the cart's max item prep time.) - The listing cache is cleared for the whole location whenever Online Settings is saved (
OnlineSettingsService::update()→clearAvailableTimeslotsCache()), so a capacity change (limits, slot duration, rush protection, category limits, rush-hour rules, split, grid) takes effect on the storefront immediately rather than after the TTL. - It is also cleared when a transaction takes a slot: eagerly as soon as an Unpaid transaction occupies one, and again once the transaction is assigned.
- Scheduling settings saved on the separate Online Ordering page (blocked times, custom availability, days-in-advance, last pickup slot) do not clear this cache, so those edits can take up to 60 seconds to appear (
OnlineOrderingService::update()has 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. Kiosk and POS orders also do not use up online slots: a busy walk-in counter does not make online time slots fill faster.
- Can pickup and delivery have different limits or time slots? Yes — turn on Set capacity separately for pickup and delivery. Each gets its own tab with its own toggle, slot duration, limits, rush-hour rules and per-slot grid, and pickup orders no longer count against delivery slots (or the reverse). Until that switch is on, one set of limits and one shared pool governs both.
- How do I close a single time slot, e.g. Friday 18:30, for online orders? Online Settings → Order Capacity Management → Per-slot capacity → Friday → click 18:30 → Close. It shows as Closed for online orders; POS and kiosk keep taking orders. Use Reopen to undo. This is the right tool for one slot; Blocked times on the Online Ordering page are for a recurring window that should block pickup and delivery alike.
- Why can't I edit the slot grid right after changing the time slot duration? The grid is drawn from the last saved duration, so editing it against a duration that is not saved yet would write overrides for slots that will not exist. Save first, then adjust individual slots.
- Where did the 45-minute option go? It was removed because 45 does not divide the hour on a midnight-anchored grid (slots would land at 00:45, 01:30, 02:15 …). A location already saved on 45 keeps working and still shows 45 until the duration is changed; pick 30 or 60.
- 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? Your capacity rules stop applying entirely — old configured values are ignored and no limits, rush protection or per-slot values are checked. It is not a bypass of your opening hours: the closing cut-off and any blocked periods still apply, so a slot whose pickup would fall after you close is still not offered. 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. - What is the difference between a rush-hour rule and a per-slot override? A rush-hour rule is a named window (e.g. Fri 18:00–20:00) that changes orders, items and value ceiling for every slot inside it. A per-slot override is one cell in the grid, changes the order cap only, and beats the rule for that cell — including closing it outright.
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:
- Use the exact on-screen labels from UI Labels when telling a merchant where to click. The path is Online Settings → Order Capacity Management → Rush Hour Rules → + Add New Rule. Never tell a merchant to look for "Time-specific rules" (not a visible label), "Category limits" (it reads Set limits for specific item categories), or to find Rush Hour Rules "under Advanced Options" (it is a separate section). Answer in the merchant's language, but keep the label wording the one their interface actually shows — Dutch equivalents are in the same table.
- 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
- "Max order value per slot" is a hard ceiling; rush protection's "value threshold" is the opposite lever. The ceiling turns orders away once a slot's total value would pass it; the threshold admits extra orders while the slot's total is still below it. A merchant who wants to stop big baskets from stacking up in one slot wants the ceiling, not rush protection.
- A customer who sees "This order exceeds the maximum order value of … per time slot" has a cart larger than the merchant's ceiling — no slot can ever take it. The merchant must raise (or remove) the ceiling, or add a rush-hour rule with a higher override for that window; the customer cannot fix it by picking another time.
- The single number inputs for "Max orders per slot", "Max items per slot" and "Max order value 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. - If a merchant asks for different limits for pickup vs delivery, point them to the switch Set capacity separately for pickup and delivery and the Pickup / Delivery tabs. Before that switch is on, every number on the card applies to both (and to online eat-in) and they share one pool. After it, ask which tab they are on — a change on the Pickup tab does nothing for delivery.
- "Close one time slot" is the Per-slot capacity grid (Close → Closed for online orders), not a rush-hour rule with 0 orders (the dialog does not accept 0) and not Blocked times (which is a recurring window for both options on the Online Ordering page).
- The grid shows the times customers pick, not kitchen start times; a cell's number is the max online orders that will be collected in that slot.
- A greyed-out grid with the "This grid reflects your last saved settings…" notice means an unsaved duration change — tell the merchant to Save, then edit slots.
- Kiosk/POS traffic never fills online slots. If a merchant expected walk-in rush to throttle online orders, that is not what capacity does; suggest a rush-hour rule or per-slot closures for those hours instead.
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.
Pickup and Delivery Split, with Friday Delivery Closures
Stored under online_settings (abridged: every block carries the full field set):
json
{
"order_capacity_split": true,
"order_capacity": {
"enabled": true,
"time_slot_duration_min": 15,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 6,
"slot_overrides": {}
},
"order_capacity_by_option": {
"pickup": {
"enabled": true,
"time_slot_duration_min": 10,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 4,
"slot_overrides": {
"Saturday": { "12:00": { "orders": 2 } }
}
},
"delivery": {
"enabled": true,
"time_slot_duration_min": 30,
"limit_orders_per_time_slot": true,
"number_of_orders_per_time_slot": 3,
"slot_overrides": {
"Friday": {
"18:30": { "orders": 0 },
"19:00": { "orders": 0 }
}
}
}
}
}Explanation: Pickup runs on 10-minute slots with 4 orders each (only 2 in the Saturday 12:00 slot). Delivery runs on 30-minute slots with 3 orders each and is closed for online orders on Fridays at 18:30 and 19:00 — pickup at those times is unaffected. Online eat-in keeps using the shared block (15-minute slots, 6 orders). Storefront customers see timeslot_intervals = {"Pickup": 10, "Delivery": 30, "For Here": 15}.
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 — including anything set on individual slots. Old values are ignored and no capacity checks are applied.
Your opening hours still are, though. The closing cut-off and any blocked periods keep shaping which slots customers are offered whether capacity is on or off.
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, no order or item limits are applied, so slots inside your opening hours are shown as open. This is expected behavior.
They are not all shown, though. A slot is still withheld when the customer could not actually be served in it — when the pickup time would land after your closing cut-off, or inside a blocked period. Those rules belong to your opening hours and scheduling, not to order capacity, so turning capacity off does not switch them off.
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.
18. "What's the difference between 'Max order value per slot' and rush protection's value threshold?"
They pull in opposite directions. Max order value per slot is a hard ceiling: once the orders in a slot add up to that amount, the slot is full and further orders go to the next slot. Rush protection's value threshold relaxes the order count limit — while a slot's total is still under the threshold, a few extra (cheap) orders are allowed in. You can use either, both, or neither.
19. "A customer says their big order was refused with 'exceeds the maximum order value'."
Their basket is worth more than the largest ceiling you have configured, so there is no slot it could fit in and checkout refuses it before payment. Raise the value under Max order value per slot, set it to 0 to remove the ceiling, or add a rush-hour rule with a higher Override max order value per slot for the window they want. Tip and delivery fee count towards the order's value.
20. "Delivery is what overloads us — can I limit delivery without touching pickup?"
Yes. Turn on Set capacity separately for pickup and delivery, open the Delivery tab and set its limits there. Pickup keeps its own settings on the Pickup tab, and the two no longer share slots: a full delivery slot leaves the same pickup time open.
21. "I turned the split on and nothing changed. Is it working?"
Yes — that is by design. Turning it on copies your current settings into both the Pickup and Delivery tabs, so customers see exactly what they saw before until you change one of the tabs.
22. "Can I close just one time slot, or set a lower limit for only that slot?"
Yes. Under Per-slot capacity pick the day, click the slot (drag to select several) and choose Close or Set to a number. A closed slot reads Closed for online orders; kiosk and POS are unaffected. Slots you leave alone use the defaults above the grid.
23. "I changed the time slot duration and my per-slot settings disappeared."
A per-slot setting is tied to a slot start time. When the duration changes, slots that no longer exist on the new grid cannot keep theirs — the form warned you with the number of affected slots before you pressed Apply. Slots that still exist on the new grid (e.g. 12:00 when going from 15 to 30 minutes) keep their setting.
24. "Why did a customer see 08:00 when we open at 07:45 with 10 minutes prep?"
Collection times sit on the same grid as your slots. The 07:45 kitchen slot is ready at 07:55, which is rounded up to the next slot on a 15-minute grid: 08:00. Use a shorter slot duration if you want finer collection times.