D91 — Per-tenant base currency: a nullable tenants.base_currency preference with a ?base= → tenant → fleet-default resolution chain (RESOLVED, extends D49)
Architecture decision record, relocated verbatim from the retired single-file
docs/tenancy/decision log. Status, thematic clusters, and how to record a new ADR: the decision log index.
The master-balance base currency was effectively global (one fleet-wide treasury.base_currency +
the per-request ?base= override — the deferral recorded in the treasury deferrals doc). It is now a
per-tenant setting: a nullable base_currency column on the central tenants table — the
established simple-tenant-preference pattern (the level/jurisdiction precedent: a natural
UPPERCASE code, a real custom column registered in Tenant::getCustomColumns()), not a new settings
mechanism. A DB CHECK built from SupportedAsset::values() (the D84 NOT VALID + VALIDATE idiom)
backstops raw writes.
The resolution chain (in GetMasterBalanceHandler::resolveBaseCode()): the explicit ?base=
param (allow-list-validated at the controller) → the tenant's stored preference → the
treasury.base_currency fleet default. A STORED preference outside the currently-supported base set
(SupportedBaseCurrencies, now config-driven via treasury.supported_base_currencies, V1 ['USD'])
is NOT honoured — honouring it would produce a structurally all-unconverted balance — so the rollup
falls back to the fleet default and logs
treasury.master_balance.unsupported_tenant_base_currency: the read stays correct, the
misconfiguration observable. treasury:warm-rates warms every supported base (∪ the fleet default),
not just the default, so a tenant-preferred base's read path is primed too. No operator/API surface
sets the column yet (ops-write only, like level/jurisdiction) — a backoffice setter is a tracked
follow-up.