Store Hours – Multi-Store Implementation
This guide documents the multi-store store-hours implementation: how data is shaped, how the API works, and how callers use it.
Legacy (pre–multi-store)
Data flow (legacy)
- Source:
storeHourswas built inscripts.template.htmlfrom:[@CONFIG: Operating_Hours@](injected as object body)[@CONFIG: Operating_Exceptions@](injected as array body)
- Shape: single store with
storeHours.openingHoursandstoreHours.closing_exceptions(array). - File:
src/js/storeHours.js– all logic used that single-store shape.
Legacy API (storeHours.js)
| Function | Purpose | Relies on |
|---|---|---|
timeStringToMinutes(timeStr) | ”HH:MM ” → minutes since midnight | — |
getCurrentAEST() | Current time in Australia/Sydney | — |
formatDateDDMMYYYY(date) | Date → “dd/mm/yyyy” | — |
isClosingException(currentDateStr) | Is date in closing_exceptions? | storeHours.closing_exceptions (array), currentDateStr in dd/mm/yyyy |
isStoreOpen() | Is “the” store open now? | storeHours.openingHours (weekdays/saturday/sunday) |
Legacy callers
| Location | Usage | Store context |
|---|---|---|
contactModal.template.html | isStoreOpen() === true → enable call/chat, set “open”/“Live & Ready” | None – single global “open” state |
storestock.template.html | Lists Kotara, Maitland, Artarmon, Burwood | Per-store rows, but no store-hours calls |
structured_data.template.html | Per-location openingHoursSpecification | Hardcoded per store, not from storeHours |
Data shape (implemented)
The runtime object uses store ids as keys and a shared closing_exceptions object. Store ids match stable ids used in templates (e.g. "kotara", "maitland", "artarmon", "burwood").
Config: Operating Hours
[@CONFIG: Operating_Hours@] provides per-store entries, for example:
"kotara": { "store_name": "Altapac Kotara", "store_address": "94 Park Ave, Kotara NSW 2289", "store_phone": "+61 2 4941 4444", "store_email": "hello@altapac.com", "openingHours": { "weekdays": { "open": "08:00", "close": "17:00" }, "saturday": { "open": "08:00", "close": "14:00" }, "sunday": { "closed": true } } }, "maitland": { /* ... */ }, "artarmon": { /* ... */ }, "burwood": { /* ... */ }Config: Closing Exceptions
[@CONFIG: Operating_Exceptions@] provides date → reason entries (YYYY-MM-DD keys):
"2026-04-03": "Good Friday", "2026-04-04": "Easter Saturday", "2026-04-05": "Easter Sunday", "2026-04-06": "Easter Monday", "2026-04-25": "Anzac Day", "2026-06-08": "King's Birthday", "2026-08-03": "Bank Holiday", "2026-10-05": "Labour Day", "2026-12-25": "Christmas Day", "2026-12-26": "Boxing Day", "2026-12-28": "Additional Day", "2027-01-01": "New Year's Day", "2027-01-26": "Australia Day", "2027-03-26": "Good Friday", "2027-03-27": "Easter Saturday", "2027-03-28": "Easter Sunday", "2027-03-29": "Easter Monday", "2027-04-25": "Anzac Day", "2027-06-14": "King's Birthday", "2027-08-02": "Bank Holiday", "2027-10-04": "Labour Day", "2027-12-25": "Christmas Day", "2027-12-26": "Boxing Day", "2027-12-27": "Additional Day", "2027-12-28": "Additional Day"Resolves to
const storeHours = { "kotara": { "store_name": "Altapac Kotara", "store_address": "94 Park Ave, Kotara NSW 2289", "store_phone": "+61 2 4941 4444", "store_email": "hello@altapac.com", "openingHours": { "weekdays": { "open": "08:00", "close": "17:00" }, "saturday": { "open": "08:00", "close": "14:00" }, "sunday": { "closed": true } } }, "maitland": { /* ... */ }, "artarmon": { /* ... */ }, "burwood": { /* ... */ }, "closing_exceptions": {"2026-04-03": "Good Friday", "2026-12-25": "Christmas Day", "…"}};Conventions
- Store ids: lowercase, no spaces (e.g.
kotara,maitland). These are the values you pass asstoreIdinto the API. - closing_exceptions: object with date strings in YYYY-MM-DD as keys and the reason for the exception as the value. All stores share this list.
API (storeHours.js)
Core functions
| Function | Signature | Purpose |
|---|---|---|
timeStringToMinutes(timeStr) | (timeStr: string) => number | null | ”HH:MM ” → minutes since midnight. |
getCurrentAEST() | () => Date | Current time in Australia/Sydney. |
formatDateDDMMYYYY(date) | (date: Date) => string | Date → “dd/mm/yyyy” (for UI). |
formatDateYYYYMMDD(date) | (date: Date) => string | Date → “YYYY-MM-DD”; used for exception matching. |
isClosingException(dateStr, exceptions?) | (dateStr: string, exceptions?) => boolean | Uses root closing_exceptions (object keys) by default; optional override for tests/reuse. |
isStoreOpen(storeId?) | (storeId?: string) => boolean | See below. |
getStoreConfig(storeId) | (storeId: string) => object | null | Returns full store config (name, openingHours, etc.) or null. |
getTodayHours(storeId) | (storeId: string) => object | null | Returns { open, close } or { closed: true } for today for that store. |
getStoreIds() | () => string[] | Returns store ids (excludes closing_exceptions and reserved keys). |
isStoreOpen(storeId?) behaviour
-
isStoreOpen()(no args)
Uses the default store Kotara (store id"kotara"). The script definesDEFAULT_STORE_ID = "kotara"and uses it when no argument is passed. -
isStoreOpen(storeId)
UsesstoreHours[storeId].openingHoursand the sharedclosing_exceptions. IfstoreIdis invalid, returnsfalse.
Implementation details
Exception date format
closing_exceptionsuses YYYY-MM-DD as object keys.isClosingExceptioncompares “today” in YYYY-MM-DD (e.g.formatDateYYYYMMDD(getCurrentAEST())) against those keys.formatDateDDMMYYYYis used only where display or legacy config needs dd/mm/yyyy.
Identifying store keys
All keys that are not closing_exceptions and not reserved (e.g. keys starting with _) are store ids. getStoreIds() filters them as:
Object.keys(storeHours).filter( (k) => k !== "closing_exceptions" && !String(k).startsWith("_"));Default store
The default store for isStoreOpen() is Kotara, set via var DEFAULT_STORE_ID = "kotara" and documented in the JSDoc in storeHours.js.
Call sites
| Caller | Usage |
|---|---|
| contactModal.template.html | Uses isStoreOpen() (default store Kotara) to enable call/chat and set “open”/“Live & Ready”. Can call isStoreOpen(storeId) with a store id (e.g. from URL hash or a data attribute) for location-aware behaviour. |
| storestock.template.html | Can show “Open / Closed” per row via isStoreOpen('kotara'), isStoreOpen('maitland'), etc., and/or getTodayHours(storeId) for “Opens 08:00” / “Closed today”. |
| structured_data.template.html | Per-location openingHoursSpecification remains hardcoded per store; a future refactor could use getStoreConfig(storeId).openingHours as the source of truth. |
Changes from legacy
| Change | Implemented behaviour |
|---|---|
| Data shape | Config exposes one object per store and a root closing_exceptions object (YYYY-MM-DD keys, reason values). Legacy single-store shape (storeHours.openingHours at top level) is supported when present for backward compatibility. |
isStoreOpen() | No-arg form uses the default store (Kotara). Passing a store id uses that store’s openingHours and the shared closing_exceptions. |
isClosingException | Uses YYYY-MM-DD; “today” is compared against closing_exceptions object keys via formatDateYYYYMMDD(getCurrentAEST()). |
storeHours global | Same name; structure is nested stores + shared closing_exceptions. New code should use getStoreConfig, getStoreIds, and isStoreOpen(storeId) rather than reading storeHours.openingHours directly. |
Summary
- Implemented: Multiple stores with per-store opening hours, one shared
closing_exceptionsobject (YYYY-MM-DD keys), and a small, reusable API instoreHours.js. - Data: One
storeHoursobject with store ids as keys and a rootclosing_exceptionsobject; exception dates use YYYY-MM-DD. - API:
getStoreIds,getStoreConfig,getTodayHours,formatDateYYYYMMDD;isStoreOpen(storeId?)with default store Kotara; exception checking uses YYYY-MM-DD. Legacy single-store config is supported when present.