feat(restaurant): customer-facing restaurant bundle (v1) #54
No reviewers
Labels
No labels
app:activities
app:chat
app:events
app:forum
app:libra
app:market
app:restaurant
app:tasks
app:wallet
app:webapp
bug
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiolabs/webapp!54
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/restaurant-bundle"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
New standalone bundle that gives customers a place to browse a restaurant's menu, build a cart, and pay an order over Lightning. Single-venue MVP, URL-driven (
/r/:slug); the cart store keys byrestaurant_idfrom day one so the future festival aggregator (aiolabs/restaurant#8) is purely a discovery-layer change.REST-only order placement; NIP-99 menu state arrives as a live Nostr overlay. NIP-17 transport for order intake is deferred to
aiolabs/restaurant#9.Companion extension change is in
aiolabs/restaurant#feat/restaurant-by-slug(3 commits: by-slug endpoint + fiat→sat conversion + KDS dark-mode contrast).Commits
Nine commits, each
vue-tsc -bclean individually:41fbad3— bundle skeleton (vite.restaurant.config.ts,restaurant.html,src/restaurant-app/*, DI tokens, scripts)1cdf87b— typedRestaurantAPIREST client + types translated from extensionmodels.py3a11d90— menu browse views (Home, RestaurantPage, ItemPage)27d98ce— cart store + cart page + add-to-cart wiring940b36b— checkout + order placement + status pollinga7f2ded— orders list + settingse01e595— Nostr live overlay (NIP-99) for menu state77c81d8— UX polish: currency display, two-phase checkout w/ QR + copy, friendly status labels3131268— orders-list polish: live status badge + fiat amount + manual refresh FABFuture-compat scaffolding baked in (no UI yet)
OrderStatustype +KNOWN_ORDER_STATUSESconst foraiolabs/restaurant#4(kitchen workflow)MenuItem.extra+Restaurant.modepass-through for#2/#3/#5/#6useCheckoutbuildsCreateOrderthrough a singlebuildCreateOrder()helper so loyalty (#5) and NIP-17 (#9) inject in one placefeatures: {}feature-flag map reserved for tier-gated UI (#2)restaurant_idso the festival aggregator (#8) is a discovery-layer changeTest plan
feat/restaurant-bundlehere andfeat/restaurant-by-slugonaiolabs/restauranthttp://localhost:5001with restaurant extension enabled and Big Jay's Bustaurant seededVITE_LNBITS_BASE_URL=http://localhost:5001 VITE_RESTAURANT_DEFAULT_SLUG=big-jays-bustaurant npm run dev:restaurant/redirects to/r/big-jays-bustaurant, menu loads (REST),[RestaurantNostrSync] subscribedin console/cartshows lines in menu currency (GTQ), totals correct/checkoutshows live≈ X satpreview from/orders/quotepending → paidautomatically/orders/:idshows "Cooking" within ~5s/orderslist shows live status badge per row + fiat amount; FAB refresh re-fetches/r/big-jays-bustauranttab updates within ~1s (Nostr overlay)npm run build:restaurant→ clean (verified locally: 2422 modules, 3s, no errors)Verified against Big Jay's Bustaurant
All sat amounts come from the extension's fiat-rate-aware
/orders/quoteendpoint (the companion fix onaiolabs/restaurant).Out of scope (tracked elsewhere)
aiolabs/restaurant#8aiolabs/restaurant#9🤖 Generated with Claude Code
types/restaurant.ts — full set of TS interfaces hand-translated from ~/dev/shared/extensions/restaurant/models.py. Key notes: - Money is integer msat end-to-end on orders, order items, and the order quote response (matches the extension). - Open OrderStatus type with KNOWN_ORDER_STATUSES const so the production / kitchen workflow (aiolabs/restaurant#4) can introduce new states without breaking the build. - MenuItem.extra carries forward-compatible metadata for inventory (#3), happy-hour / COGS (#6), loyalty (#5), and mode-gated badges (#2). Plain Record<string, unknown>. - OrderExtra.fields is the loyalty (#5) pass-through hook the useCheckout buildCreateOrder helper will inject through. - Restaurant.mode is acknowledged but not branched on in v1. services/RestaurantAPI.ts — BaseService subclass, mirrors the extension's REST surface: getRestaurantBySlug / getRestaurantById / getMenu / getMenuItem quoteOrder / placeOrder / getOrder No API key for any of these — public read and customer-facing write endpoints. Base URL pulled from appConfig.modules.restaurant.config.apiBaseUrl. modules/restaurant/index.ts — install() now constructs the API client, registers it under SERVICE_TOKENS.RESTAURANT_API, and kicks off .initialize(). Consumers (views, composables, stores) get the client via injectService starting in commit 4.End-to-end menu browse for one restaurant. composables/useMenu(slugOrId) — fetches via REST. Resolves slug or id via heuristic, calls getMenu(), exposes {restaurant, tree, items, isLoading, error, refresh} as reactive refs. Cancels in-flight requests on param change /scope dispose. components: RestaurantHeader.vue — banner, logo, name, description, open badge, currency badge, location. CategoryNav.vue — sticky horizontal pill nav over root menu nodes; scrolls to anchors. MenuTree.vue — recursive renderer (self-references by name). Renders a node's items first, then its children — items can attach to any node per the menu-tree refactor. MenuItemCard.vue — image, name, price (msat-native via currencyHint), sold-out / low-stock / featured badges, dietary + allergen chips, '+' button that opens ItemPage or quick-adds when no modifier groups. ModifierSelector.vue — radio (selection='one') / checkbox (selection='many') with min/max enforcement. v-model-style emits (update:selected, update:valid). Seeds from is_default modifiers when no existing selection is passed. views: HomePage.vue — slug input + auto-redirect when VITE_RESTAURANT_DEFAULT_SLUG is set. RestaurantPage.vue — composite: header + CategoryNav + MenuTree. Loading / error states via shadcn Alert. ItemPage.vue — full item detail: image, dietary + allergen chips, ModifierSelector, note textarea, sticky bottom bar with qty stepper + 'Add to cart' CTA (disabled for v1; cart wires in commit 5). Routes registered on the module: /, /r/:slug, /r/:slug/item/:itemId. Design: shadcn-vue components throughout (Alert, Badge, Button, Card, Checkbox, Input, Label, RadioGroup, Textarea), Tailwind 4 utility classes, theme-aware semantic colors (text-foreground, bg-background, bg-card, text-muted-foreground, bg-primary, etc.). No raw hex or theme-blind classes. Verified: vue-tsc -b clean against the whole webapp.End-to-end customer order flow against the restaurant extension. composables/useOrder(orderId) — polls GET /orders/{id} every orderPollMs (5s default) while status is non-terminal. Refetches immediately on VisibilityService.onVisible so a backgrounded tab catches up on resume. Cleans the interval on scope dispose. KNOWN_ORDER_STATUSES is the closed list; the type stays open so new statuses from aiolabs/restaurant#4 land without breaking. composables/useCheckout() — orchestrates the full flow: 1. quoteOrder per restaurant in the cart 2. pre-flight balance check (wallet.balance.value, sat -> msat) 3. placeOrder per restaurant -> { order, invoice } 4. WalletService.sendPayment(bolt11) per invoice 5. clearRestaurant(rid) on success buildCreateOrder is the single point CreateOrder is constructed; loyalty (aiolabs/restaurant#5) and NIP-17 transport (#9) both plug in here without touching the rest of the flow. components/OrderInvoiceCard.vue — bolt11 QR via qrcode lib, copy-to-clipboard, expires-in countdown. White-bg QR for scanner contrast regardless of theme (pure UX call — humans don't read QRs, cameras do). views/CheckoutPage.vue — review + total + 'Pay & place order' CTA. Progress indicator shows current restaurant + N of M during the multi-restaurant loop. Empty-cart guard redirects to /cart. On success, stores placed orders in 'restaurant.lastOrders.v1' (capped at 50, newest first) and navigates to /orders/<firstId>. views/OrderStatusPage.vue — status pill with semantic Badge variants, conditional bolt11 QR when status='pending', success alert when paid/accepted/ready, line items with modifier summary, timeline of transitions, money breakdown (subtotal / tax / tip / total). Polls live via useOrder. Routes added: /checkout, /orders/:id. Money convention: cart.unit_msat stores per-unit values directly from MenuItem.price (declared currency, not msat). The extension itself msat-ifies amounts on POST /orders/quote and /orders. The checkout's pre-flight balance check converts wallet sats -> msat before comparing to the quote's required_msat. Display strings divide by 1000 only when reading order.*_msat fields back from the extension. Design: shadcn-vue throughout (Alert, Badge, Button, Card, Separator) + Tailwind 4 + theme-aware semantic classes (bg-card, text-foreground, text-muted-foreground, text-primary, text-destructive, border-border, with the one exception of the QR card's white background, justified inline). Verified: vue-tsc -b clean.views/OrdersListPage.vue — grouped by day, source of truth is STORAGE_SERVICE['restaurant.lastOrders.v1'] (newest first, cap 50). Each row deep-links to /orders/:id where the detail page re-fetches the live order over REST so stale status never displays here. views/SettingsPage.vue — customer-side preferences persisted to STORAGE_SERVICE['restaurant.settings.v1']: - currencyDisplay toggle ('sats' | 'msat'). Local display only, the extension is always msat-canonical. - relayOverride input (comma-separated). Reload required since RelayHub initializes once on boot. - 'Clear local data' destructive button — wipes cart, history, recent venues but does NOT refund/cancel placed orders. Routes added: /orders, /settings. Verified: vue-tsc -b clean against the whole webapp.Three v1 smoke-test follow-ups that all touch CheckoutPage.vue, bundled rather than scattered across the planned commits: stores/cart.ts + CartLineItem.vue + CartPage.vue: - rename CartLine.unit_msat → unit_price (the field never was in msat — it carried the menu-item's declared currency) - add CartLine.currency snapshot; getters now return { amount, currency } shapes - grandTotal returns null for multi-currency carts (future festival aggregator); UI falls back to per-bucket subtotals views/CheckoutPage.vue: - same display rename throughout - live ≈sat preview via /orders/quote on cart change - two-phase flow: review → place → render bolt11 QR(s) + copy button → pay all (LNbits wallet) OR scan with external wallet - per-placed-order poller picks up external-wallet payments views/OrderStatusPage.vue + CheckoutPage.vue + types/restaurant.ts: - customer-friendly labels via FRIENDLY_ORDER_STATUS map ('Order received' / 'Cooking' / 'Ready for pickup' / 'Served') - open OrderStatus type with KNOWN_ORDER_STATUSES const for UI hint mapping; unknown statuses fall through gracefully Verified end-to-end against Big Jay's: GTQ-priced items display in GTQ throughout cart + checkout with live sat preview, bolt11 QR scannable by external wallets, status transitions visible without page reload. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> feat(checkout): two-phase flow with QR + copy + external-wallet support The previous all-in-one 'Pay & place order' button placed the orders AND immediately auto-paid from the LNbits wallet, so the bolt11 QR never rendered. Customers couldn't scan with their own phone wallet (Phoenix, Wallet of Satoshi, etc.) — they were stuck on the LNbits anon wallet by default. Split into two distinct phases: useCheckout (refactor): - state.step: 'idle' → 'quoting' → 'placing' → 'placed' → 'paying' → 'paid' (or 'error') - state.placedOrders: PlacedOrder[] survives across the two phases, exposing each restaurant's { order, invoice } - state.paidOrderIds: Set<string> tracks which orders the customer auto-paid this session (external scans aren't in this set; the CheckoutPage poller tracks those) - placeOrders() — runs quote, balance precheck (warns only, doesn't block — the customer might pay externally), places orders, populates placedOrders - payOrder(idx) — pays one bolt11 via POST /api/v1/payments with the customer's wallets[0].adminkey - payAll() — convenience: payOrder for each unpaid placed order - reset() — clears state back to idle CheckoutPage (rewrite): Phase 1 (review): cart subtotal in menu currency + live ≈sat preview + 'Place order' CTA. Unchanged from before except the CTA no longer also pays. Phase 2 (pay): OrderInvoiceCard per placed order showing the QR, amount, copy button, and expiry countdown. 'Pay from my LNbits wallet' CTA wraps payAll(). The page also polls every 3s — when the extension's invoice listener flips an order to 'paid' (regardless of which wallet paid it — LNbits anon auto-pay OR external scan), the badge flips, the cart bucket for that restaurant clears, and once all placed orders are paid, we redirect to /orders/<first-id> after a 1.2s success splash. Errors from auto-pay don't kill the flow — the QR stays visible so the customer can fall back to an external wallet scan. This matches the typical restaurant UX: 'here's your bill, scan or auto-pay' rather than 'we charged your wallet without asking'. Verified: vue-tsc -b clean. feat(restaurant): customer-friendly order status labels Order status came through to the customer as raw operational strings — 'paid', 'accepted', 'ready'. These are fine for the operator's KDS but unfriendly for the customer waiting on their food. types/restaurant.ts: + FRIENDLY_ORDER_STATUS map (status → label) pending → 'Awaiting payment' paid → 'Order received' accepted → 'Cooking' ready → 'Ready for pickup' completed → 'Served' canceled → 'Canceled' refunded → 'Refunded' + friendlyOrderStatus(status) helper. Unknown statuses (future kitchen-workflow values from aiolabs/restaurant#4 — e.g. 'preparing', 'plating', 'in_service') fall through to a titlecased version of the raw key so the build stays green and the surface stays readable. views/OrderStatusPage.vue: - Status Badge uses friendlyOrderStatus(). - Alert sections now have one per status with appropriate copy: paid → 'Order received / Payment confirmed — the kitchen will start preparing it shortly.' accepted → 'Cooking / Your food is being made.' ready → 'Ready for pickup / Pick up at the counter.' completed → 'Served / Enjoy! Thanks for ordering.' views/CheckoutPage.vue: Phase 2 status badge uses friendlyOrderStatus() so the checkout's live per-restaurant status pill matches the language on the order page. Deeper kitchen workflow (prep stations, courses, ETA, per-station status) stays on aiolabs/restaurant#4 — this commit is the cheap win that ships with the existing data model unchanged.Three follow-ups to the v1 orders-list page that emerged once the extension started transitioning orders through 'paid → accepted → ready' from the KDS: views/OrdersListPage.vue: - hydrate each entry from api.getOrder(id) on mount so the row reflects the live status (via friendlyOrderStatus) rather than the snapshot at place-time - surface the order's original fiat_amount + currency_display alongside the sat total - floating bottom-right FAB refresh button — the extension has no push channel for order status today (aiolabs/restaurant#9 will replace this with NIP-17 status DMs), so customers need an explicit way to pick up kitchen-side transitions without a full page reload. Bottom-right positioning avoids the global hub nav button at top-right. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>