ADR-0025: Offline FX conversion for foreign-currency aggregation
- Status: Accepted
- Date: 2026-08-15
- Deciders: maintainers
- Related: money.md · ADR-0009 · ADR-0006 · ADR-0015
Context
Transactions store their amount in the original currency
(TransactionEntity.amountMillis + currency, ADR-0009). A foreign-currency
charge — e.g. a S$45.00 travel spend — is kept in SGD by design
(money.md → Currency.currencyNearestTo). Totals and reports use the settings
currency (SettingsRepository.KEY_CURRENCY, default MYR) as the display/base
currency, but it was only ever used for formatting, never for conversion.
Aggregation summed raw amountMillis ignoring currency, so a S$45.00
transaction was added as 4500 to a MYR total — numerically wrong.
The app is offline-first with no INTERNET permission (ADR-0015), so live
fetching of market rates is out of scope.
Decision
- The user curates an FX-rate map in Settings:
foreign code → rate(base major units per 1 foreign major unit), stored in the encryptedsettingstable underKEY_FX_RATES(a serialized JSONMap<String, String>). - New pure-Kotlin
common/Fx.ktconverts minor units between currencies withBigDecimal+HALF_UP(movePointLeft(fromDigits) * rate * movePointRight(baseDigits)), preserving the no-floating-point invariant. - Home, Reports and Budget ViewModels observe
KEY_FX_RATESand fold each transaction throughFx.toBasebefore summing / taking the max, so category slices, totals and budget comparisons all land in base units. - A transaction that cannot be converted (unknown currency or missing
rate) is excluded from the aggregate — never summed numerically raw —
and a flag (
hasUnconvertedForeign) is set so the UI can surface a footnote.
Consequences
- Mixed-currency months now aggregate correctly;
S$45.00becomes MYR using the configured rate instead of4500. - Budget
spentis compared againstlimitMillisin the same base units, so the comparison is meaningful. - Rates go stale over time; converting historical rows at the current rate is an accepted approximation for a personal tracker. Per-month rate snapshots are deferred.
- Realtime market FX rates are rejected (offline-first, ADR-0015).
Alternatives considered
- Hardcoded static rates in
common/— goes stale with no way for the user to correct it. - Live FX lookup — requires network, conflicts with ADR-0015.