Buy

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:

157 lines src/AppBundle/Controller/ProfileController.php
... 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:

157 lines src/AppBundle/Controller/ProfileController.php
... 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:

158 lines src/AppBundle/StripeClient.php
... 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():

158 lines src/AppBundle/StripeClient.php
... 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():

158 lines src/AppBundle/StripeClient.php
... lines 1 - 146
$stripeSubscription = $this->findSubscription($user->getSubscription()->getStripeSubscriptionId());
$stripeSubscription->plan = $newPlan->getPlanId();
... lines 150 - 158

Finally, send that to Stripe with $stripeSubscription->save():

158 lines src/AppBundle/StripeClient.php
... 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:

158 lines src/AppBundle/StripeClient.php
... 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:

158 lines src/AppBundle/StripeClient.php
... 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:

158 lines src/AppBundle/StripeClient.php
... 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):

157 lines src/AppBundle/Controller/ProfileController.php
... 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:

108 lines src/AppBundle/Subscription/SubscriptionHelper.php
... 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():

157 lines src/AppBundle/Controller/ProfileController.php
... 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:

157 lines src/AppBundle/Controller/ProfileController.php
... 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:

152 lines app/Resources/views/profile/account.html.twig
... 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'):

152 lines app/Resources/views/profile/account.html.twig
... 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:

152 lines app/Resources/views/profile/account.html.twig
... 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:

152 lines app/Resources/views/profile/account.html.twig
... 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.

Leave a comment!

  • 2016-11-09 weaverryan

    Hey Christophe!

    Hmm. Well, I *do* know this error: it happens when you have no invoice items to invoice. The most likely cause is that Stripe is automatically creating the Invoice for you... and then when you try to create the Invoice, all the InvoiceItems have been charged already. This is *exactly* what we talk about here: https://knpuniversity.com/s.... But, Stripe should only automatically create the invoice if the duration changes (e.g. monthly to yearly). Is your duration staying the same (e.g. upgrading from one monthly plan to another)?

    Let me know! It could be a small bug somewhere in your code... or potentially a behavior change in Stripe (but I hope not!)

    Cheers!

  • 2016-11-08 Christophe Lablancherie

    Hi,

    I've one problem with this chapter 26... When i try to execute the plan change, i take an error : Nothing to invoice for customer. Could you help me to understand what happened ?

    In Stripe i can see the upgrade of the customer plan, but in my DB i think there is no change... I don't understand because i've made the same change in my code :(