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
127
docs/webapp-integration.md
Normal file
127
docs/webapp-integration.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Webapp integration
|
||||
|
||||
The customer-facing UI lives in `~/dev/webapp` (the AIO webapp).
|
||||
This note describes the contract between the webapp and any number
|
||||
of restaurants, especially the multi-restaurant cart pattern.
|
||||
|
||||
## Discovery
|
||||
|
||||
A webapp can either talk to one restaurant directly or aggregate
|
||||
many. There's no central directory inside this extension — grouping
|
||||
("festival", "collective space", "food court") is **emergent** via
|
||||
NIP-51 list events curated by whoever runs the venue:
|
||||
|
||||
```
|
||||
{
|
||||
"kind": 30000, // NIP-51 follow set / generic list
|
||||
"tags": [
|
||||
["d", "festival-2026"],
|
||||
["title", "Atitlan Bitcoin Festival 2026"],
|
||||
["p", "<restaurant1-pubkey>"],
|
||||
["p", "<restaurant2-pubkey>"],
|
||||
["p", "<restaurant3-pubkey>"]
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The webapp:
|
||||
|
||||
1. Resolves the list event for the festival / venue.
|
||||
2. Fans out to fetch each restaurant's profile (kind 0) and menu
|
||||
(kind 30402) over Nostr, **or** falls back to
|
||||
`GET /restaurant/api/v1/restaurants/{id}/menu` over REST.
|
||||
3. Subscribes to each restaurant pubkey for live updates.
|
||||
|
||||
No central wallet, no central database, no per-restaurant
|
||||
onboarding flow specific to "joining a festival". Restaurants
|
||||
exist; festivals are curated views.
|
||||
|
||||
## Multi-restaurant cart
|
||||
|
||||
A single customer can put items from multiple restaurants into one
|
||||
cart. On checkout, the webapp issues **one order per restaurant**;
|
||||
each restaurant returns its own bolt11; the customer pays N
|
||||
invoices. There is no payment splitter and no umbrella order on
|
||||
the server side.
|
||||
|
||||
The pattern, in pseudocode (this lives in the webapp, not the
|
||||
extension):
|
||||
|
||||
```js
|
||||
// 1. Group cart by restaurant.
|
||||
const cartByRestaurant = groupBy(cart.lines, line => line.restaurant_id)
|
||||
|
||||
// 2. Pre-flight quote per restaurant.
|
||||
const quotes = await Promise.all(
|
||||
Object.entries(cartByRestaurant).map(([rid, lines]) =>
|
||||
fetch(`/restaurant/api/v1/orders/quote`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(lines.map(l => ({
|
||||
menu_item_id: l.id,
|
||||
quantity: l.qty,
|
||||
selected_modifiers: l.modifiers
|
||||
})))
|
||||
}).then(r => r.json()).then(j => ({rid, lines, msat: j.required_msat}))
|
||||
)
|
||||
)
|
||||
|
||||
// 3. Fail fast on insufficient balance.
|
||||
const totalMsat = quotes.reduce((s, q) => s + q.msat, 0)
|
||||
if (walletBalanceMsat < totalMsat) {
|
||||
return showInsufficientBalanceError()
|
||||
}
|
||||
|
||||
// 4. Open one order per restaurant.
|
||||
const orders = []
|
||||
for (const q of quotes) {
|
||||
const res = await fetch(`/restaurant/api/v1/orders`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
restaurant_id: q.rid,
|
||||
items: q.lines.map(asCreateOrderItem),
|
||||
customer_pubkey: window.user.nostrPubkey,
|
||||
parent_order_ref: cart.id,
|
||||
tip_msat: q.tipMsat,
|
||||
payment_method: 'lightning'
|
||||
})
|
||||
}).then(r => r.json())
|
||||
orders.push(res)
|
||||
}
|
||||
|
||||
// 5. Pay each bolt11 in sequence.
|
||||
for (const o of orders) {
|
||||
await payInvoice(o.invoice.bolt11)
|
||||
}
|
||||
```
|
||||
|
||||
## Atomicity
|
||||
|
||||
Sequential per-restaurant payments are best-effort, not atomic. In
|
||||
practice — for internal LNbits transfers — payment failures are
|
||||
rare and usually transient. If one of N invoices does fail mid-flow:
|
||||
|
||||
- The successful orders are already paid; their restaurants will
|
||||
print and prepare them.
|
||||
- The failed order has issued an invoice that simply expires.
|
||||
- The customer settles the gap in person at the failing restaurant
|
||||
(cash, retry, or refund of the others).
|
||||
|
||||
The pre-flight `/orders/quote` step plus a balance check before
|
||||
opening any invoice eliminates the most common cause (insufficient
|
||||
funds). HODL invoices for true atomicity are on the roadmap and
|
||||
would replace step 4 once they ship.
|
||||
|
||||
## `parent_order_ref`
|
||||
|
||||
The webapp may pass an opaque `parent_order_ref` (e.g. its own
|
||||
cart id) when posting `/orders`. The extension stores it on the
|
||||
`orders` row but never reads it — useful only for the webapp to
|
||||
correlate its umbrella cart with the per-restaurant orders later.
|
||||
|
||||
## See also
|
||||
|
||||
- [[architecture]]
|
||||
- [[order-flow]]
|
||||
- [[api-reference]]
|
||||
- [[nostr-layer]]
|
||||
Loading…
Add table
Add a link
Reference in a new issue