Investment Dashboard: multi-currency amounts displayed inconsistently #36

Closed
opened 2026-05-06 10:56:07 +00:00 by casper-stevens · 4 comments
Member

Problem

The Investment Dashboard mixes currency symbols across its sections, making the same underlying amounts appear with different symbols depending on where you look.

Current behaviour

Section Displayed Stored currency
Invested (summary card) €948K GBP transactions (shows wrong EUR symbol)
Net Position (summary card) €948K GBP transactions (shows wrong EUR symbol)
By Company — test £948K GBP (correct)
By Instrument Type — loan $300K USD (correct per instrument)
Recent Transactions — Investment 2026-05-20 £948K GBP (correct)

The same £948K investment is shown as €948K in the Invested summary card, £948K in By Company, and £948K in the recent transactions list — three representations of the same value with two different currency symbols.

How "Total Invested" is calculated today

Invested sums all Investment-type transaction amounts and formats the total using a base_currency heuristic (most common transaction currency across all transactions). Because the heuristic samples all three transactions it should resolve to GBP, but the summary card still renders as €948K. This means either the heuristic is not resolving correctly for this data or the running service has not been restarted after the last fix attempt.

Root cause

The dashboard has three independent data sources each carrying their own currency field:

  • TransactionsTransaction.currency (e.g. gbp, eur)
  • InstrumentsInstrument.currency (e.g. usd, eur)
  • Derived totals (Invested, Repaid, Net Position) — computed by summing transaction amounts with no currency conversion or grouping

No normalisation happens between these sources, so a fund with GBP transactions and USD instruments shows different symbols in every section of the same page.

Proposed solutions

Option A — Per-currency breakdown (preferred)
Show each currency group separately in summary cards and tables:

Invested   £948K
           €3

Honest, no exchange-rate data needed, works naturally for multi-currency funds.

Option B — Single base currency with conversion
Choose a base currency per space (configurable, default EUR) and convert all amounts using stored or live exchange rates.

Option C — Single currency enforcement
Validate at create-time that all transactions and instruments in a space use the same currency.

Acceptance criteria

  • Invested and Net Position show the same symbol(s) as the transactions they aggregate
  • No section on the Investment Dashboard shows a different currency symbol for the same underlying amount
  • Chosen approach is documented in the handler/template
## Problem The Investment Dashboard mixes currency symbols across its sections, making the same underlying amounts appear with different symbols depending on where you look. ### Current behaviour | Section | Displayed | Stored currency | |---|---|---| | **Invested** (summary card) | €948K | GBP transactions (shows wrong EUR symbol) | | **Net Position** (summary card) | €948K | GBP transactions (shows wrong EUR symbol) | | **By Company** — test | £948K | GBP (correct) | | **By Instrument Type** — loan | $300K | USD (correct per instrument) | | **Recent Transactions** — Investment 2026-05-20 | £948K | GBP (correct) | The same £948K investment is shown as **€948K** in the Invested summary card, **£948K** in By Company, and **£948K** in the recent transactions list — three representations of the same value with two different currency symbols. ### How "Total Invested" is calculated today `Invested` sums all `Investment`-type transaction amounts and formats the total using a `base_currency` heuristic (most common transaction currency across *all* transactions). Because the heuristic samples all three transactions it should resolve to `GBP`, but the summary card still renders as `€948K`. This means either the heuristic is not resolving correctly for this data or the running service has not been restarted after the last fix attempt. ## Root cause The dashboard has three independent data sources each carrying their own currency field: - **Transactions** — `Transaction.currency` (e.g. `gbp`, `eur`) - **Instruments** — `Instrument.currency` (e.g. `usd`, `eur`) - **Derived totals** (Invested, Repaid, Net Position) — computed by summing transaction amounts with no currency conversion or grouping No normalisation happens between these sources, so a fund with GBP transactions and USD instruments shows different symbols in every section of the same page. ## Proposed solutions **Option A — Per-currency breakdown (preferred)** Show each currency group separately in summary cards and tables: ``` Invested £948K €3 ``` Honest, no exchange-rate data needed, works naturally for multi-currency funds. **Option B — Single base currency with conversion** Choose a base currency per space (configurable, default EUR) and convert all amounts using stored or live exchange rates. **Option C — Single currency enforcement** Validate at create-time that all transactions and instruments in a space use the same currency. ## Acceptance criteria - `Invested` and `Net Position` show the same symbol(s) as the transactions they aggregate - No section on the Investment Dashboard shows a different currency symbol for the same underlying amount - Chosen approach is documented in the handler/template
Author
Member

@marionrvrn can you look at which I should implement? either display currencies separately or sum them using conversion. conversion will add extra dependencies.

@marionrvrn can you look at which I should implement? either display currencies separately or sum them using conversion. conversion will add extra dependencies.
Member

Let's go with option A and display currencies separately. No need to add exchange rate dependencies at this stage and it's more accurate to show amounts in their real currencies anyway

Let's go with option A and display currencies separately. No need to add exchange rate dependencies at this stage and it's more accurate to show amounts in their real currencies anyway
Author
Member
Fix the Investment Dashboard so that the four summary metric cards (Invested, Repaid, Income, Net Position) display amounts grouped by currency rather than collapsing all amounts into a single number under a `base_currency` heuristic.

Implementation Spec: Issue #36 — Multi-Currency Fix (Option A)

Objective

Fix the Investment Dashboard so that the four summary metric cards (Invested, Repaid, Income, Net Position) display amounts grouped by currency rather than collapsing all amounts into a single number under a base_currency heuristic.

Root Cause

All broken behaviour lives in InvestmentDashboardTemplate::render() in crates/hero_biz_ui/src/web/templates/mod.rs.

  1. base_currency heuristic — picks the most frequent currency across all transactions and uses it as a single label, producing wrong symbols when currencies are mixed.
  2. Cross-currency summingtotal_invested, total_repaid, etc. sum amounts across all currencies as if they were the same.
  3. by_company and by_type — use or_insert((0.0, first_currency_seen)), so a company's second currency's amounts get the wrong symbol.

No handler changes are needed.

Files to Modify

  • crates/hero_biz_ui/src/web/templates/mod.rs (only the InvestmentDashboardTemplate::render() method, ~lines 6034–6366)

Implementation Plan

Step 1 — Replace scalar totals with per-currency maps

Remove base_currency, total_invested, total_repaid, total_interest, total_dividends, net_position. Replace with HashMap<String, f64> maps: invested_by_currency, repaid_by_currency, income_by_currency, net_by_currency. Build them by iterating self.transactions once, grouping by tx.currency.to_uppercase() and transaction type.
Dependencies: none

Step 2 — Add render_currency_breakdown helper closure

Add a local closure just before the HTML format string that takes a &HashMap<String, f64> and returns a sorted multi-line HTML string of format_amount_compact calls. Falls back to "—" for empty maps.
Dependencies: Step 1

Step 3 — Fix the four summary cards in the HTML format string

Replace the four format_amount_compact(total_X, &base_currency) calls in the format! arguments with render_currency_breakdown(&X_by_currency). Change <span class="display-6"> wrappers to <div class="mt-2 text-end fs-5 fw-bold"> so that multiple currency lines stack correctly.
Dependencies: Steps 1, 2

Step 4 — Fix by_company to group by (company, currency)

Replace HashMap<String, (f64, String)> with HashMap<String, HashMap<String, f64>>. Render each company row with stacked currency amounts using render_currency_breakdown.
Dependencies: Step 2

Step 5 — Fix by_type equivalently

Same as Step 4 but for instrument type grouping.
Dependencies: Step 2

Step 6 — Handle the ROI card

Compute all_currencies (HashSet). Show ROI only when a single currency is present; otherwise render "N/A (multiple currencies)".
Dependencies: Step 1

Step 7 — Remove dead variables and verify compile

Remove all now-unused bindings to fix compiler warnings. Run cargo check.
Dependencies: Steps 1–6

Acceptance Criteria

  • Invested card shows £948K (not €948K) for a GBP-only portfolio
  • Mixed-currency portfolio shows separate lines per currency in each summary card
  • By Company and By Instrument Type tables show correct per-currency amounts
  • Recent Transactions table unchanged (already correct)
  • Project compiles without warnings
  • Single-currency portfolios look identical to current design (no visual regression)
  • No new crate dependencies, no exchange-rate API calls

Notes

  • format_amount_compact and currency_symbol are correct as-is and handle GBP, USD, CHF, JPY, CNY, EUR
  • Always use .to_uppercase() when building currency map keys
  • Sort currency entries before rendering for deterministic output

--- ## Implementation Spec: Issue #36 — Multi-Currency Fix (Option A) ### Objective Fix the Investment Dashboard so that the four summary metric cards (Invested, Repaid, Income, Net Position) display amounts grouped by currency rather than collapsing all amounts into a single number under a `base_currency` heuristic. ### Root Cause All broken behaviour lives in `InvestmentDashboardTemplate::render()` in `crates/hero_biz_ui/src/web/templates/mod.rs`. 1. **`base_currency` heuristic** — picks the most frequent currency across all transactions and uses it as a single label, producing wrong symbols when currencies are mixed. 2. **Cross-currency summing** — `total_invested`, `total_repaid`, etc. sum amounts across all currencies as if they were the same. 3. **`by_company` and `by_type`** — use `or_insert((0.0, first_currency_seen))`, so a company's second currency's amounts get the wrong symbol. No handler changes are needed. ### Files to Modify - `crates/hero_biz_ui/src/web/templates/mod.rs` (only the `InvestmentDashboardTemplate::render()` method, ~lines 6034–6366) ### Implementation Plan #### Step 1 — Replace scalar totals with per-currency maps Remove `base_currency`, `total_invested`, `total_repaid`, `total_interest`, `total_dividends`, `net_position`. Replace with `HashMap<String, f64>` maps: `invested_by_currency`, `repaid_by_currency`, `income_by_currency`, `net_by_currency`. Build them by iterating `self.transactions` once, grouping by `tx.currency.to_uppercase()` and transaction type. Dependencies: none #### Step 2 — Add `render_currency_breakdown` helper closure Add a local closure just before the HTML format string that takes a `&HashMap<String, f64>` and returns a sorted multi-line HTML string of `format_amount_compact` calls. Falls back to `"—"` for empty maps. Dependencies: Step 1 #### Step 3 — Fix the four summary cards in the HTML format string Replace the four `format_amount_compact(total_X, &base_currency)` calls in the format! arguments with `render_currency_breakdown(&X_by_currency)`. Change `<span class="display-6">` wrappers to `<div class="mt-2 text-end fs-5 fw-bold">` so that multiple currency lines stack correctly. Dependencies: Steps 1, 2 #### Step 4 — Fix `by_company` to group by `(company, currency)` Replace `HashMap<String, (f64, String)>` with `HashMap<String, HashMap<String, f64>>`. Render each company row with stacked currency amounts using `render_currency_breakdown`. Dependencies: Step 2 #### Step 5 — Fix `by_type` equivalently Same as Step 4 but for instrument type grouping. Dependencies: Step 2 #### Step 6 — Handle the ROI card Compute `all_currencies` (HashSet). Show ROI only when a single currency is present; otherwise render "N/A (multiple currencies)". Dependencies: Step 1 #### Step 7 — Remove dead variables and verify compile Remove all now-unused bindings to fix compiler warnings. Run `cargo check`. Dependencies: Steps 1–6 ### Acceptance Criteria - [ ] Invested card shows `£948K` (not `€948K`) for a GBP-only portfolio - [ ] Mixed-currency portfolio shows separate lines per currency in each summary card - [ ] By Company and By Instrument Type tables show correct per-currency amounts - [ ] Recent Transactions table unchanged (already correct) - [ ] Project compiles without warnings - [ ] Single-currency portfolios look identical to current design (no visual regression) - [ ] No new crate dependencies, no exchange-rate API calls ### Notes - `format_amount_compact` and `currency_symbol` are correct as-is and handle GBP, USD, CHF, JPY, CNY, EUR - Always use `.to_uppercase()` when building currency map keys - Sort currency entries before rendering for deterministic output ---
Author
Member

Implementation complete

Changes made

File modified: crates/hero_biz_ui/src/web/templates/mod.rs

All changes are confined to InvestmentDashboardTemplate::render().

  • Removed the base_currency heuristic (majority-vote currency picker) and all cross-currency scalar sums (total_invested, total_repaid, total_interest, total_dividends, net_position).
  • Replaced with per-currency HashMap<String, f64> maps: invested_by_currency, repaid_by_currency, income_by_currency, net_by_currency. Built in a single pass over self.transactions grouped by tx.currency.to_uppercase() and transaction type.
  • Added a render_currency_breakdown closure that renders a per-currency map as sorted, stacked <div class="fw-bold"> lines — one line per currency, alphabetical order for deterministic output.
  • Updated the four summary metric cards (Invested, Repaid, Income, Net Position) to use render_currency_breakdown instead of a single format_amount_compact call. Changed <span class="display-6"> to <div class="text-end"> so multiple lines stack correctly.
  • Fixed by_company: replaced HashMap<String, (f64, String)> (which silently used the first-seen currency for a company) with HashMap<String, HashMap<String, f64>> (company → currency → sum). Rows now render stacked amounts per currency using the same helper.
  • Fixed by_type identically for instrument type groupings.
  • ROI card: now computed only when all transactions share a single currency; renders "N/A (multiple currencies)" otherwise, preventing false ROI figures from cross-currency arithmetic.

Test results

  • Total: 1 | Passed: 1 | Failed: 0
  • cargo check and cargo test both pass with no warnings.

Acceptance criteria

  • Invested and Net Position cards now show the same symbol(s) as the transactions they aggregate
  • No section on the Investment Dashboard shows a different currency symbol for the same underlying amount
  • Single-currency portfolios render identically to before (one line per card, ROI shown)
## Implementation complete ### Changes made **File modified:** `crates/hero_biz_ui/src/web/templates/mod.rs` All changes are confined to `InvestmentDashboardTemplate::render()`. - Removed the `base_currency` heuristic (majority-vote currency picker) and all cross-currency scalar sums (`total_invested`, `total_repaid`, `total_interest`, `total_dividends`, `net_position`). - Replaced with per-currency `HashMap<String, f64>` maps: `invested_by_currency`, `repaid_by_currency`, `income_by_currency`, `net_by_currency`. Built in a single pass over `self.transactions` grouped by `tx.currency.to_uppercase()` and transaction type. - Added a `render_currency_breakdown` closure that renders a per-currency map as sorted, stacked `<div class="fw-bold">` lines — one line per currency, alphabetical order for deterministic output. - Updated the four summary metric cards (Invested, Repaid, Income, Net Position) to use `render_currency_breakdown` instead of a single `format_amount_compact` call. Changed `<span class="display-6">` to `<div class="text-end">` so multiple lines stack correctly. - Fixed `by_company`: replaced `HashMap<String, (f64, String)>` (which silently used the first-seen currency for a company) with `HashMap<String, HashMap<String, f64>>` (company → currency → sum). Rows now render stacked amounts per currency using the same helper. - Fixed `by_type` identically for instrument type groupings. - ROI card: now computed only when all transactions share a single currency; renders "N/A (multiple currencies)" otherwise, preventing false ROI figures from cross-currency arithmetic. ### Test results - Total: 1 | Passed: 1 | Failed: 0 - `cargo check` and `cargo test` both pass with no warnings. ### Acceptance criteria - Invested and Net Position cards now show the same symbol(s) as the transactions they aggregate - No section on the Investment Dashboard shows a different currency symbol for the same underlying amount - Single-currency portfolios render identically to before (one line per card, ROI shown)
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_biz#36
No description provided.