Skip to content

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 yearly flag 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_dates field 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_dates is in a location's uneditable_fields, the inputs are disabled.

Location

  • Backoffice Route: /settings/locations/:id (route name settings-locations-id); Restricted Dates is a step within the location form, not its own route
  • Backend Model: app/RawModels/Location.php (field restricted_dates)
  • Backend Request: app/Http/Requests/BackOffice/Settings/Location/StoreLocationRequest.php
  • Vue Component: src/views/settings/Locations/forms/LocationRestrictedDates.vue (rendered from src/views/settings/Locations/LocationForm.vue)

Fields

Each entry in the restricted_dates array has the following fields.

Date (single-date mode)

PropertyValue
Field IDdate
LabelSelect Date
TypeDate (Y-m-d)
RequiredRequired when no date_from/date_until is provided
Validationnullable, 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)

PropertyValue
Field IDdate_from
LabelFrom
TypeDate (Y-m-d)
RequiredRequired when date_until is provided
Validationnullable, date_format:Y-m-d, required_with:date_until

Description: Start date of a closed range.


Date Until (range mode)

PropertyValue
Field IDdate_until
LabelUntil
TypeDate (Y-m-d)
RequiredRequired when date_from is provided
Validationnullable, 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

PropertyValue
Field IDyearly
LabelRepeats every year
TypeBoolean (checkbox)
RequiredYes (always sent for each entry)
Defaultfalse (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)

PropertyValue
Field IDoriginal_date
TypeDate (Y-m-d)
Set bySystem 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 closed toggle, special_hours / from / to modified-hours, a reason text field, recurring, affects_online, or affects_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 check

A 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

  1. Restricted dates (exact-date match → unavailable)
  2. Same-day / next-day order cutoffs
  3. 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:

StateConditionWhat the customer sees
No orderable date at allNeither 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 offThe "Choose" date/time prompt is replaced by the closed banner "Not accepting orders"
Today unavailable, a later day is availableToday is restricted (or a non-open weekday) but the future-days window reaches an open dayThe 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-offThe same-day or next-day cut-off for the selected option has passedAn 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.

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-d string comparison against the entry's date, evaluated in the location's timezone (Location::checkForBusinessHours()).
  • The location:update-restricted-dates Artisan command is scheduled yearly (once a year, at the start of January), not daily. For each entry with yearly: true whose date is already in the past (in the location timezone), it copies the old value into original_date and updates date to the same month/day in the current year. An entry with no date is skipped untouched.
  • Single-date and date-range entries are mutually exclusive per row: providing date_from/date_until requires both, and date_until must be on or after date_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 on restricted_dates.{index}.yearly 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."
  • Dates are validated with date_format:Y-m-d in StoreLocationRequest. The restricted_dates field itself is nullable|array and 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 in original_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 checkForBusinessHours for 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 }
  ]
}