This course is archived!
This tutorial uses an older version of Symfony of the stripe-php SDK. The majority of the concepts are still valid, though there *are* differences. We've done our best to add notes & comments that describe these changes.
Monthly to Yearly: The Billing Period Change
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
We just found out that this amount - $792 - doesn't seem right! Open the web debug toolbar and click to see the profiler for the "preview change" AJAX call that returned this number. Click the Debug link on the left. This is a dump of the upcoming invoice. And according to it, the user will owe $891.05. Wait, that sounds exactly right!
The number we just saw is different because, remember, we start with amount_due
,
but then subtract the plan total to remove the extra line item that Stripe adds.
Back then, we had three line items: the two prorations and a line item for the
next, full month.
But woh, now there's only two line items: the partial-month discount and a charge for the full, yearly period.
Changing Duration changes Billing Period
Stripe talks about this oddity in their documentation: when you change to a plan with a different duration - so monthly to yearly or vice-versa - Stripe bills you immediately and changes your billing date to start today.
So if you're normally billed on the first of the month and you change from monthly to yearly on the 15th, you'll be credited half of your monthly subscription and then charged for a full year. That yearly subscription will start immediately, on the 15th of that month and be renewed in one year, on the 15th.
For us, this means that the amount_due
on the Invoice is actually correct: we don't
need to adjust it. In ProfileController
, create a new variable called $currentUserPlan
set to $this->get('subscription_helper')->findPlan()
and pass it
$this->getUser()->getSubscription()->getStripePlanId()
:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 122 | |
public function previewPlanChangeAction($planId) | |
{ | |
// ... lines 125 - 127 | |
$stripeInvoice = $this->get('stripe_client') | |
->getUpcomingInvoiceForChangedSubscription( | |
$this->getUser(), | |
$plan | |
); | |
$currentUserPlan = $this->get('subscription_helper') | |
->findPlan($this->getUser()->getSubscription()->getStripePlanId()); | |
// ... lines 136 - 146 | |
} | |
// ... lines 148 - 173 | |
} |
Now, if $plan
- which is the new plan - $plan->getDuration()
matches the
$currentUserPlan->getDuration()
, then we should correct the total. Otherwise,
if the duration is changing, the $total
is already perfect:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 122 | |
public function previewPlanChangeAction($planId) | |
{ | |
// ... lines 125 - 133 | |
$currentUserPlan = $this->get('subscription_helper') | |
->findPlan($this->getUser()->getSubscription()->getStripePlanId()); | |
$total = $stripeInvoice->amount_due; | |
// contains the pro-rations | |
// *plus* - if the duration matches - next cycle's amount | |
if ($plan->getDuration() == $currentUserPlan->getDuration()) { | |
// subtract plan price to *remove* next the next cycle's total | |
$total -= $plan->getPrice() * 100; | |
} | |
// ... lines 145 - 146 | |
} | |
// ... lines 148 - 173 | |
} |
Since this looks totally weird, I'll tweak my comment to mention that amount_due
contains the extra month charge only if the duration stays the same.
Ok! Go back and refresh! Click "Bill yearly". Yes! That looks right: $891.06.
Duration Change? Don't Invoice
Because of this behavior difference when the duration changes, we need to fix one
other spot: in StripeClient::changePlan()
. Right now, we manually create an invoice
so that the customer is charged immediately. But... we don't need to do that in
this case: Stripe automatically creates and pays an Invoice when the duration changes.
In fact, trying to create an invoice will throw an error! Let's see it. First, update your credit card to one that will work.
Now, change to Bill yearly and confirm. The AJAX call should fail... and it does! Open the profiler for that request and find the Exception:
Nothing to invoice for customer
Obviously, we need to avoid this. In StripeClient
, add a new variable: $currentPeriodStart
that's set to $stripeSubscription->current_period_start
:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 153 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
$currentPeriodStart = $stripeSubscription->current_period_start; | |
// ... lines 159 - 181 | |
} | |
} |
That's the current period start date before we change the plan.
After we change the plan, if the duration is different, the current period start
will have changed. Surround the entire invoicing block with if
$stripeSubscription->current_period_start == $currentPeriodStart
:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 153 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
// ... lines 156 - 157 | |
$currentPeriodStart = $stripeSubscription->current_period_start; | |
// ... lines 159 - 166 | |
if ($stripeSubscription->current_period_start == $currentPeriodStart) { | |
try { | |
// immediately invoice them | |
$this->createInvoice($user); | |
} catch (\Stripe\Error\Card $e) { | |
$stripeSubscription->plan = $originalPlanId; | |
// prevent prorations discounts/charges from changing back | |
$stripeSubscription->prorate = false; | |
$stripeSubscription->save(); | |
throw $e; | |
} | |
} | |
// ... lines 180 - 181 | |
} | |
} |
In other words: only invoice the customer manually if the subscription period hasn't changed. I think we should add a note above this: this can look really confusing!
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 153 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
// ... lines 156 - 157 | |
$currentPeriodStart = $stripeSubscription->current_period_start; | |
// ... lines 159 - 163 | |
// if the duration did not change, Stripe will not charge them immediately | |
// but we *do* want them to be charged immediately | |
// if the duration changed, an invoice was already created and paid | |
if ($stripeSubscription->current_period_start == $currentPeriodStart) { | |
try { | |
// immediately invoice them | |
$this->createInvoice($user); | |
} catch (\Stripe\Error\Card $e) { | |
$stripeSubscription->plan = $originalPlanId; | |
// prevent prorations discounts/charges from changing back | |
$stripeSubscription->prorate = false; | |
$stripeSubscription->save(); | |
throw $e; | |
} | |
} | |
// ... lines 180 - 181 | |
} | |
} |
Take it for a Test Drive
But, now it should work! Reset things by going to the pricing page and buying a brand new monthly subscription. Now, head to your account page and update it to yearly. The amount - $891 - looks right, so hit OK.
Yes! Plan changed! My option changed to "Bill monthly" and the "Next Billing at" date is August 10th - one year from today. We should probably print the year.
In Stripe, under payments, we have one for $891, and the customer is on the Farmer Brent yearly plan.
Winning!
Hello ! There is something that has not been tested. It is a question of passing from an annual subscription to monthly. And it causes a bug. If I click on "Bill monthly", he says to me:
You will have a balance of $0 that will be automatically applied to future invoices!
Is there a solution ?