# Locations & catalogue ## Location One physical store (Brewer, City/Tower 42, Noel, St Pauls). | Field | Type | Notes | |---|---|---| | id, slug, name | | `brewer-street` | | address_line1/2, city, postcode, country | | | | lat, lng | decimal | map + "nearest store" | | phone, email | | public contact | | timezone | string | `Europe/London` | | status | enum `draft`/`live`/`hidden` | hidden = staff only | | paused_until, pause_reason | datetime?, string? | "busy mode" / temp close, set by staff or Stream | | order_number_prefix | string(2) | `BR`, `CT`, `NO`, `SP` → order `BR-042` | | hero_image, pickup_instructions | | shown on store page / order confirmation | | settings | json | free-form store config, validated by schema | ### LocationService How a location sells. One row per (location, type). Replaces Storekit's separate "Catering" venues. | Field | Notes | |---|---| | type | `collection` (web/app click & collect), `kiosk_takeaway`, `kiosk_eat_in`, `catering`, `delivery` (future) | | enabled | | | prep_time_minutes | default lead time (Storekit: 10) | | asap_enabled, asap_max_minutes | Storekit `asapThreshold` 45 | | slot_length_minutes, slot_capacity | e.g. 10 min slots, max 12 orders or N items per slot | | preorder_days, order_cutoff | catering: order by 16:00 the day before | | min_order_amount | pence | | service_charge | pence or percentage (Storekit had 75p) | ### OpeningHours / SpecialHours - `OpeningHours`: (location_id, service_type?, weekday 1–7, opens_at, closes_at). Multiple rows per day allowed (split shifts). `service_type` null = applies to all services. - `SpecialHours`: (location_id, date, closed bool, opens_at?, closes_at?, reason) — bank holidays etc. - Computed availability ("open now", next slots) is cached per location per day, invalidated on change. ## Catalogue Design choice: **one central catalogue**, per-location overrides. Eat Activ has the same menu in all 4 stores with small differences (sold out, maybe price) — editing 4 copies is how chains get drift. ### Menu A named set of categories, e.g. "All day", "Breakfast", "Catering". `id, name, description, schedule (json: weekdays + time ranges), channels (set of service types), position`. `menu_location` pivot decides which stores serve which menu. The customer sees the union of active menus for (location, service type, time). ### Category `id, menu_id, name, slug, description, image, position, schedule?` (optional narrower time window). ### Product | Field | Notes | |---|---| | id, sku | sku = our stable code, used in reports | | name, description, short_description | | | image, gallery | stadium cut-out photo preferred (design spec) | | price | pence, base price | | tax_category_id | see VAT below | | kcal, nutrition json | protein/carbs/fat/fibre per portion | | ingredients | text (for PPDS labels) | | is_popular, new_until, featured | merchandising flags | | external_refs | json `{ "stream": { "plu": "…", "id": "…" } }` — POS mapping | | status | `active` / `archived` | `category_product (category_id, product_id, position)` — a product may sit in several categories (e.g. "Popular" + "Activ Bowls"). ### ModifierGroup | Field | Notes | |---|---| | id, name, display_name | internal "CHICKEN BOWL – removal", customer-facing "Remove ingredients" | | kind | `choice` (pick one/size), `addon`, `removal` (free, negative), `swap` ("instead of") — drives UI + kitchen ticket wording | | min_select, max_select | total quantity across options (Ordit min/maxQuantity) | | min_distinct, max_distinct | distinct options (Ordit min/maxDifferent) | | max_per_option | Ordit maxEachQuantity (e.g. double chicken allowed = 2) | | free_quantity | first N selections free ("2 toppings included") | | position | | `product_modifier_group (product_id, group_id, position, min_select?, max_select?)` — groups are reusable (Storekit's shared "Add-ons" group is on nearly every product); pivot can override rules. ### Modifier `id, group_id, name, price_delta (pence, can be 0), product_id?, is_default, position, external_refs, status`. - `product_id` set → modifier *is* a product (e.g. "Salmon" add-on) and inherits allergens, stock and availability from it. Otherwise standalone. - Nesting: `modifier_modifier_group (modifier_id, group_id, position)` → "Add Salmon → How cooked?". Max depth 3 (validated) to keep UI sane. ### Allergens & dietary - `Allergen`: the **14 UK FSA allergens** (celery, cereals containing gluten, crustaceans, eggs, fish, lupin, milk, molluscs, mustard, tree nuts, peanuts, sesame, soya, sulphites). Seeded, not editable. - `product_allergen (product_id, allergen_id, level: contains | may_contain)`; same pivot for modifiers. Basket shows the **effective** allergen set of the configured item (product + chosen modifiers − removed ingredients cannot remove an allergen, only staff data can). - `DietaryTag`: vegan, vegetarian, gluten-free, dairy-free, nut-free, halal, spicy, high-protein, gut-health (brand-specific). Today all of this is free text in Storekit descriptions → structured filters are a pitch win. ### TaxCategory (UK VAT) UK VAT on food depends on **hot vs cold** and **eat-in vs takeaway** (cold takeaway food is zero-rated, eat-in and hot food standard-rated). So a product has a `tax_category` (`cold_food`, `hot_food`, `soft_drink`, `confectionery`, …) and the rate is resolved at order time from (tax_category, fulfilment type). Kiosk eat-in vs takeaway therefore changes the VAT — the reason the kiosk must ask. **[Q: confirm with their accountant]** ### AvailabilityOverride Per-location state for products and modifiers, the thing store staff touch every day. | Field | Notes | |---|---| | location_id, subject_type (`product`/`modifier`), subject_id | | | is_hidden | not sold at this store at all | | snoozed_until | sold out: 30 min / until close / until date; null = available | | snooze_source | `staff` / `stream` / `stock` | | price_override | pence, optional per-store price | Resolution order for "can I buy X at store L now?": product active → menu/category scheduled now → location service open → no override hiding/snoozing it → all required modifier groups still satisfiable.