Skip to content

Scheduled Orders

Overview

Scheduled orders let customers place online orders for a future pickup or delivery time instead of for the current time (ASAP).

Key Purpose: Enable advance ordering for future times.

Purpose

Scheduling lets customers choose a future date and time for pickup or delivery. Available timeslots are calculated per day from the location's business hours (or a custom availability schedule) and, when order-capacity management is enabled, filtered against capacity limits.

Key Concepts

  • Timeslots: Generated per day from business hours (or the custom availability schedule). Slot length is order_capacity.time_slot_duration_min (default 15 minutes; if capacity is disabled the calculation falls back to 30-minute slots). When capacity is enabled, slots are filtered against capacity limits.
  • Per-channel scheduling: Takeout (pickup), delivery, and eat-in (online "For Here") each have independent enabled toggles and a days_in_advance limit (validated as numeric|max:365; the back-office field clamps the entry to 0–365).
  • Online eat-in vs. table-QR dine-in: "Eat in" is an opt-in online-ordering option (eat_in.enabled) where a customer orders ahead on the storefront and eats in at a chosen future time — it behaves like online pickup (timeslot picker, future-days scheduling, shares online-ordering order capacity) but the order is recorded as dine-in (For Here) for POS/VAT purposes and follows the location's business hours (no cut-offs, no blocked periods). This is distinct from table-QR ordering (scanning a QR code at a table), which is immediate/ASAP only and never goes through scheduling or capacity checks.
  • Capacity checking: When order capacity is enabled, slot availability is checked at order submission for online pickup/delivery. Transaction updates that book a slot use a cache lock with retry (up to 3 attempts, 15-second lock TTL per attempt, 200 ms between retries).
  • Blocked Times: A merchant-configurable list of recurring weekly windows (blocked_periods) during which pickup/delivery slots are hidden from the picker, even during otherwise-open business hours (e.g. a daily kitchen rush). Applies to pickup and delivery only, not eat-in.
  • Send to POS timing: send_to_pos controls whether an order is sent to the POS/KDS when it is placed (order_placed) or at the scheduled pickup/delivery time (pickup_delivery_time).
  • Custom scheduled orders: An advanced toggle (custom_scheduled_orders) that exposes hours_in_advance and minutes_before_pickup_delivery_time thresholds for finer control over when scheduled orders are sent.

Actions

Enable Scheduled Pickup Orders

Toggle schedule_order_for_future_days.takeout.enabled and set schedule_order_for_future_days.takeout.days_in_advance to allow customers to schedule pickup orders.

Enable Scheduled Delivery Orders

Toggle schedule_order_for_future_days.delivery.enabled and set schedule_order_for_future_days.delivery.days_in_advance independently from pickup scheduling.

Enable Scheduled Eat-In Orders

Toggle schedule_order_for_future_days.eat_in.enabled and set schedule_order_for_future_days.eat_in.days_in_advance independently from pickup/delivery scheduling. Only shown/relevant when online eat-in ("For Here") is itself enabled (eat_in.enabled, configured on the main Online Ordering settings).

Configure Pickup Last Slot

Toggle Last pickup slot at closing time (takeout.last_slot_at_closing_time, default on) to control how close to closing the last pickup slot is. On: the last pickup slot is exactly at closing time. Off: the last pickup slot is one preparation time before closing. This setting only affects pickup — delivery always reserves preparation time before closing. It replaced the old two-option "Accept Orders Until" selector, which is no longer shown in the back office.

Configure Blocked Times

On the Blocked times card, click Add blocked time to add a recurring weekly window (blocked_periods) during which pickup/delivery slots are hidden, then pick the time slot and toggle the days of the week it's active on. Click the trash icon to remove a blocked period. Requires order_capacity.time_slot_duration_min to be configured first (the time-slot picker is disabled otherwise). Affects pickup and delivery only, not eat-in.

Enable Custom Availability

Toggle custom_availability to define a specific weekly schedule for when scheduled orders are accepted, instead of using the location's business hours.

Configure Send to POS Behavior

On the Receiving Orders form, set send_to_pos to pickup_delivery_time (send near the scheduled time) or order_placed (send immediately). Optionally toggle custom_scheduled_orders to set the hours_in_advance and minutes_before_pickup_delivery_time thresholds.

Location

  • Back-office route: /online/online-ordering (nav: Online → Online Ordering)
  • Scheduling fields (custom_availability, takeout.last_slot_at_closing_time, blocked_periods, schedule_order_for_future_days.*): src/views/online/online-ordering/forms/FullfilmentAndScheduling.vue
  • Send-to-POS / custom-scheduled-orders fields (send_to_pos, custom_scheduled_orders, hours_in_advance, minutes_before_pickup_delivery_time): src/views/online/online-ordering/forms/ReceivingOrders.vue

All settings are persisted via StoreOnlineOrderingRequest.

Fields

Takeout (Pickup) Scheduling Enabled

PropertyValue
Field IDschedule_order_for_future_days.takeout.enabled
TypeToggle (boolean)
Defaulttrue
Validation`required

Description: Allow customers to schedule pickup orders.


Takeout Days in Advance

PropertyValue
Field IDschedule_order_for_future_days.takeout.days_in_advance
TypeNumber
Default30
Min0 (back-office field clamp)
Max365
Validation`required

Description: How many days ahead customers can schedule pickup.


Delivery Scheduling Enabled

PropertyValue
Field IDschedule_order_for_future_days.delivery.enabled
TypeToggle (boolean)
Defaultfalse
Validation`required

Description: Allow customers to schedule delivery orders.


Delivery Days in Advance

PropertyValue
Field IDschedule_order_for_future_days.delivery.days_in_advance
TypeNumber
Default30
Min0 (back-office field clamp)
Max365
Validation`required

Description: How many days ahead customers can schedule delivery.


Eat-In Scheduling Enabled

PropertyValue
Field IDschedule_order_for_future_days.eat_in.enabled
TypeToggle (boolean)
Defaultfalse
Validation`nullable

Description: Allow customers to schedule online eat-in ("For Here") orders for a future time. Only shown in the back office when eat-in itself is enabled (eat_in.enabled).


Eat-In Days in Advance

PropertyValue
Field IDschedule_order_for_future_days.eat_in.days_in_advance
TypeNumber
Default30
Min0 (back-office field clamp)
Max365
Validation`required

Description: How many days ahead customers can schedule an online eat-in order.


Pickup Last Slot at Closing Time

PropertyValue
Field IDtakeout.last_slot_at_closing_time
TypeToggle (boolean)
Defaulttrue
Validation`sometimes
UI Label"Last pickup slot at closing time"

Description: When on (default), the last pickup slot is exactly the closing time. When off, the last pickup slot is one preparation time before closing. Pickup-only — delivery always reserves preparation time before closing. This replaced the old two-option accept_orders_until ("Accept Orders Until") selector, which is no longer shown in the back office. accept_orders_until is still present in the save payload/backend defaults for backward compatibility, but merchants cannot edit it — the "Accept Orders Until" UI has been fully retired.


Blocked Times

PropertyValue
Field IDblocked_periods
TypeArray of { from, to, active_days }
Default[] (empty)
Validation`sometimes

Description: A list of recurring weekly windows during which pickup/delivery slots are hidden from the storefront picker (e.g. a daily kitchen rush), even though the location is otherwise open. Each entry has a start time, an end time, and the weekdays it's active on. The back-office time-slot picker for a blocked period is generated from order_capacity.time_slot_duration_min, so slot duration must be configured before a blocked time can be added.

Business Logic:

  • Applies to pickup and delivery only — eat-in follows business hours and is never gated by blocked periods.
  • Matched against the customer-facing pickup/delivery time (kitchen prep time + delay), not the earlier kitchen-start time, using a half-open interval [from, to).
  • Location::isWithinBlockedPeriod() is the single source of truth, used consistently by order-placement validation, the post-capacity re-check, and the storefront slot filter — so a slot inside a blocked window is rejected everywhere, not just hidden from the picker.
  • When order splitting reschedules part of an order into a later slot, the reschedule search also skips blocked windows.

Custom Availability

PropertyValue
Field IDcustom_availability
TypeToggle (boolean)
Defaultfalse
Validation`required

Description: When on, a custom weekly schedule is used instead of the location's business hours.


Send to POS

PropertyValue
Field IDsend_to_pos
TypeRadio (enum SendToPosOptions)
Optionspickup_delivery_time, order_placed
Defaultpickup_delivery_time
Validationrequired + enum

Description: Whether scheduled orders are sent to the POS/KDS at the scheduled pickup/delivery time or immediately when placed.


Custom Scheduled Orders

PropertyValue
Field IDcustom_scheduled_orders
TypeToggle (boolean)
Defaultfalse
Validation`required

Description: When on, exposes the hours_in_advance and minutes_before_pickup_delivery_time thresholds.


Hours in Advance

PropertyValue
Field IDhours_in_advance
TypeNumber
Validation`required

Description: Threshold (in hours) used when custom_scheduled_orders is enabled.


Minutes Before Pickup/Delivery Time

PropertyValue
Field IDminutes_before_pickup_delivery_time
TypeNumber
Validation`required

Description: Threshold (in minutes) used when custom_scheduled_orders is enabled.


Business Logic

Timeslot Generation

Customer selects a date for a dining option (pickup / delivery / eat-in)


Eat-in (For Here)?
├── Yes → use the location's business hours (no cut-off checks, no blocked periods)
└── No (pickup/delivery) → custom_availability enabled?
        ├── Yes → use the custom weekly schedule
        └── No  → use the location's online-ordering business hours
        then: not past same/next-day cutoff?


Day has available hours (and, for pickup/delivery, not past cutoff)?
├── No  → return no slots
└── Yes → generate slots every time_slot_duration_min minutes


Capacity (order_capacity) enabled?
├── No  → all generated slots returned as available
└── Yes → batch-check each slot against capacity limits
           (eat-in shares the same online-ordering order-capacity limits as pickup/delivery)


Pickup/delivery only: mark slots inside a blocked_periods window as unavailable


Filter out past slots (for today) and full slots


Return available timeslots (cached 60 s per location/channel/dining-option/day)

Order Processing

Scheduled order placed (order_date holds the chosen time)


send_to_pos setting?
├── pickup_delivery_time → sent near the scheduled time
└── order_placed         → sent immediately on placement

Business Rules

  • Past timeslots are filtered out for today's date; when capacity is enabled, full slots are excluded.
  • A day with no available business hours, or a day already past its same-day/next-day cutoff (pickup/delivery only), returns no slots.
  • Order capacity applies to online-ordering pickup, delivery, and online eat-in (customer orders ahead on the storefront for a future dine-in time — dining option For Here). Orders on other channels/flows (e.g. Kiosk, In-house, and table-QR ordering — a customer scanning a QR code at their table) are immediate/ASAP only and never go through scheduling or order-capacity checks.
  • Online eat-in has its own future-days scheduling window (schedule_order_for_future_days.eat_in), independent from takeout/delivery, but always follows the location's business hours (not a custom availability schedule) and is exempt from same/next-day cut-offs and Blocked Times.
  • Blocked Times (blocked_periods) hide pickup/delivery slots inside merchant-defined recurring weekly windows, even during otherwise-open hours. They apply to pickup and delivery only, are matched on the customer-facing pickup/delivery time (not the kitchen start time), and are enforced consistently at slot listing, order placement, and any capacity-driven reschedule/split.
  • Booking a slot uses a cache lock with retry (up to 3 attempts, 15-second lock TTL per attempt, 200 ms between retries) to avoid double-booking under concurrency.
  • When capacity is enabled, the prep time used for slot checking is max(location average prep time, largest individual item prep time) + the channel delay (delivery.delay, takeout.delay or eat_in.delay), clamped to 0-1440 minutes (24 hours). The item term is compared against the location average, not added to it, so it can only ever raise a quote above the location baseline, never lower it. The channel delay may be negative (a merchant can shorten the lead time with e.g. -10); it is added as-is and only the final result is clamped. The item term is also 0 today: per-item prep time is gated by ONLINE_ORDERING_INCLUDE_ITEM_PREP_TIME, which defaults to false and is not set in production, staging or testing — so in practice the figure is the location average prep time plus the channel delay. The same figure is what the nearby-restaurants list advertises before the customer starts an order — one rule, one number. (Verified: app/RawModels/Location.php:2001-2018getDiningOptionPrepTime(), the single definition, with the 1440 ceiling at :60; app/Services/OrderCapacity/OrderCapacityService.php:86-89 delegates to it and :101-115 supplies the item term; config/upvendo.php line 26.)
  • The available-timeslots result is cached for 60 seconds, keyed per location, channel, dining option, day, and the prospective cart's prep time / value / quantity / category mix. It is busted immediately when a transaction books or changes a slot, and when the Online Settings page (order capacity) is saved. (Verified: app/Services/OrderCapacity/OrderCapacityService.php lines 1789-1800 (60 s TTL and cache key), 267 and 489 (busted on booking); app/Services/BackOffice/OnlineSettingsService.php lines 106-113.)
  • Saving the scheduling fields on this page (Online Ordering, PUT /back-office/online-ordering/{locationId}) does not bust the availability cache — custom_availability, blocked_periods, schedule_order_for_future_days.*, and takeout.last_slot_at_closing_time can take up to 60 seconds to show on the storefront. Only the Online Settings save clears it eagerly. (Verified: app/Services/BackOffice/OnlineOrderingService.php::update() lines 223-280 — saves and dispatches ReloadMenu, with no clearAvailableTimeslotsCache call; contrast app/Services/BackOffice/OnlineSettingsService.php lines 106-113.)
  • There is no is_asap / asap_enabled field. "ASAP" simply means no future time was chosen, so the order's order_date is the current time.
  • The old two-option "Accept Orders Until" selector has been removed from the back office; pickup's closing-edge behavior is now controlled by the single "Last pickup slot at closing time" switch (takeout.last_slot_at_closing_time).

FAQs

  • "Can scheduling be enabled for delivery but not pickup?" Yes. schedule_order_for_future_days.takeout, .delivery, and .eat_in each have independent enabled and days_in_advance settings.
  • "What happens if a slot fills up while a customer is selecting it?" Capacity is re-checked at order submission under a cache lock. When order splitting is enabled, an order that doesn't fit one slot can be split across consecutive slots within the same day.
  • "How often are available timeslots refreshed?" The result is cached for 60 seconds. It is busted immediately whenever a slot is booked or changed, and whenever Online Settings (order capacity) is saved — so capacity limit changes appear on the storefront straight away. Changes saved on the Online Ordering page itself (blocked times, custom availability, days-in-advance, last pickup slot) do not bust the cache and can take up to 60 seconds to appear. (Verified: app/Services/BackOffice/OnlineSettingsService.php lines 106-113; app/Services/BackOffice/OnlineOrderingService.php::update() lines 223-280 has no cache clear.)
  • "Where did the 'Accept Orders Until' setting go?" It was replaced by the "Last pickup slot at closing time" switch (takeout.last_slot_at_closing_time). On (default): the last pickup slot is exactly at closing time. Off: the last pickup slot is one preparation time before closing. Delivery is unaffected.
  • "How do I block off a time window so customers can't order during our lunch rush?" On the Online Ordering page, go to the Blocked times card under Fulfillment & Scheduling and add a blocked time: pick a start slot and toggle which days of the week it applies to. It hides pickup/delivery slots inside that window every week it's active. It doesn't affect eat-in.
  • "Can I schedule an eat-in (dine-in) order for later today or another day?" Only if online eat-in ("For Here") is enabled and schedule_order_for_future_days.eat_in.enabled is on. Eat-in shares the same order-capacity slot filtering as pickup/delivery but always follows business hours (not cut-offs or Blocked Times). Note this is different from scanning a table QR code, which is always immediate/ASAP.
  • "When is a scheduled order sent to the POS/KDS?" It depends on send_to_pos: pickup_delivery_time sends it near the scheduled time; order_placed sends it immediately. custom_scheduled_orders adds hours_in_advance / minutes_before_pickup_delivery_time control.

Troubleshooting

  • No slots available for a day → Check the location has online-ordering hours (or a custom availability schedule) for that day, the day isn't past its same/next-day cutoff (pickup/delivery), and that order-capacity limits or Blocked Times aren't hiding every slot.
  • A specific time window never shows slots, even though the location is open → Check the Blocked times card — a recurring blocked_periods window active on that weekday hides pickup/delivery slots inside it. Remove or edit the blocked period.
  • Scheduled order not appearing on the POS/KDS → Check send_to_pos. With pickup_delivery_time the order is sent near the scheduled time; with order_placed it is sent immediately on placement.
  • Customer sees a "slot full" error → The slot reached its capacity limit. Increase the limit in Order Capacity settings, or pick another time.
  • Days-in-advance not working → Verify scheduling is enabled for the correct channel (takeout, delivery, or eat_in) and that days_in_advance is greater than 0.
  • Can't add a Blocked Time / the time-slot dropdown is disabledorder_capacity.time_slot_duration_min must be configured first; the Blocked Times picker is derived from it.
  • A scheduling change (blocked time, custom availability, days-in-advance, last pickup slot) isn't showing on the storefront yet → Saving the Online Ordering page does not clear the availability cache, so wait up to 60 seconds and reload. If it still doesn't show after that, the change was not saved or another rule (business hours, cut-off, capacity limits) is hiding the slots. (Verified: app/Services/BackOffice/OnlineOrderingService.php::update() lines 223-280; 60 s TTL at app/Services/OrderCapacity/OrderCapacityService.php line 1800.)

Customer Impact

Storefront and kiosk behaviour below is not verified against code here (source of truth for this doc is the back-office settings + backend validation/capacity logic).

Online Ordering (storefront — not verified here)

  • Date picker for future dates
  • Timeslot selection showing only available slots
  • Confirmation reflects the chosen time

Kiosk (not verified here)

  • ASAP vs scheduled selection where applicable

Assistant Guidance

When answering questions about this feature:

  • "Accept Orders Until" no longer exists as a UI control — do not tell merchants to look for it. Point them to the Last pickup slot at closing time switch (takeout.last_slot_at_closing_time) instead; it only affects pickup, not delivery.
  • Do not conflate online eat-in (customer orders ahead on the storefront for a future dine-in time, dining option For Here) with table-QR ordering (scanning a QR code at a table). Table-QR orders are always immediate/ASAP and never use scheduling, days-in-advance, or order-capacity checks. Online eat-in does use scheduling and shares order capacity with pickup/delivery.
  • Blocked Times (blocked_periods) only apply to pickup and delivery, never to eat-in. If a merchant asks why an eat-in time slot doesn't respect a blocked window, that's expected — eat-in follows business hours only.
  • When a merchant asks why "no slots are available," check in this order: (1) location has online-ordering hours or a custom availability schedule for that day, (2) not past the same/next-day cutoff (pickup/delivery only), (3) no Blocked Times window covers the whole open period, (4) order-capacity limits aren't exhausted.
  • Mention that each of takeout, delivery, and eat-in has its own independent enabled + days_in_advance setting — changing one does not affect the others.
  • Be precise about propagation delay, because the two settings pages behave differently. Order capacity changes (Online Settings page) clear the availability cache on save and show up on the storefront immediately. Scheduling changes made on the Online Ordering page (blocked times, custom availability, days-in-advance, last pickup slot) do not clear it and can take up to 60 seconds. Do not tell a merchant that capacity changes take up to a minute — that has not been true since the settings-save cache invalidation landed.

Relations

Depends On

  • Business Hours: Source of available times when custom availability is off, and always the source for eat-in
  • Order Capacity: Slot duration (time_slot_duration_min), capacity limits, and the timeslot picker used to configure Blocked Times
  • Restricted Dates: Blocked dates

Affects

  • Transactions: Chosen time stored in order_date; dining option For Here on online eat-in orders
  • KDS / POS: Order send timing (send_to_pos)

Examples

Pickup Scheduling Enabled, Delivery Disabled

json
{
  "schedule_order_for_future_days": {
    "takeout": {
      "enabled": true,
      "days_in_advance": 30
    },
    "delivery": {
      "enabled": false,
      "days_in_advance": 30
    }
  }
}

Both Channels Enabled, Different Limits

json
{
  "schedule_order_for_future_days": {
    "takeout": {
      "enabled": true,
      "days_in_advance": 7
    },
    "delivery": {
      "enabled": true,
      "days_in_advance": 3
    }
  }
}

Eat-In Scheduling Enabled

json
{
  "eat_in": {
    "enabled": true,
    "delay": 0
  },
  "schedule_order_for_future_days": {
    "eat_in": {
      "enabled": true,
      "days_in_advance": 14
    }
  }
}

Pickup Last Slot and Blocked Times

json
{
  "takeout": {
    "last_slot_at_closing_time": false
  },
  "blocked_periods": [
    {
      "from": "14:30",
      "to": "16:00",
      "active_days": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
    }
  ]
}

Send-to-POS and Custom Scheduled Orders

json
{
  "send_to_pos": "pickup_delivery_time",
  "custom_scheduled_orders": true,
  "hours_in_advance": 2,
  "minutes_before_pickup_delivery_time": 30
}

Available Timeslots Response

The available-timeslots result is an object keyed by slot start time (H:i); each value describes that slot.

json
{
  "12:00": {
    "start": "12:00",
    "end": "12:15",
    "duration_minutes": 15,
    "available": true
  },
  "12:15": {
    "start": "12:15",
    "end": "12:30",
    "duration_minutes": 15,
    "available": false
  }
}

When order capacity is disabled, every generated slot is returned with "available": true and an informational "capacity_disabled": true flag.