Appearance
Restricted Dates (Closures)
Overview
Restricted Dates let you mark specific dates (or date ranges) on which a location is closed. Use this for holidays, planned closures, or any day the location will not accept orders.
Key Purpose: Mark dates when a location is fully closed, overriding the regular weekly business hours for those dates.
Restricted Dates is a section inside the Location form. It is not a standalone page: it appears as one step of the add/edit location flow at /settings/locations/:id (where :id is a location id, or new when creating a location).
Purpose
This section lets you add and remove date-specific closures that override the regular weekly business hours for a location. A matched restricted date makes the location unavailable for ordering on that day.
Key Concepts
- Restricted Date: A specific calendar date (
date) on which the location is closed. There are no "special hours" — a matched date is treated as a full closure. - Date Range: Instead of a single date, an entry can use a range with a start (
date_from) and end (date_until). The UI lets you toggle a row between single-date mode and range mode. - Yearly Recurrence: A
yearlyflag marks a single-date entry as repeating every year. In the UI the "Repeats every year" checkbox is disabled for range entries, and the API rejects the combination too — an entry that is both yearly and a range fails validation with "A date range cannot repeat every year. Use a single date to repeat a closure yearly, or turn off 'repeats every year' for the range." The disabled checkbox is only the first line of defence. - Closure Only (no special hours): The backend matching logic returns "unavailable" for a matched date. There is no per-channel toggle and no modified-hours option.
- Location-Level Storage: Restricted dates are stored as an array on the Location model's
restricted_datesfield and are evaluated in the location's timezone.
Actions
Add a Restricted Date
Open the location's form, scroll to the Restricted Dates section, and click the add (+) button. Pick a single date, or enable "Date range" to pick a start and end date. For a single date you can also tick "Repeats every year".
Remove a Restricted Date
Click the trash icon on a restricted date row to delete it. The location reverts to its regular business hours for that day.
Note: The UI provides add and remove only. There is no separate in-place "edit" action for an existing entry beyond changing its date/range/yearly values directly on the row before saving the location. When
restricted_datesis in a location'suneditable_fields, the inputs are disabled.
Location
- Backoffice Route:
/settings/locations/:id(route namesettings-locations-id); Restricted Dates is a step within the location form, not its own route - Backend Model:
app/RawModels/Location.php(fieldrestricted_dates) - Backend Request:
app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php - Vue Component:
src/views/settings/Locations/forms/LocationRestrictedDates.vue(rendered fromsrc/views/settings/Locations/LocationForm.vue)
Fields
Each entry in the restricted_dates array has the following fields.
Date (single-date mode)
| Property | Value |
|---|---|
| Field ID | date |
| Label | Select Date |
| Type | Date (Y-m-d) |
| Required | Required when no date_from/date_until is provided |
| Validation | nullable, date_format:Y-m-d, required_without_all of date_from and date_until |
Description: The single calendar date the location is closed.
Examples:
- "2024-12-25"
- "2024-01-01"
Date From (range mode)
| Property | Value |
|---|---|
| Field ID | date_from |
| Label | From |
| Type | Date (Y-m-d) |
| Required | Required when date_until is provided |
| Validation | nullable, date_format:Y-m-d, required_with:date_until |
Description: Start date of a closed range.
Date Until (range mode)
| Property | Value |
|---|---|
| Field ID | date_until |
| Label | Until |
| Type | Date (Y-m-d) |
| Required | Required when date_from is provided |
| Validation | nullable, date_format:Y-m-d, required_with:date_from, after_or_equal:date_from |
Description: End date of a closed range. Must be the same as or after date_from.
Repeats Every Year
| Property | Value |
|---|---|
| Field ID | yearly |
| Label | Repeats every year |
| Type | Boolean (checkbox) |
| Required | Yes (always sent for each entry) |
| Default | false (new rows are added with yearly: false) |
| Validation | `required |
Description: Whether a single-date restriction repeats every year. In the UI the checkbox is disabled for range entries, so range entries are effectively non-yearly.
Business Logic:
- yearly = true: A scheduled command rolls the year forward once the date has passed (see Business Rules).
- yearly = false: One-time restriction.
Original Date (system-managed)
| Property | Value |
|---|---|
| Field ID | original_date |
| Type | Date (Y-m-d) |
| Set by | System only |
Description: Not an editable field. The location:update-restricted-dates command writes the previous date value here when it rolls a yearly entry forward to the current year. It is not entered by users and is not part of the request validation.
The following fields do NOT exist in the codebase and are not part of restricted dates: a
closedtoggle,special_hours/from/tomodified-hours, areasontext field,recurring,affects_online, oraffects_in_house. A restricted date is always a full closure for that date with no per-channel control.
Business Logic
Date Check Flow
Location::checkForBusinessHours() evaluates availability for a requested order date/time. For restricted dates it does an exact match on the entry's date (string compare of Y-m-d) in the location timezone:
Customer/system requests an order date+time
│
▼
Check temporarily_closed_until (online ordering) → if within, unavailable
│
▼
For each restricted_date:
if requested date (Y-m-d) === restricted_date.date
→ location unavailable for that date (return false)
│
▼
Otherwise fall through to same-day/next-day cutoffs
and the regular business-hours window checkA matched restricted date short-circuits to "unavailable"; the regular business hours for that day are not consulted.
Where ranges are enforced
The storefront honours ranges. Its shared date helper matches date_from/date_until inclusively (and open-ended when only one side is set), and matches yearly entries on month/day, which is what gates the date picker, the default order date and the closed banner. So a range does close the location for customers.
The backend gate Location::checkForBusinessHours() only exact-matches the entry's date. A range — or a yearly entry whose year has not yet been rolled forward — is therefore not caught by the server-side backstop; it is just not double-checked at order placement.
Priority Order
- Restricted dates (exact-date match → unavailable)
- Same-day / next-day order cutoffs
- Regular business hours window
Customer Impact
checkForBusinessHours is invoked from the online ordering flow (OnlineOrderingOrchestrator) and from order-capacity checks (OrderCapacityService). When a requested date matches a restricted date, those checks report the location as unavailable for that date.
On the online-ordering storefront a restricted date is the most common trigger for these three states:
| State | Condition | What the customer sees |
|---|---|---|
| No orderable date at all | Neither pickup nor delivery has an orderable day inside its booking window (today + days-in-advance, honouring a temporary-closure reopening date) — typically today is restricted and "schedule for future days" is off | The "Choose" date/time prompt is replaced by the closed banner "Not accepting orders" |
| Today unavailable, a later day is available | Today is restricted (or a non-open weekday) but the future-days window reaches an open day | The date auto-advances to the first orderable day, with the hint "Today is unavailable. The earliest order date is {date}." under the date field |
| Selected date past its cut-off | The same-day or next-day cut-off for the selected option has passed | An amber alert "Same-day {option} ordering has closed (cut-off was {time}). Please choose another date." (or "Next-day …") replaces the deadline hint, and Save is disabled |
Kiosk behaviour on a restricted date is downstream of the same availability result; its exact UI presentation is not verified here.
Relations
Depends On
- Locations: Restrictions are stored per-location on the Location model.
- Business Hours: A matched restricted date overrides the regular weekly hours for that date.
Affects
- Online Ordering: Date availability (via
checkForBusinessHours). - Order Capacity: Capacity checks call
checkForBusinessHours.
Related Features
Business Rules
- A restricted date is a full closure for that date. There are no special/modified hours and no per-channel scope — when a date matches, the location is simply unavailable for ordering that day.
- Matching is an exact
Y-m-dstring comparison against the entry'sdate, evaluated in the location's timezone (Location::checkForBusinessHours()). - The
location:update-restricted-datesArtisan command is scheduled yearly (once a year, at the start of January), not daily. For each entry withyearly: truewhosedateis already in the past (in the location timezone), it copies the old value intooriginal_dateand updatesdateto the same month/day in the current year. An entry with nodateis skipped untouched. - Single-date and date-range entries are mutually exclusive per row: providing
date_from/date_untilrequires both, anddate_untilmust be on or afterdate_from. The "Repeats every year" option is disabled for range entries in the UI and rejected server-side — an entry that is both yearly and a range fails validation onrestricted_dates.{index}.yearlywith "A date range cannot repeat every year. Use a single date to repeat a closure yearly, or turn off 'repeats every year' for the range." - Dates are validated with
date_format:Y-m-dinStoreLocationRequest. Therestricted_datesfield itself isnullable|arrayand defaults to an empty array.
FAQs
- "What happens on a restricted date?" The location is closed for ordering that day. There is no option to set reduced or special hours — a restricted date is a full closure.
- "How do yearly restricted dates work across years?" Mark a single-date entry as "Repeats every year". A scheduled command (
location:update-restricted-dates) rolls the date forward to the current year once it has passed, preserving the prior value inoriginal_date. You do not need to re-enter yearly dates. - "Can I close a whole range of days?" Yes, toggle "Date range" on a row and set From and Until dates. The storefront blocks every date in the range (inclusive of both ends). Note that a range cannot also be marked "Repeats every year" — use a single date for a yearly closure.
- "Can I close online ordering but keep in-house open (or vice versa)?" No. Restricted dates have no per-channel toggle; a match makes the location unavailable through
checkForBusinessHoursfor the dining options that use that check. - "What about holidays whose date changes each year (e.g. Easter)?" Do not mark these as yearly, since the calendar date shifts. Add them as one-time dates and re-enter them each year.
Examples
These examples show the actual stored shape. Each entry has date, date_from, date_until, and yearly. Use date for a single day (with date_from/date_until null) or date_from/date_until for a range.
Single date, one-time
json
{
"date": "2024-07-15",
"date_from": null,
"date_until": null,
"yearly": false
}Single date, repeats every year
json
{
"date": "2024-12-25",
"date_from": null,
"date_until": null,
"yearly": true
}Date range
json
{
"date": null,
"date_from": "2024-12-24",
"date_until": "2024-12-26",
"yearly": false
}Multiple restricted dates on a location
json
{
"restricted_dates": [
{ "date": "2024-12-25", "date_from": null, "date_until": null, "yearly": true },
{ "date": "2024-01-01", "date_from": null, "date_until": null, "yearly": true },
{ "date": null, "date_from": "2024-07-15", "date_until": "2024-07-20", "yearly": false }
]
}