From d4d3bb86b4df15fb0e6673cbc1de123a27fb02a7 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 31 Aug 2026 10:54:10 +0000 Subject: [PATCH] fix(invoices): count every currency in total collected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Total collected" added ETB to USD-converted-to-ETB and stopped there, so money collected in any third currency was absent from the headline figure while still sitting in the invoice list underneath. Same shape as the overview dashboards fixed in 829918126. Converts each non-birr currency at the live rate, falling back to that currency's own stored rate when CBE is down, and adds the DJF tile beside the existing ETB and USD ones. A currency with no usable rate is left OUT of the total and the hint says so, rather than being added at face value — treating 1 DJF as 1 ETB would overstate it about a hundredfold, which is worse than an obviously incomplete number. --- .../src/pages/invoices/InvoicesPage.tsx | 42 +++++++++++++++++-- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx index 2b319de30..085556f9d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx @@ -235,8 +235,33 @@ export default function InvoicesPanel() { const { data: exchangeSettings } = useExchangeSettingsQuery(); const etbCollected = summary?.ETB ?? 0; const usdCollected = summary?.USD ?? 0; - const rate = exchangeSettings?.feed?.rate ?? exchangeSettings?.fallbackRate; - const etbFromUsd = rate ? usdCollected * rate : null; + const djfCollected = summary?.DJF ?? 0; + + /** Live rate when CBE is answering, this currency's stored fallback when it is not. */ + const rateFor = (code: string): number | null => + exchangeSettings?.feed?.rates?.[code] ?? + exchangeSettings?.currencies.find((c) => c.currency === code)?.fallbackRate ?? + null; + + // Every non-birr currency converted into birr, so one headline figure covers the whole + // book. A currency with no usable rate is left OUT of the total rather than added at + // face value — silently treating 1 DJF as 1 ETB would overstate it ~100x. + const foreign = [ + { code: "USD", amount: usdCollected }, + { code: "DJF", amount: djfCollected }, + ] + .filter((row) => row.amount > 0) + .map((row) => ({ ...row, rate: rateFor(row.code) })); + + const convertible = foreign.filter( + (row): row is typeof row & { rate: number } => (row.rate ?? 0) > 0, + ); + const etbFromForeign = convertible.reduce( + (sum, row) => sum + row.amount * row.rate, + 0, + ); + const totalHint = ["ETB", ...convertible.map((r) => r.code)].join(" + "); + const unconverted = foreign.length - convertible.length; const columns: ColumnDef[] = useMemo( () => [ @@ -356,8 +381,11 @@ export default function InvoicesPanel() { items={[ { label: "Total collected", - hint: etbFromUsd !== null ? "ETB + USD" : "ETB only", - value: formatMoney(etbCollected + (etbFromUsd ?? 0), "ETB"), + hint: + unconverted > 0 + ? `${totalHint} — ${unconverted} currency without a rate excluded` + : totalHint, + value: formatMoney(etbCollected + etbFromForeign, "ETB"), icon: CircleDollarSign, color: "edr-green", }, @@ -373,6 +401,12 @@ export default function InvoicesPanel() { icon: Landmark, color: "violet", }, + { + label: "Collected in DJF", + value: formatMoney(djfCollected, "DJF"), + icon: Landmark, + color: "grape", + }, ]} />