From f2045c511d4b720b0cc7795dba2f4c13c8a1ea55 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 11 May 2026 17:57:21 +0200 Subject: [PATCH] fix(restaurant): cart displays in menu currency; checkout previews live sat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cart was storing the menu item's price under a misnamed field `unit_msat` and labeling it 'sat' in the UI — so Big Jay's 25-GTQ Coffee showed as '25 sat' in the cart with no fiat conversion. The numbers were purely cosmetic (real money math goes through POST /orders/quote server-side) but misleading. stores/cart.ts: - rename CartLine.unit_msat → unit_price (since it isn't msat) - add CartLine.currency (snapshot from the menu item) - rename getter restaurantTotalsMsat → restaurantTotals; returns { amount, currency } per restaurant - rename grandTotalMsat → grandTotal; returns single { amount, currency } when the cart is one-currency, null when it spans multiple currencies (future festival aggregator with mixed-fiat restaurants — UI then falls back to per-restaurant subtotals) components/CartLineItem.vue: uses line.currency directly instead of a currencyHint prop and a hard-coded 'sat'. views/CartPage.vue: per-bucket and grand totals use the cart's currency. When the cart spans multiple currencies, hide the grand total and show a small explanatory caption. views/CheckoutPage.vue: - same display rename throughout - **new**: live ≈sat preview. On mount and whenever the cart changes, fires one POST /orders/quote per restaurant and surfaces `required_msat / 1000` as 'Pay in sats: ≈ X sat' so the customer sees the actual Lightning amount BEFORE clicking 'Pay & place order'. views/ItemPage.vue + RestaurantPage.vue: pass currency through to cart.addLine. Verified live against Big Jay's (GTQ-priced): - Coffee menu card: '25 GTQ' (unchanged) - Add to cart, /cart shows '25 GTQ' (was '25 sat') - /checkout subtotal: '25 GTQ', preview: '≈ 3,966 sat' - Quesadillas with 3 modifier groups: subtotal '80 GTQ', preview '≈ 12,691 sat' - 2x Tacos (Maíz + Brisket + Chicken): '170 GTQ', '≈ 26,968 sat' All sat amounts come from the extension's fiat-rate-aware /orders/quote endpoint (the companion fix on aiolabs/restaurant feat/restaurant-by-slug branch). Vue-tsc clean; vite build clean. --- .../restaurant/components/CartLineItem.vue | 13 ++- src/modules/restaurant/stores/cart.ts | 55 +++++++++--- src/modules/restaurant/views/CartPage.vue | 16 ++-- src/modules/restaurant/views/CheckoutPage.vue | 83 +++++++++++++++++-- src/modules/restaurant/views/ItemPage.vue | 3 +- .../restaurant/views/RestaurantPage.vue | 3 +- 6 files changed, 136 insertions(+), 37 deletions(-) diff --git a/src/modules/restaurant/components/CartLineItem.vue b/src/modules/restaurant/components/CartLineItem.vue index 0149867..cd27b60 100644 --- a/src/modules/restaurant/components/CartLineItem.vue +++ b/src/modules/restaurant/components/CartLineItem.vue @@ -10,7 +10,6 @@ import type { CartLine } from '../stores/cart' const props = defineProps<{ line: CartLine - currencyHint?: string }>() const emit = defineEmits<{ @@ -24,14 +23,12 @@ const modifierSummary = computed(() => ) const lineTotal = computed(() => { - // unit_msat is base + modifier delta snapshot; the cart store - // currently stores it as a sat-major number (price * 1000-less - // because the extension's `price` is already in the declared - // currency, not msat — see useCheckout's buildCreateOrder for the - // canonical conversion at order-place time). + // Displayed in the menu item's currency (e.g. GTQ). Authoritative + // sat conversion happens server-side at /orders/quote — the + // checkout page surfaces that as a "≈ X sat" badge. const fmt = new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }) - const total = props.line.unit_msat * props.line.quantity - return `${fmt.format(total)} ${props.currencyHint || 'sat'}` + const total = props.line.unit_price * props.line.quantity + return `${fmt.format(total)} ${props.line.currency}` }) diff --git a/src/modules/restaurant/stores/cart.ts b/src/modules/restaurant/stores/cart.ts index 7beb1e4..dfc6839 100644 --- a/src/modules/restaurant/stores/cart.ts +++ b/src/modules/restaurant/stores/cart.ts @@ -30,8 +30,16 @@ export interface CartLine { /** Snapshot at add-time so the cart still renders if the menu * item is later edited / deleted. */ name: string - /** Base price + selected modifier deltas, in msat. */ - unit_msat: number + /** + * Base price + selected modifier deltas, **in the menu item's + * declared currency** (e.g. 25 for "25 GTQ" or 100 for "100 sat"). + * Authoritative sat conversion happens server-side via + * `POST /orders/quote`; the cart's value is for display only. + */ + unit_price: number + /** ISO-ish currency code from the menu item, e.g. "GTQ", "USD", + * "sat". Used for cart-side display labels. */ + currency: string quantity: number selected_modifiers: SelectedModifier[] note?: string | null @@ -91,19 +99,42 @@ export const useCartStore = defineStore('restaurant-cart', () => { return n }) - const restaurantTotalsMsat = computed>(() => { - const out: Record = {} + /** Per-restaurant subtotal in that restaurant's declared currency. */ + const restaurantTotals = computed< + Record + >(() => { + const out: Record = {} for (const rid of Object.keys(lines.value)) { - out[rid] = lines.value[rid].reduce( - (s, l) => s + l.unit_msat * l.quantity, - 0 - ) + const bucket = lines.value[rid] + if (!bucket.length) continue + out[rid] = { + amount: bucket.reduce( + (s, l) => s + l.unit_price * l.quantity, + 0 + ), + currency: bucket[0].currency, + } } return out }) - const grandTotalMsat = computed(() => - Object.values(restaurantTotalsMsat.value).reduce((s, v) => s + v, 0) + /** + * Single-currency cart subtotal. Returns null if the cart spans + * multiple currencies (e.g. one restaurant priced in GTQ + another + * in USD via the future festival aggregator) — the UI then falls + * back to per-restaurant subtotals only. + */ + const grandTotal = computed<{ amount: number; currency: string } | null>( + () => { + const totals = Object.values(restaurantTotals.value) + if (!totals.length) return null + const currencies = new Set(totals.map((t) => t.currency)) + if (currencies.size !== 1) return null + return { + amount: totals.reduce((s, t) => s + t.amount, 0), + currency: totals[0].currency, + } + } ) function linesFor(restaurantId: string): CartLine[] { @@ -214,8 +245,8 @@ export const useCartStore = defineStore('restaurant-cart', () => { // getters restaurantsInCart, itemCount, - restaurantTotalsMsat, - grandTotalMsat, + restaurantTotals, + grandTotal, linesFor, // actions setActiveRestaurant, diff --git a/src/modules/restaurant/views/CartPage.vue b/src/modules/restaurant/views/CartPage.vue index 7771c85..a485420 100644 --- a/src/modules/restaurant/views/CartPage.vue +++ b/src/modules/restaurant/views/CartPage.vue @@ -25,12 +25,12 @@ const buckets = computed(() => // a separate fetch for the cart page header. restaurantSlug: cart.linesFor(rid)[0]?.restaurant_slug ?? '', lines: cart.linesFor(rid), - totalMsat: cart.restaurantTotalsMsat[rid] ?? 0, + total: cart.restaurantTotals[rid] ?? null, })) ) -function fmtSat(value: number): string { - return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value)} sat` +function fmt(value: number, currency: string): string { + return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }).format(value)} ${currency}` } @@ -70,7 +70,7 @@ function fmtSat(value: number): string { - {{ fmtSat(b.totalMsat) }} + {{ b.total ? fmt(b.total.amount, b.total.currency) : '—' }} @@ -91,12 +91,16 @@ function fmtSat(value: number): string {
-
+
Subtotal - {{ fmtSat(cart.grandTotalMsat) }} + {{ fmt(cart.grandTotal.amount, cart.grandTotal.currency) }}
+

+ Cart spans multiple currencies — per-restaurant subtotals above. + Lightning sat amount shown at checkout. +

+
+ + ≈ {{ new Intl.NumberFormat().format(previewSatPerRestaurant[b.restaurantId]!) }} sat +
@@ -141,10 +194,22 @@ async function placeOrder() {
-
+
Total - {{ fmtSat(cart.grandTotalMsat) }} + {{ fmt(cart.grandTotal.amount, cart.grandTotal.currency) }} + +
+
+ + + Pay in sats + + + ≈ {{ new Intl.NumberFormat().format(previewSatTotal) }} sat
diff --git a/src/modules/restaurant/views/ItemPage.vue b/src/modules/restaurant/views/ItemPage.vue index 945a4ca..2b971f7 100644 --- a/src/modules/restaurant/views/ItemPage.vue +++ b/src/modules/restaurant/views/ItemPage.vue @@ -87,7 +87,8 @@ function addToCart() { restaurant_slug: restaurant.value.slug, menu_item_id: item.value.id, name: item.value.name, - unit_msat: unitPrice.value, + unit_price: unitPrice.value, + currency: item.value.currency || restaurant.value.currency, quantity: quantity.value, selected_modifiers: selectedModifiers.value, note: note.value || null, diff --git a/src/modules/restaurant/views/RestaurantPage.vue b/src/modules/restaurant/views/RestaurantPage.vue index 3d97877..d9f06f0 100644 --- a/src/modules/restaurant/views/RestaurantPage.vue +++ b/src/modules/restaurant/views/RestaurantPage.vue @@ -84,7 +84,8 @@ function quickAdd(itemId: string) { restaurant_slug: restaurant.value.slug, menu_item_id: it.id, name: it.name, - unit_msat: it.price, + unit_price: it.price, + currency: it.currency || restaurant.value.currency, quantity: 1, selected_modifiers: [], note: null,