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.

So, how much would that Upgrade Cost?

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

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
}
}

Upcoming Invoice to the Rescue

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.

How Upgrades Work

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.

Fetching the Upcoming Invoice

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
}
}

Dumping the Upcoming Invoice

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.

Leave a comment!

0
Login or Register to join the conversation
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