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.
services.py
- place_order: validates against live menu, prices line items
authoritatively from DB (modifier ids resolved server-side, not
trusted from input), creates LNbits invoice, persists order +
items. Order id := payment_hash for zero-metadata listener
lookups.
- mark_order_paid: idempotent paid -> [accepted if auto-accept] +
stock decrement + queues a print job.
- transition_order: explicit state-machine guard for accept/ready/
complete/cancel/refund.
- quote_balance_required: pre-flight total for the webapp's
multi-restaurant balance check (per the user's requirement to
verify funds before opening any per-restaurant invoice).
tasks.py
- Single invoice listener filtered on extra.tag == 'restaurant',
looks up order by payment_hash, delegates to mark_order_paid.
Wrapped in try/except so one bad payment doesn't kill the loop.