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.
This commit is contained in:
Padreug 2026-05-02 09:34:07 +02:00
commit 42a8b08a5b
11 changed files with 1015 additions and 0 deletions

108
docs/order-flow.md Normal file
View file

@ -0,0 +1,108 @@
# Order flow
From "customer adds to cart" to "ticket prints in the kitchen", in
one restaurant's slice. The customer-side aggregation across
multiple restaurants is in [[webapp-integration]].
## State machine
```
pending ──pay──▶ paid ──accept──▶ accepted ──ready──▶ ready ──serve──▶ completed
│ │ │
└─cancel────────────┴──────────────────┴─▶ canceled
└─refund────────────────────────────────▶ refunded
```
`pending → paid` is the **only** transition driven by money. All
others are explicit calls to `PUT /api/v1/orders/{id}/status/{new}`
from the [[cms]].
States and their meaning:
| State | Set by | Meaning |
|---|---|---|
| `pending` | `services.place_order` | Invoice issued, not yet paid |
| `paid` | invoice listener | LNbits payment settled (or cash recorded) |
| `accepted` | operator (or auto-accept) | Restaurant has acknowledged, prep in progress |
| `ready` | operator | Pickup-ready / served |
| `completed` | operator | Finished |
| `canceled` | operator | Pre- or post-payment cancel |
| `refunded` | operator | Paid → refunded |
## Place order
`services.place_order(CreateOrder)`:
1. Resolves the restaurant; rejects if `is_open=False`.
2. Re-prices every line item against the live menu (modifier ids
are matched server-side; the customer's claimed `price_delta`
values are ignored).
3. Sums `subtotal_msat`, applies `tax_rate`, adds `tip_msat`
`total_msat`.
4. For Lightning / internal: calls
`lnbits.core.services.create_invoice` with
`extra={"tag": "restaurant", "restaurant_id": ...}`.
5. Persists the order with `id = payment_hash` so the listener can
look it up cheaply, plus one `order_items` row per line.
6. For cash: `payment_method = "cash"` skips invoice creation and
marks the order `accepted` directly.
Returns `(Order, OrderInvoice | None)`. The webapp pays the bolt11.
## Pre-flight quote
`POST /api/v1/orders/quote` returns `{"required_msat": <int>}` for a
hypothetical cart. The webapp sums the quotes from each restaurant
**before** opening any per-restaurant invoice — so a customer with
insufficient balance sees one error rather than partially-paid carts
across N restaurants.
## Settlement
`tasks.wait_for_paid_invoices` registers an `asyncio.Queue` listener
on LNbits' global payment stream. On each payment:
```python
if payment.extra.get("tag") != "restaurant":
return
order = await get_order_by_payment_hash(payment.payment_hash)
if order:
await mark_order_paid(order.id)
```
`services.mark_order_paid` is idempotent. It:
1. Sets `order.status``"paid"` (or `"accepted"` if
`settings.auto_accept_orders`).
2. Decrements `menu_item.stock` for each line, clamped at 0.
3. Creates a `print_jobs` row.
Stock decrements happen at settlement, not at order placement —
unpaid orders don't lock inventory.
## Print pipeline
`print_jobs` is a queue. `printer-pi` (a separate process / device
running outside this extension) picks up jobs via `GET /restaurants/
{id}/print_jobs?status=queued`, prints, and acknowledges via
`PUT /print_jobs/{id}/ack`. Status flow:
`queued → sent → acknowledged`, or `failed` with a `last_error`.
The roadmap calls for a Nostr-based push: `printer-pi` subscribes to
the restaurant's pubkey for an `order.confirmed` event, removing the
poll loop.
## Multi-restaurant carts
A single customer cart spanning multiple restaurants is handled by
the [[webapp-integration|webapp]], not here. Each restaurant's
extension instance only ever sees its own slice — its own order, its
own invoice, its own print job. There is no umbrella order on the
server side.
## See also
- [[architecture]]
- [[data-model]] — `orders`, `order_items`, `print_jobs`
- [[webapp-integration]] — multi-restaurant aggregation
- [[nostr-layer]] — outbound status DMs (NIP-17, scaffolded)