Skip to content

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)) if mailer_price is set, else NULL
  • sub_cents = the live Stripe subscription item's price.unit_amount for the canonical per-SKU item, falling back to the legacy sent_mailer item's unit_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_centsdefault_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.