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.
Execute the Plan Upgrade
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.
When the user clicks "OK", we'll make an AJAX request to the server and then tell Stripe to actually make the change.
In ProfileController
, add the new endpoint: public function changePlanAction()
.
Set its URL to /profile/plan/change/execute/{planId}
and name it account_execute_plan_change
.
Add the $planId
argument:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 137 | |
/** | |
* @Route("/profile/plan/change/execute/{planId}", name="account_execute_plan_change") | |
* @Method("POST") | |
*/ | |
public function changePlanAction($planId) | |
{ | |
// ... lines 144 - 154 | |
} | |
} |
This will start just like the previewPlanChangeAction()
endpoint: copy its $plan
code and paste it here:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 137 | |
/** | |
* @Route("/profile/plan/change/execute/{planId}", name="account_execute_plan_change") | |
* @Method("POST") | |
*/ | |
public function changePlanAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
$stripeClient = $this->get('stripe_client'); | |
$stripeSubscription = $stripeClient->changePlan($this->getUser(), $plan); | |
// causes the planId to be updated on the user's subscription | |
$this->get('subscription_helper') | |
->addSubscriptionToUser($stripeSubscription, $this->getUser()); | |
return new Response(null, 204); | |
} | |
} |
Changing a Subscription Plan in Stripe
To actually change the plan in Stripe, we need to fetch the Subscription, set its plan to the new id, and save. Super easy!
Open StripeClient
and add a new function called changePlan()
with two arguments:
the User
who wants to upgrade and the SubscriptionPlan
that they want to change
to:
// ... lines 1 - 4 | |
use AppBundle\Entity\User; | |
use AppBundle\Subscription\SubscriptionPlan; | |
// ... lines 7 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 144 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
// ... lines 147 - 155 | |
} | |
} |
Then, fetch the \Stripe\Subscription
for the User with $this->findSubscription()
passing it $user->getSubscription()->getStripeSubscriptionId()
:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 144 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
// ... lines 148 - 155 | |
} | |
} |
Now, update that: $stripeSubscription->plan = $newPlan->getPlanId()
:
// ... lines 1 - 146 | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
$stripeSubscription->plan = $newPlan->getPlanId(); | |
// ... lines 150 - 158 |
Finally, send that to Stripe with $stripeSubscription->save()
:
// ... lines 1 - 146 | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
$stripeSubscription->plan = $newPlan->getPlanId(); | |
$stripeSubscription->save(); | |
// ... lines 151 - 158 |
But Charge the User Immediately
Ok, that was easy. And now you probably expect there to be a "catch" or a gotcha that makes this harder. Well... yea... there totally is. Sorry.
I told you earlier that Stripe doesn't charge the customer right now: it waits until the end of the cycle and then bills for next month's renewal, plus what they owe for upgrading this month. We want to bill them immediately.
How? Simple: by manually creating an Invoice and paying it. Remember: when you create an Invoice, Stripe looks for all unpaid invoice items on the customer. When you change the plan, this creates two new invoice items for the negative and positive plan proration. So if we invoice the user right now, it will pay those invoice items.
And hey! We already have a method to do that called createInvoice()
. Heck it
even pays that invoice immediately:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 53 | |
public function createInvoice(User $user, $payImmediately = true) | |
{ | |
$invoice = \Stripe\Invoice::create(array( | |
"customer" => $user->getStripeCustomerId() | |
)); | |
if ($payImmediately) { | |
// guarantee it charges *right* now | |
$invoice->pay(); | |
} | |
return $invoice; | |
} | |
// ... lines 67 - 156 | |
} |
In our function, call $this->createInvoice()
and pass it $user
:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 144 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
$stripeSubscription->plan = $newPlan->getPlanId(); | |
$stripeSubscription->save(); | |
// immediately invoice them | |
$this->createInvoice($user); | |
// ... lines 154 - 155 | |
} | |
} |
Finally, return $stripeSubscription
at the bottom - we'll need that in a minute:
// ... lines 1 - 8 | |
class StripeClient | |
{ | |
// ... lines 11 - 144 | |
public function changePlan(User $user, SubscriptionPlan $newPlan) | |
{ | |
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId()); | |
$stripeSubscription->plan = $newPlan->getPlanId(); | |
$stripeSubscription->save(); | |
// immediately invoice them | |
$this->createInvoice($user); | |
return $stripeSubscription; | |
} | |
} |
Back in the controller, call this with $stripeSubscription = $this->get('stripe_client')
then ->changePlan($this->getUser(), $plan)
:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 141 | |
public function changePlanAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
$stripeClient = $this->get('stripe_client'); | |
$stripeSubscription = $stripeClient->changePlan($this->getUser(), $plan); | |
// ... lines 149 - 154 | |
} | |
} |
Upgrading the Plan in our Database
Ok, the plan is upgraded! Well, in Stripe. But we also need to update the subscription row in our database.
When a user buys a new subscription, we call a method on SubscriptionHelper
called
addSubscriptionToUser()
. We pass it the new \Stripe\Subscription
and the User
:
// ... lines 1 - 8 | |
class SubscriptionHelper | |
{ | |
// ... lines 11 - 60 | |
public function addSubscriptionToUser(\Stripe\Subscription $stripeSubscription, User $user) | |
{ | |
$subscription = $user->getSubscription(); | |
if (!$subscription) { | |
$subscription = new Subscription(); | |
$subscription->setUser($user); | |
} | |
$periodEnd = \DateTime::createFromFormat('U', $stripeSubscription->current_period_end); | |
$subscription->activateSubscription( | |
$stripeSubscription->plan->id, | |
$stripeSubscription->id, | |
$periodEnd | |
); | |
$this->em->persist($subscription); | |
$this->em->flush($subscription); | |
} | |
// ... lines 79 - 106 | |
} |
Then it guarantees that the user has a subscription row in the table with the correct
data, like the plan id, subscription id, and $periodEnd
date.
Now, the only thing we need to update right now is the plan ID: both the subscription ID and period end haven't changed. But that's ok: we can still safely reuse this method.
In ProfileController
, add $this->get('subscription_helper')->addSubscriptionToUser()
passing it $stripeSubscription
and $this->getUser()
:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 141 | |
public function changePlanAction($planId) | |
{ | |
// ... lines 144 - 147 | |
$stripeSubscription = $stripeClient->changePlan($this->getUser(), $plan); | |
// causes the planId to be updated on the user's subscription | |
$this->get('subscription_helper') | |
->addSubscriptionToUser($stripeSubscription, $this->getUser()); | |
// ... lines 153 - 154 | |
} | |
} |
And that's everything. At the bottom... well, we don't really need to return
anything to our JSON. So just return a new Response()
with null
as the content
and a 204
status code:
// ... lines 1 - 14 | |
class ProfileController extends BaseController | |
{ | |
// ... lines 17 - 141 | |
public function changePlanAction($planId) | |
{ | |
// ... lines 144 - 147 | |
$stripeSubscription = $stripeClient->changePlan($this->getUser(), $plan); | |
// causes the planId to be updated on the user's subscription | |
$this->get('subscription_helper') | |
->addSubscriptionToUser($stripeSubscription, $this->getUser()); | |
return new Response(null, 204); | |
} | |
} |
This doesn't do anything special: 204
simply means that the operation was successful,
but the server has nothing it wishes to say back.
Executing the Upgrade in the UI
Copy the route name, then head to the template to make this work.
First, find the button, copy the data-preview-url
attribute, and paste it. Name
the new one data-change-url
and update the route name:
// ... lines 1 - 61 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
// ... lines 67 - 82 | |
<table class="table"> | |
<tbody> | |
<tr> | |
<th>Subscription</th> | |
<td> | |
{% if app.user.hasActiveSubscription %} | |
{% if app.user.subscription.isCancelled %} | |
// ... lines 90 - 92 | |
{% else %} | |
// ... lines 94 - 97 | |
<button class="btn btn-xs btn-link pull-right js-change-plan-button" | |
data-preview-url="{{ path('account_preview_plan_change', {'planId': otherPlan.planId}) }}" | |
data-plan-name="{{ otherPlan.name }}" | |
data-change-url="{{ path('account_execute_plan_change', {'planId': otherPlan.planId}) }}" | |
> | |
Change to {{ otherPlan.name }} | |
</button> | |
{% endif %} | |
// ... lines 106 - 107 | |
{% endif %} | |
</td> | |
</tr> | |
// ... lines 111 - 134 | |
</tbody> | |
</table> | |
</div> | |
// ... lines 138 - 146 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
// ... lines 151 - 152 |
Above in the JavaScript, set a new changeUrl
variable to $(this).data('change-url')
:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 7 | |
<script> | |
jQuery(document).ready(function() { | |
// ... lines 10 - 15 | |
$('.js-change-plan-button').on('click', function(e) { | |
// ... lines 17 - 20 | |
var previewUrl = $(this).data('preview-url'); | |
var changeUrl = $(this).data('change-url'); | |
// ... lines 23 - 56 | |
}) | |
}); | |
</script> | |
{% endblock %} | |
// ... lines 61 - 152 |
Then, scroll down to the bottom: this callback function will be executed if the
user clicks the "Ok" button to confirm the change. Make the AJAX call here: set the
url
to changeUrl
, the method
to POST
, and attach one more success function:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 7 | |
<script> | |
jQuery(document).ready(function() { | |
// ... lines 10 - 15 | |
$('.js-change-plan-button').on('click', function(e) { | |
// ... lines 17 - 24 | |
$.ajax({ | |
url: previewUrl | |
}).done(function(data) { | |
// ... lines 28 - 34 | |
swal({ | |
title: 'Change to '+planName, | |
text: message, | |
type: "info", | |
showCancelButton: true, | |
closeOnConfirm: false, | |
showLoaderOnConfirm: true | |
}, function () { | |
$.ajax({ | |
url: changeUrl, | |
method: 'POST' | |
}).done(function() { | |
// ... lines 47 - 52 | |
}); | |
// todo - actually change the plan! | |
}); | |
}); | |
}) | |
}); | |
</script> | |
{% endblock %} | |
// ... lines 61 - 152 |
Inside that, call Sweet Alert to tell the user that the plan was changed! Let's also add some code to reload the page after everything:
// ... lines 1 - 2 | |
{% block javascripts %} | |
// ... lines 4 - 7 | |
<script> | |
jQuery(document).ready(function() { | |
// ... lines 10 - 15 | |
$('.js-change-plan-button').on('click', function(e) { | |
// ... lines 17 - 24 | |
$.ajax({ | |
url: previewUrl | |
}).done(function(data) { | |
// ... lines 28 - 34 | |
swal({ | |
title: 'Change to '+planName, | |
text: message, | |
type: "info", | |
showCancelButton: true, | |
closeOnConfirm: false, | |
showLoaderOnConfirm: true | |
}, function () { | |
$.ajax({ | |
url: changeUrl, | |
method: 'POST' | |
}).done(function() { | |
swal({ | |
title: 'Plan changed!', | |
type: 'success' | |
}, function() { | |
location.reload(); | |
}); | |
}); | |
// todo - actually change the plan! | |
}); | |
}); | |
}) | |
}); | |
</script> | |
{% endblock %} | |
// ... lines 61 - 152 |
OK! Let's do this! Refresh the page! Click to change to the "New Zealander". $99.88 - that looks right, now press "Ok". And ... cool! I think it worked! When the page reloads, our plan is the "New Zealander" and we can downgrade to the "Farmer Brent".
In the Stripe dashboard, open payments, click the one for $99.88, and open its Invoice. Oh, it's a thing of beauty: this has the two line items for the change.
If you check out the customer, their top subscription is now to the New Zealander plan.
So we're good. Except for one last edge-case.
Hello, I'm using SweetAlert2 so it's not the same thing.
I've this code :