# Domain model (draft v0.1) Single-tenant: one deployment per client, so no tenant ids anywhere (see `instance-and-branding.md`). Conventions for all entities: - IDs: ULID (sortable, safe to expose). Public order tracking uses a separate random token. - Money: integer **minor units** (pence) + `currency` (GBP). Never floats. - Time: UTC in DB, each `Location` has a `timezone` (Europe/London) for opening hours and slots. - Soft delete (`archived_at`) for catalogue + people; orders are never deleted. - Every write through the API emits a domain event → **audit log** (see `identity-permissions-audit.md`). - Translatable text (`name`, `description`) stored as JSON `{ "en": "…" }` — English only at launch. ## Modules | Module | Doc | Entities | |---|---|---| | Locations | `locations-and-menu.md` | Location, LocationService, OpeningHours, SpecialHours | | Catalogue | `locations-and-menu.md` | Menu, Category, Product, ModifierGroup, Modifier, Allergen, DietaryTag, TaxCategory, AvailabilityOverride | | Ordering | `orders-and-payments.md` | Basket, Order, OrderLine, OrderStatusChange, Payment, Refund | | Identity | `identity-permissions-audit.md` | Customer, SocialAccount, StaffUser, Role, Permission, RoleAssignment | | Audit | `identity-permissions-audit.md` | AuditEvent | | Integrations & devices | `integrations-and-devices.md` | DispatchChannel, OrderDispatch, IntegrationEvent, NotificationLog, Device | | Loyalty | `loyalty.md` | LoyaltyProgram, LoyaltyAccount, LoyaltyLedgerEntry, Reward, Tier, RewardRedemption | | Instance & branding | `instance-and-branding.md` | InstanceSettings, Branding, ContentBlock | ## ERD (core) ```mermaid erDiagram LOCATION ||--o{ LOCATION_SERVICE : offers LOCATION ||--o{ OPENING_HOURS : has LOCATION ||--o{ SPECIAL_HOURS : has LOCATION }o--o{ MENU : "serves (menu_location)" MENU ||--o{ CATEGORY : contains CATEGORY }o--o{ PRODUCT : "lists (category_product)" PRODUCT }o--o{ MODIFIER_GROUP : "uses (product_modifier_group)" MODIFIER_GROUP ||--o{ MODIFIER : contains MODIFIER }o--o{ MODIFIER_GROUP : "nests (modifier_modifier_group)" MODIFIER }o--o| PRODUCT : "may reference" PRODUCT }o--o{ ALLERGEN : contains PRODUCT }o--o{ DIETARY_TAG : tagged PRODUCT }o--|| TAX_CATEGORY : taxed LOCATION ||--o{ AVAILABILITY_OVERRIDE : "per-store price/snooze" CUSTOMER ||--o{ SOCIAL_ACCOUNT : "signs in with" CUSTOMER ||--o{ ORDER : places LOCATION ||--o{ ORDER : fulfils DEVICE ||--o{ ORDER : "kiosk source" ORDER ||--o{ ORDER_LINE : has ORDER_LINE ||--o{ ORDER_LINE : "children (modifiers)" ORDER ||--o{ ORDER_STATUS_CHANGE : history ORDER ||--o{ PAYMENT : paid_by PAYMENT ||--o{ REFUND : refunded_by ORDER ||--o{ ORDER_DISPATCH : "sent via" LOCATION ||--o{ DISPATCH_CHANNEL : configures DISPATCH_CHANNEL ||--o{ ORDER_DISPATCH : used_by LOCATION ||--o{ DEVICE : hosts STAFF_USER ||--o{ ROLE_ASSIGNMENT : has ROLE ||--o{ ROLE_ASSIGNMENT : granted_as ROLE }o--o{ PERMISSION : bundles LOCATION ||--o{ ROLE_ASSIGNMENT : "scope (nullable = global)" ``` `AuditEvent` is polymorphic (actor + subject) and deliberately not drawn — it references everything. ## Where Storekit data maps | Storekit | Ours | |---|---| | venue (per store, + separate "Catering" venue) | `Location` + `LocationService(type=catering)` — one location, many services | | category | `Category` | | product (`price` pence, `deliverectId`, `plu`) | `Product` (`price`, `external_refs.stream`, `sku`) | | modifierGroup (`min`, `max`, `isRemoval`) | `ModifierGroup` (`min_select`, `max_select`, `kind=removal`) | | allergens/dietary inside description text | structured `Allergen` / `DietaryTag` relations |