Skip to content

ADR-0025: Offline FX conversion for foreign-currency aggregation

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 encrypted settings table under KEY_FX_RATES (a serialized JSON Map<String, String>).
  • New pure-Kotlin common/Fx.kt converts minor units between currencies with BigDecimal + HALF_UP (movePointLeft(fromDigits) * rate * movePointRight(baseDigits)), preserving the no-floating-point invariant.
  • Home, Reports and Budget ViewModels observe KEY_FX_RATES and fold each transaction through Fx.toBase before 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.00 becomes MYR using the configured rate instead of 4500.
  • Budget spent is compared against limitMillis in 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.