If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Honestly, upgrade and downgrading a plan would be really easy, except that we need to calculate how much we should charge the user and tell them so they can confirm.
Getting this right takes some work, but the result is going to be gorgeous, I promise. Here's the plan: as soon as this screen loads, we'll make an AJAX call back to the server. The server will calculate how much to charge the customer for the upgrade, and send that back so we can show it.
In ProfileController
, add a new public function called previewPlanChangeAction()
.
Set the URL to /profile/plan/change/preview/{planId}
and give it a name:
account_preview_plan_change
:
... lines 1 - 13 | |
class ProfileController extends BaseController | |
{ | |
... lines 16 - 113 | |
/** | |
* @Route("/profile/plan/change/preview/{planId}", name="account_preview_plan_change") | |
*/ | |
public function previewPlanChangeAction($planId) | |
{ | |
... lines 119 - 130 | |
} | |
} |
Use the $planId
in the route to load a $plan
object with $this->get('subscription_helper')
and then call findPlan()
with $planId
:
... lines 1 - 13 | |
class ProfileController extends BaseController | |
{ | |
... lines 16 - 116 | |
public function previewPlanChangeAction($planId) | |
{ | |
$plan = $this->get('subscription_helper') | |
->findPlan($planId); | |
... lines 121 - 130 | |
} | |
} |
Ok ok, but I'm ignoring the big, huge elephant in the room: how the heck are we going to figure out how much to charge the user? I mean, I certainly don't want to try to calculate how far through the month the user is and figure out a prorated amount. Fortunately, we don't have to: Stripe has a killer feature to help us out.
Open the Stripe Api docs and find Invoices. Check out this "Upcoming Invoices" section. Cool. With upcoming invoices, we can ask Stripe to tell us what the Customer's next invoice will look like.
This could be used to show the user how much they'll be charged on renewal, or,
by passing a subscription_plan
parameter, this will return an Invoice that describes
how much they would be charged for changing to that plan.
A big part of all of this is prorating. In the subscription documentation, Stripe talks a lot about what will happen in different scenarios. By default, Stripe does prorate, which means that if we are 1/4th through the month on the Farmer Brent Plan and we upgrade, then 3/4th's of that cost should be credited as a discount towards paying for the final 3/4th's of a month of the New Zealander plan. When you switch between plans that have the same duration, like a monthly plan to another monthly plan, the billing period doesn't change: you simply switch to the new plan right in the middle of the month, and are billed normally again on your normal billing date.
Yea, it's hard! The tl;dr is that Stripe does these calculations for us.
Let's use this endpoint: in StripeClient
, add a new function:
getUpcomingInvoiceForChangedSubscription()
with two arguments: the User
that
will be upgrading and the SubscriptionPlan
they want to change to:
... lines 1 - 4 | |
use AppBundle\Entity\User; | |
use AppBundle\Subscription\SubscriptionPlan; | |
... lines 7 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 135 | |
public function getUpcomingInvoiceForChangedSubscription(User $user, SubscriptionPlan $newPlan) | |
{ | |
... lines 138 - 142 | |
} | |
} |
Inside, it's easy: return \Stripe\Invoice::upcoming()
and pass it a few parameters.
First, customer
set to $user->getStripeCustomerId()
and second, subscription
set to $user->getSubscription()->getStripeSubscriptionId()
:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 135 | |
public function getUpcomingInvoiceForChangedSubscription(User $user, SubscriptionPlan $newPlan) | |
{ | |
return \Stripe\Invoice::upcoming([ | |
'customer' => $user->getStripeCustomerId(), | |
'subscription' => $user->getSubscription()->getStripeSubscriptionId(), | |
... line 141 | |
]); | |
} | |
} |
This tells Stripe which subscription we would update. Now, in our system, every user should only have one, but it doesn't hurt to be explicit.
The last option is subscription_plan
: in other words, which plan do we want to
change to. Set it to $newPlan->getPlanId()
:
... lines 1 - 8 | |
class StripeClient | |
{ | |
... lines 11 - 135 | |
public function getUpcomingInvoiceForChangedSubscription(User $user, SubscriptionPlan $newPlan) | |
{ | |
return \Stripe\Invoice::upcoming([ | |
'customer' => $user->getStripeCustomerId(), | |
'subscription' => $user->getSubscription()->getStripeSubscriptionId(), | |
'subscription_plan' => $newPlan->getPlanId(), | |
]); | |
} | |
} |
Back in ProfileController
, use this to set a new $stripeInvoice
variable via
this->get('stripe_client')->getUpcomingInvoiceForChangedSubscription()
passing
it $this->getUser()
and the new $plan
:
... 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 | |
); | |
... lines 127 - 130 | |
} | |
} |
So, what does this fancy Upcoming invoice actually look like? Let's find out by
dumping it. Then, return a JsonResponse
with... I don't know, how about a total
key set to a hardcoded 50
for now. Oh, and make sure you dump $stripeInvoice
:
... 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 | |
); | |
dump($stripeInvoice); | |
return new JsonResponse(['total' => 50]); | |
} | |
} |
Ok, let's keep going by hooking up the frontend and finishing the cost calculation.
"Houston: no signs of life"
Start the conversation!
// 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
}
}