DEV Community

loading...

Stripe - Upgrading a subscription synchronously (immediately charge for a proration without webhooks)

risafj profile image Risa Fujii Updated on ・3 min read

I was doing some research on how upgrading subscriptions works on Stripe. Stripe's default system is to charge the customer for the proration cost in their next invoice, along with the fee for the upcoming billing cycle (docs).

For example, if I upgraded my subscription to some service from the standard plan ($5/month) to the premium plan ($10/month) at the midpoint of my billing cycle, my invoice on the first of the next month would be something like $12.50. $10 for the charge for the next month, $5 for the premium plan portion I started using half a month ago, and $2.50 refund for the unused portion of the standard plan after the upgrade.

But this billing system could be confusing for customers, and I wanted to see if there was a way to charge customers immediately for the proration ($2.50 in our example above). There was no prorate_now flag or anything, but it could be done manually. Here are the high level steps and a gist to walk you through it. The code will most likely require customization for your use case.

Note: I used Stripe's test environment for this as the docs suggest.

Prerequisites

  • Add the stripe gem to your Rails app by adding the line below to your Gemfile and running bundle.
gem 'stripe'
Enter fullscreen mode Exit fullscreen mode
  • Assign your Stripe publishable and secret keys to environment variables, and create config/initializers/stripe.rb (you can get your keys from the Stripe dashboard):
Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key:      ENV['STRIPE_SECRET_KEY']
  }

Stripe.api_key = Rails.configuration.stripe[:secret_key]
Enter fullscreen mode Exit fullscreen mode
  • In the Stripe test environment, you need to create at least one product, two plans, and a customer who is subscribed to the lower tier plan. These can be created from the Stripe dashboard, or by sending requests to their API; I've linked the relevant docs for making API requests in the previous sentence.

  • The customer also needs to have a credit card so they can be charged (not a real one, but a mock card for testing purposes). If you created the customer via the API, a mock card was probably auto-created for you. You can make sure of this by going to the dashboard and checking that customer's details.

High level steps

  1. Create a preview invoice, which simulates what a customer's next invoice would look like if they upgraded now
  2. Calculate the proration cost from the preview invoice
  3. Create a new one-off invoice with that amount
  4. Charge the customer immediately
  5. Check if the invoice has been paid, then upgrade the customer with proration disabled (otherwise, they will be charged twice for the upgrade!)

Note: Creating a preview invoice does not create an actual invoice, so you don't have to worry about billing the customer accidentally (explained in the docs).

Gist as a proof of concept

The test below follows the steps I described above. The customer is subscribed to my cheaper plan, which costs JPY 100 per month, but they want to upgrade to the plan that costs JPY 1,000, so we charge them for the difference before upgrading them. I also have a User model and a Plan model in my Rails app (which correspond to customer and plan respectively in Stripe) that I occasionally refer to in my test.

And now for the actual code:

I will also link the relevant parts of the Stripe docs, because I personally found it a chore to sift through the detailed but scattered docs.

Read about...

  • creating a preview invoice and calculating the proration cost here
  • creating an one-off invoice here
  • synchronously paying an invoice here
  • checking if an invoice was paid successfully here
  • upgrading a customer with proration disabled here

Please let me know in the comments if any part of the code is unclear, or if there are alternative ways to do this. Thank you!

Discussion

pic
Editor guide
Collapse
marckohlbrugge profile image
Marc Köhlbrugge

Just a heads up for anyone else that gets erratic behavior with this code:

current_prorations = invoice.lines.data.select { |ii| ii.period.start == proration_date }

All line items seem to match the select statement, so it returns whatever line item happens to be first in the array. It seems that order is random. So sometimes you end up getting the pro-rated amount the customer needs to pay, while other times, you get the "unused amount" on the current subscription period, which is basically the discount amount itself.

I think the official Stripe Docs had this same code, but this part has since been removed. They don't provide an alternative, but one way would be to look for the line item with "Remaining time on" in the description.

Collapse
marckohlbrugge profile image
Marc Köhlbrugge

I haven't thoroughly tested, but this seems to work more reliably:

invoice.lines.data.pluck(:amount).keep_if(&:negative?).inject(:+)

👆 This returns the 'unused amount'. So if a customer is halfway through a $25/mo period, this would be about $12.50

It looks through all the line items, selects just the negative amounts, and adds them all up. Usually this is just one line item, but in theory it could be multiple.

invoice.total

👆 This is pretty simple. It's the total amount (including any taxes) the customer were to pay. So continuing the $25/mo example, with let's say a $200/year annual plan, this would be about $200 - $12.50 (see above) = $187.50

Collapse
zer0pants profile image
Mike Roberts

Hey Risa! This was helpful.

I wanted to clarify something in the above though in regards to upgrading a customer with proration disabled.

Check if the invoice has been paid, then upgrade the customer with proration disabled (otherwise, they will be charged twice for the upgrade!)

My understanding is that disabling proration will not prevent the customer from being charged on the next invoice - rather it would pro-rate the amount they will be charged and that amount would find it's way onto the next invoice.

See Stripe docs

With proration disabled, the customer is billed the full amount for the new plan when the next invoice is generated.

Did you find something different and maybe I'm misinterpreting the documentation?

Thanks mate! 😀

Collapse
risafj profile image
Risa Fujii Author

Thanks for the comment :)
Full disclosure, I haven’t used Stripe since I wrote this, so let me refresh my memory and get back to you!

Collapse
risafj profile image
Risa Fujii Author

Regarding the part of the docs you quoted - I believe this is expected behaviour.

With proration disabled, the customer is billed the full amount for the new plan when the next invoice is generated. The customer is still moved to the new plan immediately, however, even though they don’t pay for the new plan until the next billing cycle.

Let me illustrate with the example plans I used in the post ($5/month standard plan and $10/month premium plan).

What would happen normally in Stripe:

  • Sep 1 (start of billing cycle): I sign up with the standard plan and pay $5 for September
  • Sep 15 (midpoint of cycle): I upgrade to the premium plan and don’t pay anything
  • Oct 1 (start of next cycle): I pay roughly $12.50 ($10 for the charge for October, $5 for the premium plan portion I started using half a month ago, and $2.50 refund for the unused portion of the standard plan after the upgrade.)

What we’re trying to achieve in the gist:

  • Sep 1 (start of billing cycle): I sign up with the standard plan and pay $5 for September
  • Sep 15 (midpoint of cycle): I upgrade to the $10 plan and pay roughly $2.50 ($5 charge for the premium plan for the rest of September, minus $2.50 for the unused portion of the standard plan after the upgrade.)
  • Oct 1 (start of next cycle): I pay $10 for October

When they say “the customer is billed the full amount for the new plan”, I think they just mean the customer gets billed normally for the upcoming month, i.e. $10 for October.
The fact that no proration is applied to the next billing cycle’s invoice is also tested in my gist, in the final assertion (JPY 1,000 is the monthly price for the premium plan).

Hope this makes sense! Please let me know if I’m misinformed/you need more clarifications.

Thread Thread
zer0pants profile image
Mike Roberts

Legend! Thanks for such a thorough reply. The proration stuff all makes sense, it's that last point that gets me which is what will show on the final invoice? I want exactly the outcome you describe.

So I think I need to get off this board and prototype this functionality today ;) Thanks again for the post 💪

Update: Confirmed the approach works 🥳

Thread Thread
risafj profile image
Risa Fujii Author

Good luck, let me know if it works! :)

Collapse
hssm profile image
Houssam Salem

Thank you, Risa, for the thorough write-up!

My problem was somewhat different, but I used some of the ideas from here to end up with my solution. I didn't really have the confidence to just manually add in my own proration lines but it turns out it's not an issue if you understand what you're doing (which was easier with your help).

If you end up needing to support SCA you might find yourself following a similar approach. There's little documentation on this since it's very new and I had to contact Stripe support just for the idea (which might obvious in hindsight).

Collapse
rob117 profile image
Rob Sherling

This article is exactly what I needed! Thanks!

Collapse
risafj profile image
Risa Fujii Author

Happy to be of help :)

Collapse
hosseinnedaee profile image
Hossein Nedaee

Thank you, so helpful.

Collapse
risafj profile image
Risa Fujii Author

Thanks, happy to hear it!

Collapse
luccasmaso profile image
Luccas Maso

Nice solution! By the way, how to you handle downgrade cenarios?

Collapse
risafj profile image
Risa Fujii Author

Thanks and sorry, just saw this!

I haven't looked into downgrades, but I believe the default behaviour is to prorate the customer in the next invoice, just like upgrades (i.e. the customer gets refunded for the portion of the original plan that they didn't use).

When a customer’s plan changes during a billing cycle, a proration line item is applied on their next invoice.
stripe.com/docs/billing/subscripti...

I'm not sure about anything beyond what the docs say, so it might be good to contact Stripe's customer support for details.