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.
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.
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 | |
} | |
} |
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!
I have managed to solve this by using the ending_balance
field of the Invoice. Also, I have noticed a separate issue using amount_due
when downgrading from a plan to another having the same duration. amount_due
works fine when there is an upgrade, but not when there is a downgrade.
Here is my code (apologies for the ugly nested else condition AND please note that I am using different logic in the condition that compare the plan durations)
$total = $invoice->total;
// contains the pro-rations
// *plus* - if the duration matches - next cycle's amount
if ($userPlan->getDuration() === $newPlan->getDuration()) {
$total -= $newPlan->getPrice() * 100;
} else {
if ($invoice->ending_balance < 0) {
$total = $invoice->ending_balance;
}
Hey Martin,
Thanks for sharing your solution with others! Though, proration is a bit more complex than just price difference between 2 plans. It also takes into account how many "unused" time left from the old subscription, so the actual logic should be more complex, otherwise it will show the correct proration only in the beginning of your old subscription.
I hope this helps!
Cheers!
I've further improved the code removing my ugly nested else / if conditions. The previous solution I posted was also working - this one is just cleaner. I can confirm that this has been tested using 16 different scenarios including 6 days after initial subscription and the total always comes out calculated correctly and matching what Stripe actually charges.
// contains the pro-rations *plus* the next cycle's amount *plus* any negative ending balance
$total = $invoice->amount_due + $invoice->ending_balance;
// 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;
}
Plan upgrade with same duration
Plan downgrade with same duration
Plan changed to lower duration
Plan changed to higher duration
Plan upgrade + higher duration
Plan upgrade + lower duration
Plan downgrade + higher duration
Plan downgrade + lower duration
(everything tested moments after the initial subscription + 6 days after initial subscription)
Hey Kiuega!
Hmm. This could be due to another change in newer versions of the API. In the version of the API used in this tutorial, the total
property that's returned from Stripe would be a negative number when going from yearly to monthly. After completing the change, that negative balance would be added to the "Customer" so that future invoices would automatically be lower. If you're seeing 0 for total
, I bet that data is stored in another place or there is some other little tweak that needs to be made to get things working correctly.
Let me know what you find out :). We use this exact feature here on SymfonyCasts.com but using an older API version.
Cheers!
Okay! I will also come back to this in a while to see if there is anything to do for the new version and I will tell you here
Hello @KNP team,
first of all, this tutorial is awesome and so well explained. It`s one of my favorite tutorials right now :)
But strange things happen now. When I switch from the yearly plan to the monthly, it says "You will have an approximate balance of $0 that will be automatically applied to future invoices!". Why does he get a $0 balance, switching from yearly to monthly? Shouldn`t the customer get notified about the potential forthcoming credit balance change when the subscription plan switches to monthly, i.e. "You will have an approximate balance of 890$ that will be automatically applied to future invoices!" ?
Because, when I proceed the following step and look into Stripe it actually says that the credit balance is $890.99, in the "preview", sweet alert does not reflect this (instead it gives me a 0 credit balance back).
The problem is, that when I switch from yearly to monthly, Stripe actually gives me in the upcoming invoice an amount_due of 0.
The two lines are (edited)
Because of amount_due = 0, previewPlanChangeAction in ProfileController:
/**
* @Route("/profile/plan/change/preview/{planId}", name="account_preview_plan_change")
*/
public function previewPlanChangeAction($planId)
{
$plan = $this->get('subscription_helper')
->findPlan($planId);
$stripeInvoice = $this->get('stripe_client')
->getUpcomingInvoiceForChangedSubscription(
$this->getUser(),
$plan
);
$currentUserPlan = $this->get('subscription_helper')
->findPlan($this->getUser()->getSubscription()->getStripePlanId());
// contains the prorations
// *plus* - if the duration matches - the next cycle's amount
$total = $stripeInvoice->amount_due;
dump($stripeInvoice);
if ($plan->getDuration() == $currentUserPlan->getDuration()) {
$total -= $plan->getPrice() * 100;
}
return new JsonResponse(['total' => $total/100]);
}
returns a JSON Response of total = 0.
But don`t we want to inform the customer about his existing credit balance, after switching his plan?
Many thanks in advance,
Chris
Hey Kiuega !
Ah, we missed replying to the original comment! A fail for us! As I just replied above - https://symfonycasts.com/sc... - I'm guessing that on a newer API version, the data must be available somewhere else. As Chris mentioned, after you complete the change from yearly to monthly, the account balance *is* correctly applied to the user. So the question is: where is Stripe exposing that data before the change? It's *possible* that they're not exposing it now, but that would surprise me.
Do you see any other data in the upcoming invoice that's useful?
Cheers!
In src/AppBundle/StripeClient.php on line 167 you compare
$stripeSubscription->current_period_start == $currentPeriodStart.
But $currentPeriodStart on line 158 equals $stripeSubscription->current_period_start.
Do you sure what comparison "if(true)" has the meaning?
Hey alexchromets
Nice question, and actually it look's like it is but we have a note about it on our code blocks. The thing here is that the currentPerdiodStart may change after saving the new plan, and it has an special meaning for Stripe:
// 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) {
Cheers!
// 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
}
}
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 ?