Buy

Changing your Plan from Monthly to Yearly

So far, we only offer monthly plans. But sheep love commitment, so they've been asking for yearly options. Well, great news! After all that upgrade stuff we just handled, this is going to be easy.

Creating the New Plans

First, in Stripe's dashboard, we need to create two new plans. Call the first "Farmer Brent yearly" and for the total... how about 99 X 10: so $990, per year.

Then, add the New Zealander yearly, set to 1990, billed yearly.

Cool! I'm not going to update our checkout to allow these plans initially, because, honestly, that's super easy: just create some new links to add these plans to your cart, and you're done.

Nope, we'll skip straight to the hard stuff: allowing the user to change between monthly and yearly plans.

Adding the SubscriptionPlan Objects

First, we need to add these plans to our system. Open the SubscriptionPlan class. To distinguish between monthly and yearly plans, add a new property called duration: this will be a string, either monthly or yearly. At the top, I love constants, so create: const DURATION_MONTHLY = 'monthly' and const DURATION_YEARLY = 'yearly':

45 lines src/AppBundle/Subscription/SubscriptionPlan.php
... lines 1 - 4
class SubscriptionPlan
{
const DURATION_MONTHLY = 'monthly';
const DURATION_YEARLY = 'yearly';
... lines 9 - 44
}

Next, add a $duration argument to the constructor, but default it to monthly. Set the property below:

45 lines src/AppBundle/Subscription/SubscriptionPlan.php
... lines 1 - 4
class SubscriptionPlan
{
... lines 7 - 15
private $duration;
public function __construct($planId, $name, $price, $duration = self::DURATION_MONTHLY)
{
... lines 20 - 22
$this->duration = $duration;
}
... lines 25 - 44
}

Finally, I'll use the "Code"->"Generate" menu, or Command+N on a Mac, select "Getters" and then choose duration. That gives me a nice getDuration() method:

45 lines src/AppBundle/Subscription/SubscriptionPlan.php
... lines 1 - 4
class SubscriptionPlan
{
... lines 7 - 40
public function getDuration()
{
return $this->duration;
}
}

In SubscriptionHelper, we create and preload all of our plans. Copy the two monthly plans, paste them, update their keys to have yearly and add the last argument for the yearly duration:

133 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 15
public function __construct(EntityManager $em)
{
... lines 18 - 31
$this->plans[] = new SubscriptionPlan(
'farmer_brent_yearly',
'Farmer Brent',
990,
SubscriptionPlan::DURATION_YEARLY
);
$this->plans[] = new SubscriptionPlan(
'new_zealander_yearly',
'New Zealander',
1990,
SubscriptionPlan::DURATION_YEARLY
);
}
... lines 46 - 131
}

Now, these are at least valid plans inside the system.

The Duration Change UI

Here's the goal: on the account page, next to the "Next Billing at" text, I want to add a link that says "bill yearly" or "bill monthly". When you click this, it should follow the same workflow we just built for upgrading a plan: it should show the cost, then make the change.

In ProfileController::accountAction(), add yet another variable here called $otherDurationPlan:

170 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 19
public function accountAction()
{
$currentPlan = null;
$otherPlan = null;
$otherDurationPlan = null;
... lines 25 - 42
}
... lines 44 - 168
}

This will eventually be the SubscriptionPlan object for the other duration of the current plan. So if I have the monthly Farmer Brent, this will be set to the yearly Farmer Brent plan.

To find that plan, open SubscriptionHelper and add a new function called findPlanForOtherDuration() with a $currentPlanId argument:

133 lines src/AppBundle/Subscription/SubscriptionHelper.php
... lines 1 - 8
class SubscriptionHelper
{
... lines 11 - 74
public function findPlanForOtherDuration($currentPlanId)
{
if (strpos($currentPlanId, 'monthly') !== false) {
$newPlanId = str_replace('monthly', 'yearly', $currentPlanId);
} else {
$newPlanId = str_replace('yearly', 'monthly', $currentPlanId);
}
return $this->findPlan($newPlanId);
}
... lines 85 - 131
}

I'll paste in some silly code here. This relies on our naming conventions to switch between monthly and yearly plans.

Back in the controller, copy the $otherPlan line, paste it, then update the variable to $otherDurationPlan and the method to findPlanForOtherDuration():

170 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 19
public function accountAction()
{
... lines 22 - 23
$otherDurationPlan = null;
if ($this->getUser()->hasActiveSubscription()) {
... lines 26 - 31
$otherDurationPlan = $this->get('subscription_helper')
->findPlanForOtherDuration($currentPlan->getPlanId());
}
... lines 35 - 42
}
... lines 44 - 168
}

Pass that into the template as a new variable:

170 lines src/AppBundle/Controller/ProfileController.php
... lines 1 - 14
class ProfileController extends BaseController
{
... lines 17 - 19
public function accountAction()
{
... lines 22 - 23
$otherDurationPlan = null;
if ($this->getUser()->hasActiveSubscription()) {
... lines 26 - 31
$otherDurationPlan = $this->get('subscription_helper')
->findPlanForOtherDuration($currentPlan->getPlanId());
}
return $this->render('profile/account.html.twig', [
... lines 37 - 40
'otherDurationPlan' => $otherDurationPlan,
]);
}
... lines 44 - 168
}

Cool!

In account.html.twig, scroll down to the Upgrade Plan button. Copy that whole thing. Then, keep scrolling to the "Next Billing at" section. If the user has a subscription, paste the upgrade button:

166 lines app/Resources/views/profile/account.html.twig
... lines 1 - 67
{% block body %}
<div class="nav-space">
<div class="container">
<div class="row">
<div class="col-xs-6">
... lines 73 - 88
<table class="table">
<tbody>
... lines 91 - 116
<tr>
<th>Next Billing at:</th>
<td>
{% if app.user.hasActiveNonCancelledSubscription %}
{{ app.user.subscription.billingPeriodEndsAt|date('F jS') }}
<button class="btn btn-xs btn-link pull-right js-change-plan-button"
data-preview-url="{{ path('account_preview_plan_change', {'planId': otherDurationPlan.planId}) }}"
data-plan-name="{{ otherDurationPlan.name }} {{ otherDurationPlan.duration }}"
data-change-url="{{ path('account_execute_plan_change', {'planId': otherDurationPlan.planId}) }}"
>
Bill {{ otherDurationPlan.duration }}
</button>
... lines 130 - 131
{% endif %}
</td>
</tr>
... lines 135 - 148
</tbody>
</table>
</div>
... lines 152 - 160
</div>
</div>
</div>
{% endblock %}
... lines 165 - 166

And since this process will be the same as upgrading, we can re-use this exactly. Just change otherPlan to otherDurationPlan... in all 4 places. Update the text to "Bill" and then otherDurationPlan.duration. So, this will say something like "Bill yearly".

Dump the Upcoming Invoice

Before we try this, go back into ProfileController and find previewPlanChangeAction(). The truth is, changing a plan from monthly to yearly should be identical to upgrading a plan. But, it's not quite the same. To help us debug an issue we're about to see, dump the $stripeInvoice variable:

170 lines src/AppBundle/Controller/ProfileController.php
... 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
);
dump($stripeInvoice);
... lines 134 - 141
}
... lines 143 - 168
}

And now that I've told you it won't work, let's try it out! Refresh the account page. Then click the new "Bill yearly" link. Ok:

You will be charged $792.05 immediately

Wait, that doesn't seem right. The yearly plan is $990 per year. Then, if you subtract approximately $99 from that as a credit, it should be something closer to $891. Something is not quite right.

Leave a comment!

  • 2017-03-14 Blueblazer172

    Thanks for that beautiful hint :)
    I'll try to dev it :)

  • 2017-03-14 Victor Bocharsky

    Hey Blueblazer172,

    Yeah, I see, that will be a totally no good! So you need to track whether the user has active subscription or no on your website. I think a simple User::$subscriptionStatus field will be enough to tracking that. And you could consider some predefined statuses like: active, active, active_pending_cancellation, canceled, etc. depends on your subscription logic. But also you probably need to store more information in the DB like Stripe customer ID, Stripe subscription ID, Stripe subscription period end, etc.

    Cheers!

  • 2017-03-13 Blueblazer172

    Hey,

    my last question is about how to deny buying a subscriptoion again if there is already one. I think that would be very annoying if the user tests buying twice and it works and he still has one subscription.
    you get what i mean? :P