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:
parent
7f7915a041
commit
42a8b08a5b
11 changed files with 1015 additions and 0 deletions
108
docs/order-flow.md
Normal file
108
docs/order-flow.md
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue