ADR-0039: User-managed supported currencies
- Status: Accepted
- Date: 2026-09-27
- Deciders: maintainers
- Related: ADR-0009 · ADR-0017 · ADR-0025 · ADR-0019 · money.md
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/Currencykeeps the full built-in catalog (22 currencies) with minor digits, display symbols,TOKENS, andDETECT. This stays compile-time and side-effect free soparser/and the money codecs remain pure Kotlin (no Android imports, JVM-only unit tests).- A user-managed enabled subset lives in the encrypted
settingstable underKEY_SUPPORTED_CURRENCIES(CSV of ISO codes). Unset or all-unknown values fall back toDEFAULT_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.currencyAmountandNotificationFilter.AMOUNTnow derive fromCurrency.amountAlternation()so the rule set and the capture filter cannot drift from the catalog. Because seeded rule regexes are plain strings in therulestable,RuleRepositorybumpsRULE_SEED_VERSIONto 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/DETECTbecomes 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(inRuleRepository) and aRULE_SEED_VERSIONbump so existing installs pick up the new symbols; fresh installs get them automatically. Documented in the codebase-map recipe. Tokenordering 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.