Skip to main content
Version: Latest
Technical Manual

Business Hours

Business Hours is the one place a module asks "are we open?". Queues, entry points and the dialer, automated SMS sends, the Omnichannel load editor and work-order service levels all read hours through it, so a holiday entered once closes every one of them.

It has no feature of its own. It is two libraries:

ProjectHolds
CrestApps.OrchardCore.BusinessHours.AbstractionsHoursSchedule, HoursWindow, HoursDateException, HoursScheduleOption, IOpenHoursService, IHoursScheduleSource, AddHoursScheduleSource<T>() and the pure HoursScheduleEvaluator
CrestApps.OrchardCore.BusinessHours.CoreDefaultOpenHoursService and AddOpenHours()

The schedules themselves are kept by the features that own them. Today that is the Contact Center Business Hours feature (CrestApps.OrchardCore.ContactCenter.BusinessHours), whose business-hours calendars are a schedule source. See Contact Center configuration.

The schedule​

public sealed class HoursSchedule
{
public string TimeZoneId { get; set; } // IANA or Windows id; empty means UTC
public IList<HoursWindow> Windows { get; set; } // { Day, OpenMinute, CloseMinute }
public IList<HoursDateException> DateExceptions { get; set; } // { Date, Name, Closed, OpenMinute, CloseMinute }
}
  • A day can have several windows (a lunch break) or none (closed).
  • A window that opens and closes at the same minute is open all day.
  • A window that closes before it opens runs past midnight: a Friday window from 22:00 to 06:00 is open Friday night and early Saturday.
  • A date exception owns its whole local date. Closed = true (the default) is a holiday; otherwise the date is open only from OpenMinute to CloseMinute. The early hours of an overnight window that run into a holiday are closed.
  • Wall-clock times convert with CalendarTime: a time a daylight-saving change skips moves forward by the gap, and a time that happens twice is the earlier of the two.

Asking whether something is open​

Inject IOpenHoursService:

MethodReturns
IsOpenAsync(scheduleId, utcInstant, timeZoneId?)true/false, or null when the id is empty or no source knows the schedule (or it is disabled). Treat null as open, so a missing schedule never blocks live work.
GetOpenWindowsAsync(scheduleId, fromUtc, toUtc, timeZoneId?)The open TimeWindows, merged and clipped to the range, or null for an unknown schedule.
ListSchedulesAsync()Every source's schedules, each once, by name, for an editor picker.

timeZoneId reads the schedule in another zone than its own, such as the contact's local time.

A feature that asks registers the service with services.AddOpenHours() (it registers once, whichever feature asks first).

Keeping schedules​

A feature that stores schedules implements IHoursScheduleSource (FindAsync(id) and ListAsync()) and registers it:

services
.AddOpenHours()
.AddHoursScheduleSource<MyHoursScheduleSource>();

The first source that knows an id answers for it. A module that holds a schedule in memory can evaluate it without the service through HoursScheduleEvaluator.IsOpen and HoursScheduleEvaluator.GetOpenWindows.

Consumers​

ConsumerHow it reads hours
Contact Center queues, entry points, dialer and manual screeningIBusinessHoursService, which answers through IOpenHoursService
Omnichannel automated sends and the load editorIBusinessHoursGate (OpenHoursBusinessHoursGate), registered by Omnichannel Activities; always open when no source knows the calendar
Work-order service levelsOpenHoursServiceHoursProvider, an IServiceHoursProvider registered by the Work Orders module; a policy's weekly hours are evaluated by HoursScheduleEvaluator too

The SMS automation feature no longer depends on the Contact Center Business Hours feature: it uses the gate that Omnichannel Activities registers, and the calendars appear in its pickers when the Business Hours feature is enabled.