Currency Management#
Overview#
Sure uses a static YAML catalog as the single source of truth for all currency definitions. This catalog drives Money::Currency object construction, formatting, validation, and UI selection throughout the app. There is no runtime lookup against an external currency service — if a currency code is not in the catalog, it is unknown to the system.
The Currency Catalog (config/currencies.yml)#
config/currencies.yml defines every supported currency as a keyed YAML entry (lowercase ISO code → attributes). Each entry carries:
| Field | Purpose |
|---|---|
iso_code | Uppercase ISO 4217 code (e.g., USD) |
iso_numeric | Numeric ISO code (blank for non-ISO currencies like BTC) |
symbol, html_code | Display symbols |
minor_unit, minor_unit_conversion | Subunit name and conversion factor |
default_precision | Decimal places for formatting |
separator, delimiter, default_format | Locale-specific formatting |
priority | Sort order (lower = higher priority in UI) |
The catalog includes fiat currencies (USD at priority 1, EUR at priority 2, etc.), precious metals (XAU, XAG, XPD, XPT), and a small set of crypto assets (BTC, DOGE, USDC) .
Money::Currency — The Catalog Interface#
lib/money/currency.rb wraps the YAML catalog:
Money::Currency.new(code)— looks upcode(downcased) in@@instancescache, or loads from YAML. RaisesMoney::Currency::UnknownCurrencyErrorif the code is absent .Money::Currency.all— parses and memoizes the full YAML file viaYAML.safe_load.Money::Currency.as_options— returns all instances sorted by priority then name; used for UI dropdowns .Money::Currency.popular— returns the first 12 by priority; used for quick-select UI .
Instance attributes (symbol, minor_unit_conversion, default_precision, etc.) are read directly from the YAML data .
Currency Validation: Two Layers#
1. Account model (persistence layer)#
Account validates presence of currency but does not validate that the code exists in the catalog . Any 3-letter or non-standard string can be saved as an account's currency if it passes the presence check. The Family model enforces catalog membership more strictly: normalize_currency_code calls Money::Currency.new(value) and rescues UnknownCurrencyError, silently dropping invalid codes .
Family#enabled_currency_codes controls the set of currencies available to a family. If enabled_currencies is nil, all catalog entries are available; otherwise only the explicit list plus the primary currency is returned . The primary currency falls back to "USD" if not set .
2. Provider-import normalization layer (CurrencyNormalizable)#
app/models/concerns/currency_normalizable.rb is included by provider sync models (Plaid, SimpleFIN, LunchFlow, Enable Banking) to sanitize incoming currency codes before storage:
- Rejects blank values →
nil - Normalizes to uppercase
- Enforces a 3-letter
[A-Z]{3}format — rejects codes likeDOGEorUSDC - Validates against the catalog via
Money::Currency.new— rejects codes likeXXX
Invalid codes are logged as warnings and return nil, letting callers apply their own fallback .
Gap:
CurrencyNormalizableonly enforces 3-letter codes. Crypto tickers like BTC/DOGE that appear as holdingcurrencyfields bypass this concern; theHoldingmodel validates only presence, not catalog membership .
Crypto Accounts: Fiat Valuation vs. Crypto Holdings#
Crypto accounts (type "Crypto") have a deliberate split between account-level valuation and individual asset tracking:
- Account currency = fiat. When creating a Coinbase account,
native_balance.currencyfrom the API payload (e.g.,USD,EUR,GBP) becomes the account'scurrency. The accountbalancereflects the fiat value of all holdings combined;cash_balanceis set to0because no cash is held directly . - Holdings carry the crypto asset. Each holding belongs to a
Securityresolved with aCRYPTO:ticker prefix (e.g.,CRYPTO:BTC). The holding'scurrencyfield stores the fiat denomination of theamount(i.e., the valuation currency, not the asset's own ticker) .
For Coinbase, CoinbaseAccount::HoldingsProcessor derives the fiat price via native_balance.amount / quantity, then stores amount in native_currency . For CoinStats, CoinstatsAccount::HoldingsProcessor similarly resolves holdings with CRYPTO: prefixed tickers and passes inferred_currency (a fiat code) as the holding currency .
When computing weight or amount_in_account_currency, Holding calls Money#exchange_to to convert the holding amount into the account's fiat currency; if the exchange rate is missing, it rescues ConversionError and falls back to the raw amount .
Key Files#
| File | Role |
|---|---|
config/currencies.yml | Static currency catalog |
lib/money/currency.rb | Catalog parser and Money::Currency class |
app/models/concerns/currency_normalizable.rb | Provider-import validation concern |
app/models/account.rb | Account model with presence validation and provider factory methods |
app/models/family.rb | Family-level currency preferences and enabled_currency_codes |
app/models/holding.rb | Holding model; stores fiat-denominated amounts for crypto assets |
app/models/coinbase_account/holdings_processor.rb | Coinbase crypto → fiat holding import |
app/models/coinstats_account/holdings_processor.rb | CoinStats crypto → fiat holding import |