Multi-currency schema foundation (Phase 1 of MULTI_CURRENCY_DESIGN)
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.
This commit is contained in:
@@ -8,7 +8,22 @@ pub struct Product {
|
||||
pub slug: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
/// Sat-denominated price. For SAT-currency products this equals
|
||||
/// `price_value`. For fiat-priced products (USD, EUR, etc.) this
|
||||
/// is a snapshot from the most recent invoice creation against
|
||||
/// the product, kept for back-compat with v0.1 SDKs and admin UI
|
||||
/// that haven't migrated to the typed currency view yet. The
|
||||
/// canonical price is `price_currency` + `price_value`.
|
||||
pub price_sats: i64,
|
||||
/// Operator-facing currency: 'SAT', 'BTC', 'USD', 'EUR' (and
|
||||
/// future ISO 4217 codes). Defaults to 'SAT' for products
|
||||
/// created before v0.1.0:48 / migration 0010.
|
||||
#[serde(default = "default_currency")]
|
||||
pub price_currency: String,
|
||||
/// Price in the smallest indivisible unit of `price_currency`:
|
||||
/// sats for SAT/BTC, cents for USD/EUR.
|
||||
#[serde(default)]
|
||||
pub price_value: i64,
|
||||
pub active: bool,
|
||||
/// Arbitrary JSON metadata the developer can attach.
|
||||
pub metadata: serde_json::Value,
|
||||
@@ -16,6 +31,10 @@ pub struct Product {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
fn default_currency() -> String {
|
||||
"SAT".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum InvoiceStatus {
|
||||
|
||||
Reference in New Issue
Block a user