The RFC in 6 cards: what it solves, the key decisions, where the risk lives.
What it shipsPrepaid usage (v2): the customer pays a fixed recurring money allowance, rated usage draws down a visible balance, exhaustion blocks, top-ups extend; at renewal the invoice bills the full allowance again, the unused allowance expires, and only top-up money carries over.
✓ scope closed
The backbone is a new money-denominated, order-scoped grant ledger (UsageGrant): paid prepaid invoice items mint grants (amount = the paid item amount), rated usage draws the balance down, refunds revoke exactly the refunded money. The deliverable is merchant-complete: plan configuration, upfront invoicing, balance API and app view, enforcement, notifications, manual top-up, settlement with the expiry split. Auto top-up is an explicit follow-up, designed to ride the same mechanism. → try it live in the simulator
"At renewal the invoice always bills the full allowance: the unused part of the allowance expires with the period, while unused top-up money carries over until consumed. The mental model is a phone plan: a fixed monthly fee with included consumption, plus extra credit you bought that stays yours."
Decision · 0Renewal arithmetic (v2): fixed billing. Every renewal bills the full allowance; the unused allowance expires; top-up money carries over.
✓ settled in team review (Aug 4)
Settled in the Aug 4 team review, superseding the refill-to-N arithmetic of v1: fixed billing gives predictable recurring revenue and the familiar subscription mental model, and the balance stays self-limiting because the allowance expires at settlement (breakage, reported per period). What survives for the customer: top-up money never expires and never pays twice, and the consumption order burns expiring money first. A side effect: the minimum-usage floor question is moot, the fixed bill is the revenue floor.
→ run the "Rolling a top-up surplus" scenario in the simulator
✕ Superseded: refill-to-N (v1; Alternative #1 in v2): the renewal bills only max(0, allowance - carryOver) and the whole balance carries. Remains a possible future plan option for strict pay-for-the-difference semantics. The accumulate-everything fixed variant (bill the full allowance and stack the whole remainder on top) also stays rejected: unbounded organic growth that would need cap machinery.
"At renewal the invoice always bills the full allowance: the unused part of the allowance expires with the period, while unused top-up money carries over until consumed."
DecisionA new UsageGrant ledger, because the derived balance does not hold.
✓ closed in review
The first draft hoped for a derived balance; review sank it on three facts: the issued
InvoiceItem has no order item reference,
Usage is keyed by (order, plan), and carry-over makes the balance cumulative across periods, so a derived read would aggregate everything since inception on every query. The ledger is append-only, money-denominated (v2), corrects through revocation, and every grant traces to a paid invoice item or a settlement's persistent remainder.
✕ Rejected: derived balance (#1) · an FK on InvoiceItem (#2, "fixes attribution but not the cumulative carry-over read") · a mutable counter as system of record (#9, at-least-once InvoiceWasPaid would need a dedup row, which is a grant row stripped of its fields).
"…with carry-over the balance is cumulative across periods, so a purely derived balance would aggregate every row since inception on every read. These facts together are why this design records grants explicitly."
InvariantCorrectness rests on the DB row lock, not the Redis mutex: the Redis lock is fail-open.
✓ closed in review
The per-order Redis mutex is best-effort: on acquisition failure the platform logs and executes anyway. So every balance-affecting write runs in a transaction with SELECT … FOR UPDATE on the order row (the pool is order-scoped in v2). The parent row, deliberately: locking grant rows cannot block concurrent INSERTs of new rows (the phantom problem). One owner: UsageGrantManager::withPoolLock; every caller goes through it. Consequence: writes serialize per order; the per-order throughput ceiling is measured against the postpaid baseline during phase 1, and the GA thresholds are derived from that measurement. → test yourself: quiz question 2
"This lock is best-effort: on acquisition failure the platform logs and executes anyway… Balance correctness therefore cannot rest on Redis; it rests on database-level locking."
DecisionMoney is granted when the invoice is paid, not when it is issued.
✓ closed in review
Honest prepaid: no credit before the money arrives. When the payment lands, the listener finds the order item the paid line belongs to via a deterministic key (order + root plan external id + period overlap); the match is unique because an order may hold at most one item per root prepaid plan (invariant 8). The listener runs inside the payment flow, so it never fails the payment: on ambiguity it skips the grant, alerts ops, and the
usage-grants:settle replay mints it later. The cost: merchants whose customers pay invoices later (net terms, for example net-30) would leave their customers balance-less until payment, so they wait for the deferred issuance mode.
✕ Rejected (v1): pending grants minted at issuance and activated on payment (#8): trivial mapping, but an extra state machine and issuance-time writes inside InvoiceFactory, and it buys nothing while invariant 8 holds. It stays the designated fallback and the natural shape of the future net-terms mode.
"If pre-existing data still yields multiple candidates, the listener must not fail the payment… it skips the grant, logs an error with a metric and an ops alert… the uniqueness-invariant path is the committed design, not a preference."
RiskSix postpaid paths must be gated: "this is where most of the implementation risk lives".
▲ risk focus
Lifting the plan invariant exposes prepaid items to code written under postpaid assumptions. The ugliest is gate 3: today's time-prorated cancellation credit is written for plain prepaid items; for prepaid usage the policy is an open v2 decision (#4: recommended, credit the unused top-up money, none for the expiring allowance). All six gates touch code running for every merchant; the safety net is the billing-timing guard pattern and the postpaid test suites passing unchanged. → test yourself: quiz question 4
"…today's prorated cancellation credits prepaid items by time remaining. For prepaid metered that refunds consumed units (prepay 30, consume 30 on day one, cancel mid-month, get half the money back)."
OpenSix decisions remain (churn credit, discounted funding, backdated rating among them); the phase 1 starting slice depends on none of them and can start.
⏳ decision log
Every open question is recorded in the RFC with a recommended default, an owner and a closure criterion. The v1 decisions 0 (renewal arithmetic) and 4 (balance bounds) are resolved by v2 (fixed billing; the allowance expires, persistent money is uncapped) and moved to Alternatives. The money-correctness phases wait; phase 1 (entity, migration, the three Usage columns, read-only fields) is deliberately decision-independent.
"The decisions below block the money-correctness phases of the rollout (phases 2 to 5); the phase 1 starting slice does not depend on any of them."
Decision log · verbatim from Unresolved questions