Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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.

Leave a comment!

4
Login or Register to join the conversation
Kiuega Avatar
Kiuega Avatar Kiuega | posted 3 years ago | edited

Hello, I'm using SweetAlert2 so it's not the same thing.

I've this code :

                    Swal.fire({
                        title: 'Change to '+planName,
                        text: message,
                        icon: "info",
                        showCancelButton: true,
                        showLoaderOnConfirm: true,
                        preConfirm: function()
                        {
                            // todo - actually change the plan!
                            return $.ajax({
                                url: changeUrl,
                                method: 'POST',
                            }).done(function(){
                                Swal.fire({
                                    title: 'Plan changed !',
                                    icon: 'success',

                                },function() {
                                    location.reload();
                                });
                            });
                        }
                    });```


But I noticed that with this way of doing things, I cannot display the second alert correctly.
But I'm doing well in done () because I can do console.log (). But the second SweetAlerte does not want to appear.
And no errors in the console

EDIT: The problems was caused by the SweetAlert2 package installation.

I did :`npm remove sweetalert2` and `npm install --save sweetalert2` and now it works
Reply

Hey Kiuega

Interesting solution, great that you found how to fix it.

However why are you using return inside preConfirm?

Cheers!

Reply
Default user avatar
Default user avatar Christophe Lablancherie | posted 5 years ago

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 :(

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

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.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9, <7.4",
        "symfony/symfony": "3.1.*", // v3.1.10
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.8
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.2
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.3.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.26
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "friendsofsymfony/user-bundle": "~2.0.1", // v2.0.1
        "stof/doctrine-extensions-bundle": "^1.2", // v1.2.2
        "stripe/stripe-php": "^3.15", // v3.23.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.2.1
        "phpunit/phpunit": "^5.5", // 5.7.20
        "composer/package-versions-deprecated": "^1.11" // 1.11.99
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.4
        "symfony/phpunit-bridge": "^3.0", // v3.3.0
        "hautelook/alice-bundle": "^1.3", // v1.4.1
        "doctrine/data-fixtures": "^1.2" // v1.2.2
    }
}
userVoice