Billing Migration — Provisioning Rules¶
Scope. Single source of truth for the per-(org, template_size) price that the M2 provisioner stamps onto a Stripe Price + a billing_rate_card_entries row. Every provisioning entry point — provision_meter.py, seed_rate_card_entries.py — and every PRD/tooling reference to "the default price" cites this file.
Companion docs: prd.md (the why), provisioning-behavior.md (provisioner behavior & dry-run plan).
If this file disagrees with the PRD, the PRD wins and this file is wrong — open a PR to reconcile.
R1. Default price table¶
The migration ships with per-SKU defaults that vary by market and format. The canonical "standard" price is $0.65 USD (US 4x6, UK A6); non-standard formats and privacy/self-mailer formats default higher to reflect production cost. A6_NL is $0.80 USD and is structurally pinned (R2).
| Market | Billing key | Format | Default unit amount | Currency |
|---|---|---|---|---|
| US | 4x6 |
4x6 Standard | 65¢ | USD |
| US | 6x9 |
6x9 (Non-standard) | 70¢ | USD |
| US | 6x18_bifold |
BiFold / Self-Mailer | 80¢ | USD |
| US | 12x9_bifold |
BiFold / Self-Mailer | 80¢ | USD |
| UK | A6 |
A6 (UK) | 65¢ | USD |
| UK | A5-ENV |
Enveloped A5 (Privacy) | 80¢ | USD |
| NL | A6_NL |
A6 (NL) | 80¢ | USD |
| UK | A5 |
A5 (legacy) | 85¢ | USD |
| UK | intelliprint_A4_letter |
A4 letter | 120¢ | USD |
Authoritative copy in code: TEMPLATE_SIZE_BILLING_DEFAULTS in app/core/integrations/stripe/meters.py. The values in that dict must match the table above byte-equal. PRD reference: §D3.
R2. The NL invariant¶
A6_NL is always provisioned at $0.80 USD, regardless of:
- the org's
organization.configuration.mailer_price, - the org's bucket (A, B, or C),
- the live Stripe subscription Price unit amount.
A6_NL is the only billing key with a structural-not-negotiable price. The mailer_price per-org override map (PRD D4) does not apply to it. Bucket-B custom-rate-portable orgs that include NL still get NL at $0.80; only their non-NL SKUs follow the bucket-B mailer_price path.
If an operator needs to deviate (e.g. a one-off contractual exception), they must pass --unit-amount explicitly to provision_meter.py. The default-resolution path never produces a non-$0.80 NL price.
R3. Bucket → unit_amount resolution¶
For every (org, template_size) pair the M2 provisioner computes the unit amount as follows:
| Bucket | Predicate | Unit amount (cents) |
|---|---|---|
| A — default-portable | mailer_cents is NULL or == default_cents, AND sub_cents == default_cents |
defaults.default_unit_amount_cents (R1) |
| B — custom-rate-portable | sub_cents == mailer_cents AND mailer_cents != default_cents |
mailer_cents (except A6_NL: see R2) |
| C — blocked | sub_cents != mailer_cents (incl. sub_cents is None) |
not provisioned; out of auto-migration |
Where:
default_cents=TEMPLATE_SIZE_BILLING_DEFAULTS[template_size].default_unit_amount_cents(R1)mailer_cents=int(round(organization.configuration.mailer_price * 100))ifmailer_priceis set, else NULLsub_cents= the live Stripe subscription item'sprice.unit_amountfor the canonical per-SKU item, falling back to the legacysent_maileritem'sunit_amount, falling back to NULL
Authoritative copy in code: the R1 defaults are the constants in TEMPLATE_SIZE_BILLING_DEFAULTS (app/core/integrations/stripe/meters.py), enforced at provisioning time by app/methods/billing_rate_card.py; mailer_cents ↔ default_cents drift surfaces as a canonical-drift diagnostic in app/methods/billing_preflight.py. The original M2.6 per-org provisioner (_resolve_unit_amount_for_org, with its interactive --assume-yes drift confirmation) was carved off to project/brandon-billing-migration-scripts (ENG-3035) once the migration completed.
R4. Currency¶
Every billing key today is priced in USD, including UK and EU SKUs. This is intentional and matches the current subscription cadence. Multi-currency support is out of scope until M4 rollout review.
R5. Unknown sizes fail closed¶
If a template_size is not present in TEMPLATE_SIZE_BILLING_DEFAULTS, the provisioner must raise — it must not default to any value. PRD reference: §R1, §R6.
Where these rules are enforced¶
| Surface | Rule(s) enforced | File |
|---|---|---|
| Stripe meter + product + Price create path | R1, R4, R5 | app/methods/billing_rate_card.py (_provision_stripe_meter_chain) |
| Per-org provisioning + drift surfacing | R1, R2, R3, R5 | app/methods/billing_rate_card.py, app/methods/billing_preflight.py |
| Default constants table | R1, R4 | app/core/integrations/stripe/meters.py |
One-time migration/triage CLIs (provision_meter, inspect_preflight, seed_rate_card_entries) |
— | carved off to project/brandon-billing-migration-scripts (ENG-3035) |
When changing any rule above, update this file first, then update the code so the values match. The rule file is the spec; the code is the implementation.