restaurant/docs/data-model.md
Padreug 42a8b08a5b docs: Obsidian-style vault under docs/
Add a navigable Obsidian vault as the project's first-class
technical documentation. Notes cross-reference with [[wikilinks]];
docs/index.md is the Map of Content.

New notes:
  index.md             MOC, entry point
  architecture.md      what the extension owns vs what lives outside
  data-model.md        entity-by-entity schema reference
  menu-tree.md         the arbitrary-depth tree concept
  order-flow.md        state machine + invoice listener + print
  nostr-layer.md       kinds 0/30402/5/1059, signing, t-tags
  api-reference.md     endpoint catalog by audience
  cms.md               Vue 3 + Quasar 2 UMD conventions, q-tree
  webapp-integration.md  multi-restaurant cart pattern + atomicity
  glossary.md          domain terms

Existing notes (kept as-is):
  adr-0001-menu-tree.md  the storage choice rationale
  design-conversation.md trimmed transcript

README.md adds a Documentation section pointing at docs/index.md
with the headline note list. Each note links to ~3-5 others; the
vault forms a connected graph.

A project-level memory rule (saved outside the repo) commits us to
keeping these docs in sync as the code evolves: any commit that
materially changes schema, API, order flow, Nostr surface, CMS
conventions, or webapp integration must update the relevant note(s)
in the same commit.
2026-05-09 07:11:06 +02:00

149 lines
4.7 KiB
Markdown

# Data model
Schema-by-schema reference. The migration that creates each table is
in `migrations.py`; the pydantic shapes are in `models.py`; CRUD is
in `crud.py`.
All tables live under the Postgres schema `restaurant.` (or the
SQLite equivalent), keyed off the LNbits `Database("ext_restaurant")`
binding.
---
## `restaurants`
One row per restaurant. **One LNbits wallet can own many.**
| Column | Notes |
|---|---|
| `id` | `urlsafe_short_hash()` |
| `wallet` | LNbits wallet id — payment receiver, signing-key fallback |
| `name`, `slug` | `slug` is the URL segment in `/restaurant/<slug>` |
| `description`, `currency`, `timezone`, `location`, `geohash` | metadata |
| `logo_url`, `banner_url`, `social_links` (JSON) | profile dressing |
| `open_hours` (JSON) | weekly schedule, see [[order-flow]] |
| `is_open`, `accepts_cash`, `accepts_lightning` | runtime toggles |
| `tip_presets` (JSON int[]), `tax_rate` | money / UX hints |
| `printer_endpoint` | URL or `nostr:<pubkey>` for [[order-flow|print jobs]] |
| `nostr_pubkey`, `nostr_relays` (JSON str[]) | per-restaurant Nostr identity (optional override; defaults to the LNbits Account keypair) |
| `nostr_event_id`, `nostr_event_created_at` | last published kind-0 metadata event |
| `extra` (JSON) | free-form |
Published as a [[nostr-layer|kind-0 metadata event]] on create / update.
---
## `menu_nodes`
The [[menu-tree]] — adjacency list with materialized path. Capped
at 4 levels (`MAX_MENU_DEPTH = 3`, zero-indexed).
| Column | Notes |
|---|---|
| `id` | `urlsafe_short_hash()` |
| `restaurant_id` | FK |
| `parent_id` | self-FK, NULL = root |
| `name`, `description`, `image_url`, `sort_order` | display |
| `depth` | denormalized 0..3 — O(1) max-depth checks |
| `path` | denormalized `'rootid'` or `'rootid/childid'` — cheap subtree queries |
| `time` | created_at |
Indexes: `(restaurant_id)`, `(parent_id)`, `(path)`. See
[[adr-0001-menu-tree]] for why this shape and not a closure table.
Not published as a Nostr event itself — internal organizational
structure only. Renames trigger re-publish of every item in the
subtree (so the [[nostr-layer|ancestor `t` tag]] stays current).
---
## `menu_items`
| Column | Notes |
|---|---|
| `id`, `restaurant_id` | |
| `node_id` | nullable — orphans allowed when a node is deleted with `cascade=False` |
| `name`, `description`, `price`, `currency`, `sku` | core |
| `images`, `dietary`, `allergens`, `ingredients` (JSON arrays) | structured tags |
| `calories`, `sort_order`, `is_available`, `is_featured` | display |
| `stock`, `low_stock_threshold` | inventory; nullable = unlimited |
| `nostr_event_id`, `nostr_event_created_at` | last published kind-30402 |
| `extra` (JSON) | free-form |
Published as [[nostr-layer|NIP-99 kind 30402]] on create / update;
deleted via NIP-09 kind 5.
---
## `modifier_groups` + `modifiers`
A menu item can have multiple modifier groups. Each group has a
`kind` (`required` | `optional`) and `selection` (`one` | `many`).
Each modifier carries a `price_delta`.
This unifies "required choices" (e.g. *Choose your protein: Chicken /
Tofu*) with "optional addons" (e.g. *Extra cheese +5*) under one
schema.
---
## `availability_windows`
Per-item time-of-day availability.
| Column | Notes |
|---|---|
| `menu_item_id` | FK |
| `weekday` | 0=Mon..6=Sun, NULL = every day |
| `start_time`, `end_time` | `'HH:MM'` 24h, restaurant timezone |
Used by clients to gray out items outside their window. The extension
itself doesn't auto-disable items — it surfaces the windows in the
[[api-reference|menu response]] so the consumer decides.
---
## `orders` + `order_items`
See [[order-flow]] for the state machine and amounts (msat).
`orders.id` is set to `payment_hash` for Lightning orders, giving the
invoice listener a zero-metadata lookup path. `order_items` snapshot
price + selected modifiers at order time, so subsequent menu edits
don't rewrite history.
---
## `print_jobs`
Created when an order transitions to `paid`. `printer-pi` polls or
subscribes, prints, and acknowledges via `PUT /print_jobs/{id}/ack`.
| Column | Notes |
|---|---|
| `restaurant_id`, `order_id` | FKs |
| `status` | `queued``sent``acknowledged`, or `failed` |
| `attempts`, `last_error` | retry bookkeeping |
---
## `settings`
Single-row table (id=1) for per-instance toggles.
| Toggle | Effect |
|---|---|
| `nostr_publish_enabled` | gate for kind-0 / kind-30402 publishing |
| `nostr_orders_enabled` | enable subscription to kind-1059 DMs (NIP-17 unwrap is currently stubbed) |
| `invoice_expiry_seconds` | LNbits invoice lifetime |
| `auto_accept_orders` | `paid``accepted` automatically (see [[order-flow]]) |
---
## See also
- [[architecture]]
- [[menu-tree]]
- [[order-flow]]
- [[nostr-layer]]
- [[adr-0001-menu-tree]]