If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Head over to the account template. When the user clicks upgrade, we need to make
an AJAX call to our new endpoint. To get that URL, find the button and add a new
attribute: data-preview-url
set to path('account_preview_plan_change')
, passing
a planId
wildcard set to otherPlan.planId
:
... lines 1 - 33 | |
{% block body %} | |
<div class="nav-space"> | |
<div class="container"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
... lines 39 - 54 | |
<table class="table"> | |
<tbody> | |
<tr> | |
<th>Subscription</th> | |
<td> | |
{% if app.user.hasActiveSubscription %} | |
{% if app.user.subscription.isCancelled %} | |
... lines 62 - 64 | |
{% else %} | |
... lines 66 - 69 | |
<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 }}" | |
> | |
Change to {{ otherPlan.name }} | |
</button> | |
{% endif %} | |
... lines 77 - 78 | |
{% endif %} | |
</td> | |
</tr> | |
... lines 82 - 105 | |
</tbody> | |
</table> | |
</div> | |
... lines 109 - 117 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 122 - 123 |
Cool! Copy that new attribute name and go back up to the JavaScript section. Let's
read that attribute: var previewUrl = $(this).data('preview-url')
. And while
we're here, create a planName
variable set to $(this).data('plan-name')
:
... lines 1 - 2 | |
{% block javascripts %} | |
... lines 4 - 7 | |
<script> | |
jQuery(document).ready(function() { | |
... lines 10 - 15 | |
$('.js-change-plan-button').on('click', function(e) { | |
e.preventDefault(); | |
swal('Loading Plan Details...'); | |
var previewUrl = $(this).data('preview-url'); | |
var planName = $(this).data('plan-name'); | |
... lines 23 - 28 | |
}) | |
}); | |
</script> | |
{% endblock %} | |
... lines 33 - 123 |
Now, make that AJAX call! I'll use $.ajax()
with url
set to previewUrl
. Chain
a .done()
to add a success function with a data
argument. And just to try
things out, open sweet alert with a message: Total $
then data.total
, since the
endpoint returns that field:
... lines 1 - 2 | |
{% block javascripts %} | |
... lines 4 - 7 | |
<script> | |
jQuery(document).ready(function() { | |
... lines 10 - 15 | |
$('.js-change-plan-button').on('click', function(e) { | |
e.preventDefault(); | |
swal('Loading Plan Details...'); | |
var previewUrl = $(this).data('preview-url'); | |
var planName = $(this).data('plan-name'); | |
$.ajax({ | |
url: previewUrl | |
}).done(function(data) { | |
swal('Total $'+data.total); | |
}); | |
}) | |
}); | |
</script> | |
{% endblock %} | |
... lines 33 - 123 |
Ok team, try that out. Refresh the account page and click "Change to New Zealander". Bam! Total $50!
With the frontend somewhat functional, let's finish the logic in our endpoint.
At the bottom, Symfony keeps a list of the AJAX requests. Click the 4f4
sha link
to get more information about our AJAX request. Then, click the Debug link on the
left.
In the last chapter, we dumped the upcoming \Stripe\Invoice
object that we got
from the Stripe API. This is it! It looks a little funny, but the data is hiding
under the _values
property, and it holds a couple of really interesting things.
First, check out amount_due
, and remember, everything is stored in cents, not
dollars. This is the amount we'll show to the user. But if it seems a little too
high, you're right. Keep watching.
Second, the invoice line items can be found under the lines
key. And there are
three.
The first line item is negative: its a credit for any unused time on your current plan. If you're half-way through a month, then the second half should be applied as a credit. This is that credit. Since we just signed up a few minutes ago, this is just slightly less than the full price of $99.
The second line item is a charge for the new plan, for however much time is left in the month. Again, if we're upgrading half-way through the month, I should only need to pay for half of the new plan in order to use it for the last half of the month.
The third line item, well, this is where things get ugly. This is a charge for a full month on the new plan: $199.
What? Why is that here? Why would I pay for half of the month of the New Zealander plan and also for a full month?
Here's what's going on: when a customer upgrades, Stripe does not charge them anything immediately. Instead, Stripe allows you to switch, but then, at the end of the month, it will charge you for the partial, prorated month you just used, plus the full next month, minus the partial-month refund for your original plan.
Phew! That's why you see three line items: the first two for adjusting to the new plan for part of the month, plus the cost for the full-price renewal.
Honestly, this feels weird to me. So let's do something better: let's charge the customer immediately for the plan price change, and then let them pay for the normal, full-month renewal next month. This is totally possible to do.
But that means, to show the user the amount they will be charged right now, we need
to read the amount_due
value and then subtract the full price of the plan,
to remove the extra line item.
In ProfileController
, add a new variable $total
set to $stripeInvoice->amount_due
:
... lines 1 - 13 | |
class ProfileController extends BaseController | |
{ | |
... lines 16 - 116 | |
public function previewPlanChangeAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
$stripeInvoice = $this->get('stripe_client') | |
->getUpcomingInvoiceForChangedSubscription( | |
$this->getUser(), | |
$plan | |
); | |
// contains the pro-rations *plus* the next cycle's amount | |
$total = $stripeInvoice->amount_due; | |
... lines 130 - 134 | |
} | |
} |
Add a comment above - this stuff is confusing, so let's leave some notes. Then, correct
the total by subtracting $plan->getPrice() * 100
to convert into cents - our price
is stored in dollars:
... lines 1 - 13 | |
class ProfileController extends BaseController | |
{ | |
... lines 16 - 116 | |
public function previewPlanChangeAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
$stripeInvoice = $this->get('stripe_client') | |
->getUpcomingInvoiceForChangedSubscription( | |
$this->getUser(), | |
$plan | |
); | |
// contains the pro-rations *plus* the next cycle's amount | |
$total = $stripeInvoice->amount_due; | |
// subtract plan price to *remove* next the next cycle's total | |
$total -= $plan->getPrice() * 100; | |
... lines 133 - 134 | |
} | |
} |
Then, return $total / 100
in the JSON:
... lines 1 - 13 | |
class ProfileController extends BaseController | |
{ | |
... lines 16 - 116 | |
public function previewPlanChangeAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
$stripeInvoice = $this->get('stripe_client') | |
->getUpcomingInvoiceForChangedSubscription( | |
$this->getUser(), | |
$plan | |
); | |
// contains the pro-rations *plus* the next cycle's amount | |
$total = $stripeInvoice->amount_due; | |
// subtract plan price to *remove* next the next cycle's total | |
$total -= $plan->getPrice() * 100; | |
return new JsonResponse(['total' => $total/100]); | |
} | |
} |
Let's try it guys: go back and refresh.
Click "Change to New Zealander". Ok, $99.93
- that looks about right. Remember,
the upgrade should cost about $100, but since we've been using the old plan for
a few minutes, the true cost should be slightly lower.
Ok! It's time to execute this upgrade! To save us some time, I'll paste some JavaScript into the AJAX 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 - 23 | |
$.ajax({ | |
url: previewUrl | |
}).done(function(data) { | |
var message; | |
if (data.total > 0) { | |
message = 'You will be charged $'+data.total +' immediately'; | |
} else { | |
message = 'You will have a balance of $'+(Math.abs(data.total))+' that will be automatically applied to future invoices!'; | |
} | |
swal({ | |
title: 'Change to '+planName, | |
text: message, | |
type: "info", | |
showCancelButton: true, | |
closeOnConfirm: false, | |
showLoaderOnConfirm: true | |
}, function () { | |
// todo - actually change the plan! | |
}); | |
}); | |
}) | |
}); | |
</script> | |
{% endblock %} | |
... lines 49 - 139 |
This first display how much we will charge the user. And check this out: it could be positive, meaning we'll charge them, or negative for a downgrade, meaning they'll get an account credit that will automatically be used for future charges.
Finally, this shows the user one last alert to confirm the change. If they click "Ok", the last callback will be executed. And it'll be our job to send one more AJAX call back to the server to finally change their plan.
Let's do it!
Hey Kiuega!
We do some pretty advanced stuff like this in our site too with the number of "seats" on your team (you can increase/decrease them). To be honest, this stuff is VERY complex, and we have learned how to do it through trial and error - much like what you're doing. In other words, make a change, look at the API response, and reverse-engineer which "properties" appear to be the ones that you want. Stripe's docs are good... but sometimes things are so advanced, that it's not perfectly clear what to use. While it's very strange for there to be 2 different properties depending on an increase vs decrease, it doesn't surprise me too much. The BEST thing about Stripe is how stable their API is. If you find that this situation works for you today, it will continue to work for you tomorrow. We add a lot of logging to our codebase in these types of situations: if we find that an assumption "looks" wrong, we log an error that goes to our Slack channel so that we can look into it and, if necessary, tweak the codebase. That doesn't happen often - you get to a pretty stable situation pretty fast. Also, we're on a different API version that you're using - so I can't compare the exact fields you're using with the one's we're using - there could definitely be differences across API versions.
Sorry I can't give you a better answer... but I *do* think what you're doing is the right direction.
Cheers!
Hello weaverryan
I think I have found the right solution that can work in any case!
From now on, instead of recovering the "amount_due" value which can sometimes be problematic, I recover the "total" which corresponds to the price to be paid (without counting the balance of the user).
In StripeClient.php I created a function which returns the amount of a customer's balance. And another function which will calculate the real price from a total price taking into account the balance of the user.
And it works !
I also take into account the trial periods. This means that a trial user who wants to test with more seats can. But when previewing the next invoice, I tell him "You have X trial days left. After that, you will be debited with X €".
Now, I don't know if the "total" attribute also takes into account a reduction coupon that would have been applied. Otherwise, this fact must still be taken into account.
EDIT : The reduction coupon is taken into account! No need to code more, everything seems operational!
// 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
}
}
Hey! In case we have a plan that has price ranges (depending on the units). Imagine a user who has a subscription with 150 units. He wants to modify his subscription and take only 100 units but for the same plan (or else for another plan with units, or just change plan for another plan with a fixed price, without quantity), in this case we must specify the quantity in our function, what that it happens.
So, should I have something that looks like this:
Now, in my controller, on the <b>previewPlanChange function</b>
It works if I increase the quantity. On the other hand, if I decide to decrease the quantity, then, systematically, the "amount_due" will be equal to 0 while the "total" will have the good value taking into account the prorations.
The point is if I change :<b>$total = $stripeInvoice->amount_due;</b> by<b>$total = $stripeInvoice->total;</b>
I'm not sure if it's "correct".
Note that I only have this problem in the preview of the invoice. If I confirm, Stripe will do the prorations, the calculations according to what I have to pay, what I have left in my balance, etc. The problem really lies in the preview of the invoice when changing plans