Subscription Change Spec: Proration and Renewal Edge Cases
Every subscription bug I have shipped lived in the gap between "customer wants to change plan" and "billing system agrees on the number." The code is never the hard part. The spec is. If you cannot tell me in writing what happens when a $30 plan upgrades to $100 ten days into a cycle, you will ship a bug. This is how I write that spec.
Review Note
Reviewed May 6, 2026. This article is now part of the public topic path for the Spec-First Development Hub. It was rechecked for concrete examples, internal links, and indexable metadata before returning to the sitemap and feed.
Start With the Four Change Vectors
There are only four things a customer can do to a subscription, and each has different billing semantics. I list them explicitly before anything else, because I have watched teams conflate "pause" with "cancel" and argue about it in a postmortem.
- Upgrade: larger plan, higher MRR. Effective immediately. Prorated delta charged now, renewal date stays put.
- Downgrade: smaller plan. Effective at end of cycle. No money moves today. New price applies at renewal.
- Cancel: access ends at end of cycle, not on the click. Subscription stays
activewithcancel_at_period_end=trueuntil the boundary. - Pause: subscription freezes. No charges. Resume restarts the cycle from the pause point, not from zero.
Write those four behaviors as the first section of the spec. If a PM disagrees with any, you have found a conversation worth having before code exists.
Pick One Proration Rule and Commit
Proration has three common flavors, and the "which one" decision is load-bearing. Pick one, document it, and never let it turn into folklore.
- By seconds: most precise, matches Stripe's default. Unused time is calculated to the second.
- By days: simpler mental model for support staff. A 30-day cycle has 30 equal slices.
- Flat: no proration. Full charge on upgrade, no refund on downgrade. Fine for low ACV consumer products. Indefensible for B2B.
I prefer by-seconds for accuracy and by-days for support sanity. Whatever you pick, the spec must state the rounding rule (banker's rounding, floor to the cent, etc.) and the currency precision. A $0.005 disagreement between your service and your payment provider will haunt reconciliation for a year.
The Renewal Boundary Is Where Everything Breaks
The classic bug: a customer clicks upgrade at 23:59:58 UTC on renewal day. The renewal job fires at 00:00:00 and charges the old plan for a new cycle. The upgrade handler fires at 00:00:01 and charges a new cycle at the new plan. Double-billed, boundary moved, webhooks out of order.
The spec must say which job owns the boundary. My rule: if a change request arrives within a 5-minute grace window on either side of renewal, the change is deferred until after renewal posts, then applied as a fresh change. The spec must define the window, who checks it, and what the UI shows when a change is queued.
Downgrades and the Credit Question
When a customer downgrades, you owe them something. The spec must name what that something is. Three defensible options:
- Customer credit. Applied to the next invoice. My default. Keeps cash on the books and reduces chargeback risk.
- Next-invoice line item. Same outcome, different accounting. Tax implications differ — check with finance.
- Refund to original payment method. Highest trust signal, worst unit economics. Reserve for annual plans or regulated markets.
Whatever you pick, the spec must say what happens to the credit balance if the customer cancels before the next invoice. Refund, forfeit, or hold? Silence on this question is always the wrong answer.
Trials Are Their Own Spec Chapter
Trial edges are where I see the most improvisation. Customer in a 14-day trial upgrades on day 5. New trial? Trial ends now? Paid clock from day 5 or day 14? Write down every combination:
- Upgrade during trial: trial continues, new plan applies when trial ends. No charge today.
- Downgrade to a no-trial plan: trial honored to original end date, then new plan starts paid. Do not charge mid-trial.
- Trial extension: support tools only, logged separately. Never a side effect of a plan change.
- Cancel during trial: ends immediately or at trial end? Pick one and show it in the UI.
Dunning, Tax, and the States Nobody Draws
What happens when a past-due customer tries to upgrade? In my spec: the upgrade is blocked until the failed invoice clears. Otherwise you are extending credit to someone already in collections. The spec should state every subscription state and which transitions are legal from each: active, past_due, paused, canceled, incomplete.
Tax is the other quiet landmine. VAT and GST rates depend on plan value and sometimes on customer location. The spec must say when tax is recomputed: on every plan change, only at renewal, or both. I recompute on every change. It is a tiny cost and it prevents the "why is my VAT different from my invoice" support ticket.
Webhook Order Is Part of the Contract
When a plan changes, downstream services need to know in a specific order. Fire invoice.created before plan.changed and a CRM that listens to plan.changed will try to read an invoice that does not yet exist. My canonical order for an upgrade:
customer.updated— reflect the new plan tier on the customer record.invoice.created— the proration invoice exists.invoice.payment_succeeded— money cleared.subscription.updated(plan changed) — the authoritative state change.
The spec must declare the order, whether it is guaranteed or best-effort, and how consumers should handle out-of-order delivery. Idempotency keys on every webhook are not optional.
Acceptance Criteria With Real Math
This is the test I put in every subscription spec. It is the "customer on $30/mo upgrades to $100/mo 10 days into a 30-day cycle" case, fully specified.
Given an active subscription on the $30/month plan
And the current cycle started 10 days ago (20 days remaining)
And proration is calculated by seconds
And the customer is not in a trial, dunning, or paused state
When the customer upgrades to the $100/month plan
Then an immediate invoice is created for the prorated delta:
unused_old = $30 * (20 / 30) = $20.00 credit
used_new = $100 * (20 / 30) = $66.67 charge
net due now = $66.67 - $20.00 = $46.67
And the renewal date does NOT move
And the next invoice on the renewal date is $100.00
And webhooks fire in order: customer.updated, invoice.created,
invoice.payment_succeeded, subscription.updated
And tax is recomputed on the $46.67 line, not the old $30 line
A spec with one worked example like this catches more bugs in review than four pages of prose. Write the numbers. Show your arithmetic.
Final Takeaway
Subscription changes are not complicated because the math is hard. They are complicated because every decision hides a policy choice that feels obvious to the person making it and invisible to everyone else. The spec's job is to drag those choices into daylight: which proration rule, which effective date, which credit treatment, which webhook order, which boundary ownership. Write them down once, argue about them once, and stop shipping the same billing bug every quarter.
Rollout Evidence for Billing Changes
I do not let subscription changes launch on unit tests alone. The release plan needs evidence that finance, support, and downstream systems see the same state engineering sees.
- Invoice preview: at least one upgrade, downgrade, trial conversion, and past-due blocked change, with exact line items attached to the PR.
- Webhook trace: a sandbox run that shows event order, idempotency keys, and consumer acknowledgements.
- Ledger reconciliation: internal ledger total equals processor total for the sandbox customer after every transition.
- Support script: the answer support gives when a customer asks why their invoice contains a credit or prorated charge.
That evidence sounds heavy until the first invoice dispute lands. Then it is simply the fastest way to answer the customer without opening three dashboards and guessing.
Topic Path
Read the hub first, then use these adjacent examples and templates to place this article inside the full workflow.
Keep Reading
Editorial Note
- Author details: Spec Coding Editorial Team
- Editorial policy: How we review and update articles
- Corrections: Contact the editor