diff --git a/src/__tests__/integration/analytics-sql.test.ts b/src/__tests__/integration/analytics-sql.test.ts index dbedd16..09e4eb9 100644 --- a/src/__tests__/integration/analytics-sql.test.ts +++ b/src/__tests__/integration/analytics-sql.test.ts @@ -105,6 +105,19 @@ describe("NATIVE_CURRENCY", () => { expect(unconverted).toBe(true); }); + it("labels a statement-less wallet row AUD, because its amount already is", async () => { + // Wallet rows store a provisional AUD figure in amount AND amount_aud, and + // the native figure in foreign_currency_amount. Reading the foreign code + // here printed "INR 9.42" for an INR 524 tap (txn 8162). + const id = await scratchTxn( + "transaction_date, description, amount, amount_aud, transaction_type, payment_method, source, source_ref, foreign_currency_amount, foreign_currency_code", + "'2026-10-03','Wallet foreign fixture', 9.42, 9.42, 'debit', 'card', 'wallet', 'evt:fixture-native-ccy', 524.00, 'INR'" + ); + const { ccy, unconverted } = await currencyOf(id); + expect(ccy).toBe("AUD"); + expect(unconverted).toBe(false); + }); + it("prefers the statement's currency over the foreign-charge record", async () => { // Opposite convention: on an AUD statement, `amount` is AUD and // foreign_currency_code merely notes what was originally charged. Reading diff --git a/src/app/transactions/page.tsx b/src/app/transactions/page.tsx index 3158910..27bfc43 100644 --- a/src/app/transactions/page.tsx +++ b/src/app/transactions/page.tsx @@ -1085,7 +1085,18 @@ function TransactionsContent() { ) : ( <> {formatAmount(t.amount_aud ?? t.amount, t.transaction_type)} - {t.currency && t.currency !== "AUD" && ( + {/* + The native figure: `foreign_currency_amount` where one + was recorded (a statement or wallet row, whose `amount` + is AUD), else `amount` on a non-AUD statement. Printing + `amount` under the foreign code put the AUD figure + behind an INR label on every wallet row. + */} + {t.foreign_currency_code && t.foreign_currency_code !== "AUD" && t.foreign_currency_amount !== null ? ( +
+ {formatAmount(t.foreign_currency_amount, t.transaction_type, t.foreign_currency_code)} +
+ ) : t.currency && t.currency !== "AUD" && (
{formatAmount(t.amount, t.transaction_type, t.currency)}
diff --git a/src/lib/analytics-sql.ts b/src/lib/analytics-sql.ts index ce70069..909221a 100644 --- a/src/lib/analytics-sql.ts +++ b/src/lib/analytics-sql.ts @@ -89,10 +89,17 @@ export const EXCLUDE_RECONCILED_SOURCE = `NOT (t.statement_id IS NULL AND t.reco * figure and `foreign_currency_code` names it (order-ingestion.ts leaves * `amount_aud` NULL rather than asserting an FX rate it does not have). * + * - A wallet row also has no statement, but follows the STATEMENT convention: + * `amount` = `amount_aud` = a provisional AUD figure, and the native figure + * lives in `foreign_currency_amount` (wallet-ingestion.ts). `amount_aud` + * being present is what says `amount` is already AUD. Without that arm, an + * INR 524 tap stored as A$9.42 rendered as "INR 9.42" -- the AUD number + * under the foreign label (txn 8162, 2026-10-03). + * * Reading `s.currency` alone labels every foreign order row as AUD. * `s` must be the statements alias in scope. */ -export const NATIVE_CURRENCY = `COALESCE(s.currency, t.foreign_currency_code, 'AUD')`; +export const NATIVE_CURRENCY = `COALESCE(s.currency, CASE WHEN t.amount_aud IS NULL THEN t.foreign_currency_code END, 'AUD')`; /** * True when a row's AUD value is unknown: it is denominated in something other