restaurant/docs/webapp-integration.md
Padreug 6dae57f3f4 feat(api): public GET /restaurants/by-slug/{slug}
Prerequisite for the customer webapp module (aiolabs/webapp,
branch feat/restaurant-bundle): the webapp's /r/:slug route needs
to resolve a slug to a Restaurant payload without an admin key.

crud.get_restaurant_by_slug already exists (used by the server-
rendered CMS routes in views.py); just expose it as a public REST
endpoint. Mirrors api_get_restaurant by id and is declared before
the bare-id route so the static prefix wins FastAPI's path match.

Verified live against seeded 'Big Jay's Bustaurant':
  GET /restaurant/api/v1/restaurants/by-slug/big-jays-bustaurant
  -> 200 with the Restaurant payload.
2026-05-11 19:17:35 +02:00

137 lines
4.4 KiB
Markdown

# 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.
For the **single-venue** case, a webapp that routes on a URL slug
(`/r/big-jays-bustaurant`) resolves the slug → restaurant via the
public `GET /restaurants/by-slug/{slug}` endpoint
([[api-reference]]) and proceeds with that one `id` for menu reads
and order placement. Slug is just a URL nicety — internally
everything continues to key on the restaurant `id`.
For the **aggregator** case (multiple restaurants in one cart), the
webapp consumes a curated NIP-51 list event:
```
{
"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]]