Story 2159: every commercial number moves out of the code and into a row you can edit

WHY. The catastrophe this Story prevents is a price change that needs a deploy. Today the AI markup (1.5×), the FX peg (implicitly ₹100/$), the GST rate, the minimum top-up — every commercial number the founder locked in the pricing brainstorm — lives as a Python constant. To move markup from 1.5× to the founder's launch 3× you would edit code, open a PR, wait for CI, and redeploy a Lambda. Worse, a constant is invisible to the people who own pricing: staff cannot see it, cannot audit it, cannot retune it. pramaan-docs ADR-151 (config-driven) + ADR-158 (money integrity) say the opposite: a commercial number is data, an admin-editable row, never a constant. Story-2159 is the S1 foundation of the RFD-BILL-001 billing wave — it builds the two config tables every later story (S2 admin edit, S3 metering, S6 storage) reads from, and it does so without touching the append-only ledger, the debit idempotency, or a single live invoice.

🛰 Satellite — the one sentence

Two new global config tables in the billing schema — billing.plan (one row per commercial plan) and billing.pricing_config (one row of cross-plan knobs) — are seeded with the founder's launch numbers, wired so firm_subscription.plan is an FK into the plan catalog, and read by the AI debit path so the markup and FX come from a row a staff member can edit instead of a constant a developer has to redeploy.

✈️ Airplane — a thermostat, not a soldered resistor

Think of the old code as a heater with the temperature soldered in: to run warmer you desolder the resistor and fit a new one (edit the constant, redeploy). Story-2159 replaces the solder joint with a thermostat dial. The dial is billing.pricing_config: a single row holding ai_markup=3.0, fx_usd_to_inr=110, gst_pct=18, min_topup_inr=1000, wallet_floor_usd=1000, cap_warn_pct=60. The per-plan knobs (seat price, bonus credits, storage, seat minimums) are a second dial-board, billing.plan, one row per plan. The heater's control circuit — the AI debit path — is rewired to read the dial each time instead of the soldered value. Crucially, the wiring change is surgical: the debit's math contract, its once-per-run idempotency, and the append-only ledger are untouched; only the two numbers it multiplies by now come off the dial.

🔬 Microscopic — the four moving parts

1. billing.plan — the per-plan dial board

One row per commercial plan, every column an editable number. The migration seeds the two go-forward plans with the founder's placeholders (RFD §14):

startup   min_seats=1  bonus_credits_inr=5000  free_storage_gb=5   storage_upgradeable=false
business  min_seats=3  seat_price_inr=25000    free_storage_gb=100 storage_overage_multiple=5  usage_matrix_enabled=true

Per-plan nullability is intentional: seat_price_inr is NULL on Startup and bonus_credits_inr is NULL on Business — a number that does not apply to a plan is absent, not a misleading zero. monthly_allowance_inr_per_seat is seeded NULL on Business because the founder left it unpinned in §14; staff set it in S2 rather than the migration inventing a commercial number.

2. billing.pricing_config — the single cross-plan dial

A singleton by construction: the primary key is a boolean defaulted true and CHECKed true, so a second row is a constraint violation, not a race. The founder launch values live as column server-defaults, and the migration inserts exactly one row:

ai_markup=3.0  fx_usd_to_inr=110  gst_pct=18  min_topup_inr=1000  wallet_floor_usd=1000  cap_warn_pct=60

3. firm_subscription.plan — a CHECK becomes an FK

The plan column was guarded by a hardcoded plan IN ('trial','starter','growth', 'enterprise') CHECK — the very kind of in-code taxonomy ADR-151 forbids. The migration drops that CHECK and adds fk_firm_subscription_plan → billing.plan.plan_key: the allowed set is now the catalog, editable without a deploy. The subtle part is not breaking firms already on the old plans. The live app still writes trial on firm creation, so a validated FK against a two-row (startup/business) table would reject every new firm. The migration therefore also seeds the four legacy keys as inactive transitional anchor rows (is_active=false, commercial fields NULL) so the FK validates for existing and new subscriptions until the taxonomy migration (S2+) flips the app to the new keys. Seeding only the two go-forward plans would have been "done at the component boundary" — green in a two-plan unit test, broken the moment a real firm is created.

4. The AI debit path reads the dial (idempotency untouched)

The pure credit-math core gains one optional parameter: debit_millicredits(..., usd_to_inr=CREDITS_PER_USD). The default is the existing display peg, so every current caller and unit test is byte-for-byte unchanged. The write path (credit_ledger_writes.record_run_debit) now sources the two numbers from config:

config_markup, fx = await load_ai_pricing(session)   # billing.pricing_config, one bounded SELECT
fee = platform_fee if platform_fee is not None else config_markup
millicredits = credits.debit_millicredits(cost_usd=..., platform_fee=fee, usd_to_inr=fx)

load_ai_pricing falls back to the as-built constants if the config row is missing, so a debit can never fail on a config read — the same fallback-only posture the model-cost wave used. The frozen platform_fee ledger column now stamps the config markup actually applied, so per-row stamping (ADR-158) still protects past invoices when the dial later moves. The ON CONFLICT (firm_id, run_id) once-per-run guard, the append-only trigger, and the bucket-draw are all exactly as they were: this Story changes which two numbers the debit multiplies by, nothing about when or whether it writes.

What this Story deliberately does NOT do

Left outWhy
Staff admin edit route / DTOsS2 owns the human surface; S1 is only the tables + seed the reader.
Migrating live firms off trial/starter/growth/enterpriseGrandfathered via inactive anchor rows; the taxonomy flip is a later story, not an S1 data rewrite.
Flipping live billing to 3×/₹110The debit path is gated by credit_metering_enabled (off in prod); wiring the source now is inert until metering is enabled separately.
Per-row FX ledger columnThe effective rate is recoverable from stored micro-USD cost + millicredits; a dedicated column is a Wave-2 order-record concern (RFD §5.4), not asked here.
Any hardcoded commercial numberThe whole point — every value is a seeded, editable row (unpinned ones seeded NULL, not invented).
Recall in one sentence: Story-2159 lifts every commercial number out of Python constants into two editable billing rows (plan + pricing_config), makes firm_subscription.plan an FK into that catalog without breaking grandfathered firms, and has the AI debit read markup & FX from the config row — all additive, reversible, and with the append-only ledger and debit idempotency untouched.