Migration 0010 adds the columns needed to price products + policies
in something other than satoshis (USD, EUR, BTC at higher denoms)
while keeping every existing operator's data behaviorally identical.
This is the foundation work; admin UI write path, buy page
rendering, and rate fetcher land in subsequent phases. See
MULTI_CURRENCY_DESIGN.md at the parent licensing/ folder for the
full design.
Schema changes (all additive):
- products gain price_currency (TEXT NOT NULL DEFAULT 'SAT') and
price_value (INTEGER NOT NULL DEFAULT 0). Backfill copies
price_sats → price_value on every existing row, so SAT-priced
products carry their information identically through the
migration.
- policies gain price_currency_override (nullable, NULL = inherit
from product) and price_value_override (nullable, mirrors the
existing price_sats_override).
- invoices gain four nullable columns: listed_currency, listed_value,
exchange_rate_centibps, exchange_rate_source. NULL on every
current row; populated by the daemon when an invoice is created
against a fiat-priced product.
- discount_codes gains discount_currency (DEFAULT 'SAT'). 'percent'
codes are currency-agnostic; 'fixed_sats' and 'set_price' codes
use this column to express "$10 off" or "set price to $25"
against fiat-priced products.
- New index idx_products_currency for future "list products by
currency" admin views.
Read path:
- Product struct gains price_currency + price_value fields
(#[serde(default)] for back-compat with any cached/persisted
shapes that predate them).
- row_to_product extracts the new columns; falls back to SAT/
price_sats if a row predates 0010 (defensive — migration always
runs at boot, but no reason to crash if it didn't).
- All four product SELECTs add the new columns.
Write path (legacy SAT-only callers):
- create_product dual-writes price_sats AND price_value to the
same value, with price_currency = 'SAT'.
- update_product dual-writes price_sats and price_value when the
caller passes a new sat price.
Migration regression test:
- migration_0010_backfills_existing_products_to_sat seeds three
products (free, $100, $2500-equivalent) and a policy with a
sat override BEFORE 0010 runs, applies 0010, asserts every row
ends up with price_currency = 'SAT' and price_value =
price_sats. Catches any future change that breaks the
backfill contract.
- migration_0009_is_idempotent now pinned to 0009 by filename
(was: "the last migration"). 0010+ are not idempotent (ALTER
TABLE ADD COLUMN can't be retried in SQLite); the
idempotency test is specifically for 0009 because that
migration's whole point was being safely re-runnable.
Test count: 33 (was 32; +1 migration_0010_backfills test).
Decisions locked in (per MULTI_CURRENCY_DESIGN open questions):
- Default currency on new products: SAT. Operators explicitly
pick USD for fiat-priced products.
- Multi-currency available to all tiers (NOT gated behind Pro/
Patron) — the right product call.
- Rate source priority: Kraken → Coinbase → CoinGecko (lands
in Phase 4 of the design).
- Recurring subscriptions: SAT-priced subs charge the same sat
amount each cycle (no rate adjustment needed); USD-priced subs
re-quote each cycle so the dollar amount is stable.