Skip to content

ADR-0039: User-managed supported currencies

Context

common/Currency (ADR-0009) shipped a fixed catalog of eight currencies and supportedCodes() fed every picker — the base-currency chips, the FX-rate inputs, and the transaction editor's currency dropdown. Users who spent in a currency outside that set (THB, HKD, AUD, …) had no way to display or aggregate it, and there was no way to trim the pickers either if a user only ever used one or two currencies.

A single global list couples two distinct needs: what the parser can know about a currency (minor digits, symbol, tokens) and what a user wants to see in pickers. The first is pure and must stay available to the pure-Kotlin parser without a settings lookup; the second is a user preference.

Decision

  • common/Currency keeps the full built-in catalog (22 currencies) with minor digits, display symbols, TOKENS, and DETECT. This stays compile-time and side-effect free so parser/ and the money codecs remain pure Kotlin (no Android imports, JVM-only unit tests).
  • A user-managed enabled subset lives in the encrypted settings table under KEY_SUPPORTED_CURRENCIES (CSV of ISO codes). Unset or all-unknown values fall back to DEFAULT_SUPPORTED_CURRENCIES — the original eight — so existing installs are unchanged on upgrade. An explicitly saved empty selection stays empty (the UI still disables removing the last visible chip), so the repository never silently re-enables a choice the user cleared.
  • Pickers read the enabled subset (in catalog order for stable UI): base-currency chips, which always also include the current base currency; FX-rate rows; and the transaction editor's CurrencyField, which always also includes the transaction's own currency. Removing a currency is hide-only: stored transactions keep their code, stored FX rates keep converting, and the last enabled currency cannot be removed from the pickers.
  • New catalog symbols parse from notifications. BankRuleSets.currencyAmount and NotificationFilter.AMOUNT now derive from Currency.amountAlternation() so the rule set and the capture filter cannot drift from the catalog. Because seeded rule regexes are plain strings in the rules table, RuleRepository bumps RULE_SEED_VERSION to 8 and, once per version, swaps the exact prior alternation substring inside untouched seeded patterns for the new one (see the ADR-0019 amendment).
  • Deep links validate against the full catalog (Currency.isKnown), not the enabled subset, so a link to a hidden-but-valid currency still resolves.

Consequences

  • A currency added to SPECS + TOKENS/DETECT becomes usable for base currency, FX rates, manual entry, display, and parsing without touching the UI or the ViewModels.
  • Enabling a currency is a preference, not a schema change: no migration, and removing one never rewrites transaction data.
  • Each future catalog expansion needs a one-line addition to PREVIOUS_CURRENCY_ALTERNATION (in RuleRepository) and a RULE_SEED_VERSION bump so existing installs pick up the new symbols; fresh installs get them automatically. Documented in the codebase-map recipe.
  • Token ordering is now load-bearing (A$/HK$/… before $, CN¥ before ¥); the catalog test suite locks the precedence down.

Alternatives considered

  • Runtime-defined custom currencies (user types code, symbol, minor digits) — rejected: the symbol alternation is baked into seeded rule patterns and the pure parser has no settings access, so a runtime registry would leak a settings dependency into parser/ for little benefit over a curated set.
  • Reconcile rules unconditionally, or rebuild each pattern — rejected: it would overwrite user-edited patterns and break rule provenance (ADR-0002, ADR-0019). The substring swap is scoped to the prior shipped alternation.
  • Delete transaction rows / FX rates when a currency is removed — rejected: destructive and unnecessary; hiding from pickers is reversible and lossless.