Compare commits

..

3 commits

Author SHA1 Message Date
d61e48b3e6 fix(cms): KDS card text legible on dark mode
The age-escalation highlights (bg-amber-1 / bg-orange-1 /
bg-red-1) are very pale Quasar shades. On LNbits dark mode the
q-card inherits a near-white text color from the theme — paired
with the pale background that's white-on-cream, which is what
the user reported: the '1x Coffee' on a ready-card was barely
visible.

Pin an explicit text-grey-9 alongside each pale bg so dark text
on light background renders in both themes. The 'no highlight'
branch returns '' unchanged, so non-aged orders still use the
q-card's theme-aware default text color.
2026-05-11 18:16:39 +02:00
2294bcd0c0 fix(services): convert fiat menu prices to sat via exchange rates
Before this fix `_to_msat(item.price)` blindly did `price * 1000`,
treating any menu price as sat-denominated regardless of the item's
`currency` field. Quote and bolt11 were internally consistent but
charged ~0.1% of the real price for fiat-priced menus.

  Big Jay's seeded with GTQ:
    2× Tacos (Maíz, +Brisket, +Chicken)
    pre-fix:  170 GTQ → 170000 msat → 170 sat invoice (~$0.14)
    post-fix: 170 GTQ → 26968000 msat → 26968 sat invoice (~$22)

services.py:
  - Drop `_to_msat` in favor of a `_price_to_msat(amount, currency)`
    helper. Sat-aliased currencies ("sat", "sats", "satoshi",
    "msat", …) take the flat ×1000 path; everything else round-
    trips through lnbits.utils.exchange_rates.fiat_amount_as_satoshis
    (same pool the events extension uses).
  - Update _price_line_item: item.price AND each modifier.price_delta
    are converted using item.currency. Modifier deltas inherit the
    parent item's currency since we don't carry a per-modifier
    currency field.
  - Update quote_balance_required: same conversion via the item's
    currency.

Verified live against the seeded "Big Jay's Bustaurant":
  GTQ → sat conversion matches LNbits's bitcoin-price aggregate
  (Binance / Blockchain / Bitfinex / Bitstamp / Coinbase / yadio).
  Quote returns 26968 sats for 170 GTQ — within ~2% of expected
  rate from external sources.
2026-05-11 17:49:12 +02:00
13de28e2e1 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 09:03:10 +02:00
4 changed files with 5 additions and 44 deletions

View file

@ -15,7 +15,6 @@ the catalog.
| Method | Path | Notes | | Method | Path | Notes |
|---|---|---| |---|---|---|
| `GET` | `/restaurants/{id}` | Restaurant profile | | `GET` | `/restaurants/{id}` | Restaurant profile |
| `GET` | `/restaurants/by-slug/{slug}` | Restaurant profile by URL slug — used by webapps that route on `/r/:slug` and need to resolve to an `id` before any other lookup. 404 when no match |
| `GET` | `/restaurants/{id}/menu` | `{restaurant, tree, items}` — the canonical [[menu-tree|menu tree]] (hydrated children + items per node) plus a flat enriched items list with modifier groups + availability windows pre-joined | | `GET` | `/restaurants/{id}/menu` | `{restaurant, tree, items}` — the canonical [[menu-tree|menu tree]] (hydrated children + items per node) plus a flat enriched items list with modifier groups + availability windows pre-joined |
| `GET` | `/menu_items/{id}` | Single item | | `GET` | `/menu_items/{id}` | Single item |
| `GET` | `/menu_nodes/{id}` | Single node row | | `GET` | `/menu_nodes/{id}` | Single node row |

View file

@ -73,26 +73,6 @@ orange, `>15min` red) and offers one-tap state transitions.
Today the monitor + KDS poll every 58 s. SSE / Nostr push is on Today the monitor + KDS poll every 58 s. SSE / Nostr push is on
the roadmap. the roadmap.
### Dark-mode color discipline
Quasar's pale `bg-{color}-1` utility classes (e.g. `bg-orange-1`,
`bg-red-1`, `bg-amber-1`) pair fine with the default light theme
but render **white-on-cream** under LNbits' dark theme — the
q-card otherwise inherits the body's light text color. The KDS
cards pin a dark text class alongside every pale background so
the card stays legible regardless of theme:
```js
if (order.status === 'ready') return 'bg-amber-1 text-grey-9'
if (ageSec > 900) return 'bg-red-1 text-grey-9'
if (ageSec > 300) return 'bg-orange-1 text-grey-9'
return '' // theme-default branch keeps q-card's own text color
```
Any future surface that ages / escalates with `bg-{color}-1` must
do the same. Never assume a pale background "just works" on dark
theme.
## Settings ## Settings
`settings.html` saves restaurant fields via `settings.html` saves restaurant fields via

View file

@ -37,22 +37,14 @@ States and their meaning:
2. Re-prices every line item against the live menu (modifier ids 2. Re-prices every line item against the live menu (modifier ids
are matched server-side; the customer's claimed `price_delta` are matched server-side; the customer's claimed `price_delta`
values are ignored). values are ignored).
3. Converts each item's `price` from its declared `currency` to 3. Sums `subtotal_msat`, applies `tax_rate`, adds `tip_msat`
msat. For sat-denominated items (`currency` ∈ `{sat, sats,
satoshi}`) this is a flat `× 1000`. For fiat (`USD`, `GTQ`, …)
it calls `lnbits.utils.exchange_rates.fiat_amount_as_satoshis`
to look up the live rate, then `× 1000`. The conversion lives
in `services._price_to_msat` so the rate lookup is the same path
the quote endpoint uses — a customer's preview and the recorded
`order.total_msat` cannot drift apart between request and place.
4. Sums `subtotal_msat`, applies `tax_rate`, adds `tip_msat`
`total_msat`. `total_msat`.
5. For Lightning / internal: calls 4. For Lightning / internal: calls
`lnbits.core.services.create_invoice` with `lnbits.core.services.create_invoice` with
`extra={"tag": "restaurant", "restaurant_id": ...}`. `extra={"tag": "restaurant", "restaurant_id": ...}`.
6. Persists the order with `id = payment_hash` so the listener can 5. Persists the order with `id = payment_hash` so the listener can
look it up cheaply, plus one `order_items` row per line. look it up cheaply, plus one `order_items` row per line.
7. For cash: `payment_method = "cash"` skips invoice creation and 6. For cash: `payment_method = "cash"` skips invoice creation and
marks the order `accepted` directly. marks the order `accepted` directly.
Returns `(Order, OrderInvoice | None)`. The webapp pays the bolt11. Returns `(Order, OrderInvoice | None)`. The webapp pays the bolt11.

View file

@ -9,17 +9,7 @@ of restaurants, especially the multi-restaurant cart pattern.
A webapp can either talk to one restaurant directly or aggregate A webapp can either talk to one restaurant directly or aggregate
many. There's no central directory inside this extension — grouping many. There's no central directory inside this extension — grouping
("festival", "collective space", "food court") is **emergent** via ("festival", "collective space", "food court") is **emergent** via
NIP-51 list events curated by whoever runs the venue. 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:
``` ```
{ {