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).
  • Per-day trigger (custom_scheduled_orders_per_day): Weekday overrides for the hours_in_advance / minutes_before_pickup_delivery_time pair, keyed on the pickup day in the location's timezone. Sparse — only overridden days are stored.
  • 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 entry (blocked_periods), then pick the Time slot and toggle the days of the week it's active on. Each entry blocks exactly one slot start (stored as a one-minute half-open window, from → from + 1 min); add one entry per slot to block a longer stretch. Click the trash icon to remove a blocked period. The Time slot dropdown is disabled with Configure time slot duration first until a slot duration exists on the capacity block that governs pickup or delivery — the shared order_capacity block, or, with Set capacity separately for pickup and delivery on, either per-option block; under split the dropdown lists the slot starts of both options' grids. Affects pickup and delivery only (one list for both), not eat-in. To close a single slot for pickup or delivery only, use the Per-slot capacity grid on Online Settings instead (see Order Capacity).

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 current back office writes one-slot entries (to = from + 1 minute) so that an entry targets exactly one slot start; older free-form ranges are still honoured by the engine. The Time slot dropdown is generated from the slot duration of the capacity block governing pickup and delivery (order_capacity.time_slot_duration_min, or the per-option blocks' durations when order_capacity_split is on), so a 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.


Per-Day Scheduled Order Trigger ​

PropertyValue
Field IDcustom_scheduled_orders_per_day
Type{ enabled: boolean, days: { <Weekday>: { hours_in_advance, minutes_before_pickup_delivery_time } } }
Defaultabsent (treated as disabled, no days)
Validationnullable|array; enabled nullable|boolean; for each day present, both hours_in_advance and minutes_before_pickup_delivery_time are required|numeric|min:1

Description: Weekday overrides for the trigger pair. Storage is sparse — only the days you actually override are stored, and days you leave alone fall through to the location-wide setting. Day keys are full English weekday names (Monday … Sunday).

Setting only one of the two values for a day is rejected with a 422: an override is the pair or nothing.

The weekday is read in the location's own timezone, from the pickup/delivery moment — not from the raw UTC instant, and not from when the order was placed. A Friday 00:30 Amsterdam pickup is Thursday 22:30 UTC; it uses Friday's override.


Business Logic ​

Which Trigger Wins ​

Three settings can decide when a scheduled order reaches the POS. They are resolved in this order:

1. custom_scheduled_orders_per_day enabled, AND the pickup day has a
   complete override                          → use that day's pair
2. custom_scheduled_orders enabled            → use hours_in_advance +
                                                 minutes_before_pickup_delivery_time
3. send_to_pos = order_placed                 → send now
4. otherwise                                  → the channel's own default

Once a trigger pair applies: if the pickup is at least hours_in_advance hours away, the order is sent minutes_before_pickup_delivery_time before the pickup time; if it is nearer than that, it is sent immediately.

A per-day block that is enabled but has no entry for that weekday, or has an incomplete one, falls through to step 2 — it does not disable the trigger.

This one resolver governs every channel that sends scheduled orders, online ordering and Uber Eats alike. It replaced four copied-and-pasted versions of the rule, so the behaviour no longer differs by channel.

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 the platform's item-prep-time flag, 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-2018 — getDiningOptionPrepTime(), 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, and custom_scheduled_orders_per_day can override that pair per weekday. See Which Trigger Wins.
  • "Can Sunday use a different lead time from Monday?" Yes — enable the per-day trigger and set the pair on just the days you want to differ. The day is the pickup day in the location's timezone.

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 disabled → order_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, generated slots carry an informational "capacity_disabled": true flag and are returned with "available": true — except slots whose customer pickup time would fall after the closing cut-off, and (for pickup and delivery) slots inside a blocked period. Disabling capacity switches off the capacity limits, not the location's opening hours. See Order Capacity.