ByteBite Docs
Architecture

Opening hours

Weekly opening windows shown on the storefront, edited in the dashboard, and used to gate pickup checkout.

Opening hours

Slice 8 stored weekly opening windows and showed them. Slice 12 (Hours → Checkout-Gate) rejects pickup checkout when the hostname restaurant is closed at now. Display stays; gating is added.

One location per restaurant. Hours hang on restaurant_id. There is no timezone column; clock times are local, the same EUR/local convention as prices.

Weekday and windows

Weekday is an integer 0–6 (Monday–Sunday, ISO weekday minus 1). There was no prior domain vocabulary for days.

Each window has opens_at / closes_at as local HH:MM. opens_at must be before closes_at (no overnight window). Several windows on the same day are allowed. Adjacent windows (14:00 close then 14:00 open) are valid. Overlapping windows on the same weekday return 400.

An empty week (no windows) is valid to save. It means closed: checkout is 409.

Open-at rule

Domain: isOpenAt(windows, now: Date) → boolean. Weekday comes from now with the same 0–6 convention. A window matches when opensAt <= clock < closesAt (open inclusive, close exclusive). Adjacent windows share the boundary instant with the later window. An empty list is closed.

Checkout in checkoutStorefrontCart takes now: Date = new Date() (same injectable-clock pattern as expirePendingOrders). Tests freeze now; they must not use wall-clock.

Table

restaurant_opening_windows

ColumnNotes
idUUID
restaurant_idFK → restaurants.id (ON DELETE restrict). Tenant key. Indexed
weekdayInteger 0–6 (Monday–Sunday)
opens_atPostgreSQL time (no time zone), public contract HH:MM
closes_atSame. Check constraint opens_at < closes_at
created_at / updated_atTimestamps

Overlap is enforced in domain/API, not as a PostgreSQL exclusion constraint.

Public API

GET /storefront/restaurant includes hours.windows for the hostname tenant.

GET /storefront/hours is the same week as a dedicated resource ({ restaurantId, windows }).

  • Unknown or inactive restaurant → 404 (Restaurant not found)
  • Empty week → 200 with windows: []
  • Query/body restaurantId is ignored
  • Cart and menu GETs stay 200 when closed

Example (Luigi, Host luigi.bytebite.test):

{
  "restaurantId": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1",
  "windows": [
    { "weekday": 0, "opensAt": "11:00", "closesAt": "14:00" },
    { "weekday": 0, "opensAt": "17:00", "closesAt": "22:00" },
    { "weekday": 4, "opensAt": "11:00", "closesAt": "14:00" },
    { "weekday": 5, "opensAt": "12:00", "closesAt": "22:00" }
  ]
}

POST /storefront/checkout is host-tenant scoped. Body restaurantId is ignored.

  • Open window → existing checkout (pending, cash, unpaid, snapshot prices)
  • Closed or empty windows → 409 (Restaurant is closed). Cart is unchanged. No order row
  • Unknown or inactive host → 404, not 409

Dashboard API

Requires a staff session. Restaurant context is the path (ADR-021), not the storefront hostname. Hours GET/PUT are otherwise unchanged.

MethodPathNotes
GET/dashboard/restaurants/:restaurantId/hoursWeek for that restaurant
PUT/dashboard/restaurants/:restaurantId/hoursAtomically replaces the week

PUT body: { "windows": [ { "weekday": 0, "opensAt": "11:00", "closesAt": "14:00" } ] }. Extra restaurantId in the body is ignored.

Unknown restaurantId → 404. Overlap / opens_at >= closes_at / malformed times → 400. Luigi writes are not visible on Mario’s GET or Mario’s storefront host. Hours have no child id in the path; a Luigi week cannot be mutated through Mario’s restaurantId (Mario GET stays Mario’s rows; unknown ids are 404).

The dashboard UI is / (http://dashboard.bytebite.test/). It uses the same restaurant switcher as Menu and Orders.

Seeds

Luigi and Mario get disjoint example windows so isolation is obvious.

  • Luigi: Mon 11:00–14:00 and 17:00–22:00, Fri 11:00–14:00, Sat 12:00–22:00
  • Mario: Tue 12:00–21:00, Sun 12:00–16:00

Seeds still create no orders. Isolation is proven by real PUTs and frozen-clock checkouts in tests, not by seed orders.

Storefront

The tenant start page shows the week when windows is non-empty, plus Open/Closed. When closed, the primary checkout CTA is disabled and copy explains that pickup is unavailable. Guests can still browse the menu and keep a cart.

Frozen-clock isolation

Tests use a frozen local now, never new Date():

ClockLuigi hostMario host
Monday 12:00200 pendingclosed 409
Monday 15:00 or Tuesday409, no order(Tuesday 13:00 Mario 200)
Luigi windows: []409unchanged
Unknown host404, not 409404, not 409
Body restaurantId of the other tenanthostname tenant onlyhostname tenant only

Order pause

restaurants.orders_paused is a separate switch. Weekly windows stay the only clock. Outside those windows checkout is still 409 Restaurant is closed, even if the pause is on. Inside the windows, a pause rejects pickup and delivery with 409 Restaurant is not taking orders and leaves the cart. The menu stays public. PUT of the weekly hours does not clear the pause. PATCH /dashboard/restaurants/:restaurantId with { ordersPaused } is 404 for an unknown restaurant.

Not in this slice

Holidays, special hours, vacation, timezone column, overnight windows, BitePay, e-mail, sound/PWA, production deploy. Dashboard hours writes already require a staff session (Staff authentication). Kitchen status: Kitchen status. SMS on accept/reject/timeout/ready: Transactional notifications.

On this page