Stripe plan change proration service
Stripe is a popular payment system. And very mature one. This service has a lot of built-in functionality that all contemporary businesses needs online. One example of these functionalities is subscriptions along with plans. When a subscription plan gets changed in the middle of the billing cycle, we might want to charge the client with the new plan amount only for the rest part of the current billing cycle and return the not used amount of the current billing cycle. So the price of subscription is fair and calculated daily.
For example, current plan costs 10$. Assuming the client has already paid it. The new plan costs 20$. The client switches plan in the middle of the billing cycle. The new plan daily amount for the remaining time of the current billing cycle is 1/2 of 20$, it’s 10$. As it’s a plan upgrade and thew new plan costs more, we could just charge them with these 10$. But wait, there is not used daily amount of the current plan that’s 1/2 of 10$, it’s 5$. In other words, the client has already paid some money for the remaining time of the current billing cycle. So, we deduct that money from the charge. As the result, in this example, we need to charge the client only 5$. It’s calculated as 10$ (new plan remaining time of the current billing cycle) - 5$ (already paid money for that period). This process of plan change calculations is called prorations.
Graphically it can be explained as follow:
Stripe can calculate prorations for us. But that feature is available only for per-seat plans. In this post, you will see how to build a service written in Ruby from scratch that calculates prorations. It also demonstrates a ruby service object design pattern in action.
Use Ruby programming language with some tricks we come up with the following class:
class PlanChangeProration extend Dry::Initializer extend Memoist option :user option :plan_id def unused_time_cost -(unused_time_line_item&.amount || 0) end def total [remaining_time_cost - unused_time_cost, 0].max end private def remaining_time_cost new_plan.price * 100 * new_plan_remaining_time / new_plan_billing_cycle_time.to_f end def new_plan_billing_cycle_time current_plan_used_time + new_plan_remaining_time end def current_plan_used_time proration_date - (unused_time_line_item&.period&.start || proration_date) end def new_plan_remaining_time (remaining_time_line_item&.period&.end || new_plan_period_end) - proration_date end def new_plan_period_end proration_date + (new_plan.is_yearly? ? 365.days.to_i : 30.days.to_i) end memoize def new_plan Plan.find(plan_id) end # For information about upcoming_invoice https://stripe.com/docs/billing/subscriptions/prorations def upcoming_invoice Stripe::Invoice.upcoming( customer: customer.id, subscription: subscription.id, subscription_plan: new_plan.name, subscription_proration_date: proration_date ) end memoize def customer Stripe::Customer.retrieve(user.stripe_customer_id) end memoize def subscription customer.subscriptions.first end memoize def proration_date Time.now.to_i end # Stripe always generates 2 or 3 line items for the "upcoming invoice" memoize def upcoming_line_items return  unless subscription upcoming_invoice.lines.data end # Line item with description "Unused time on Medium Team after 16 Jun 2022" (Medium Team is current plan) def unused_time_line_item upcoming_line_items end # Line item with description "Remaining time on xl_team_without after 16 Jun 2022" (xl_team_without is new plan) def remaining_time_line_item upcoming_line_items end end
And this is how to use it:
service = PlanChangeProration.new(current_user, new_plan_id) service.unused_time_cost # would return 5$ in our example service.total # also 5$, but this is the charge amount