Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

SYPOMark's avatar

Switch Team Subscription Error

Hi there,

I don't know if anybody can help. Any pointers around this issue would be helpful, also, it'd be interesting to know if anyone else has come across the same issue.

I've just launched a fresh install of Laravel Spark, using Laravel 5.3 and Spark 3.0. I know these are older versions, but we are using these as we have another install - using the same versions - that we have made many changes to, and I wanted to see if any of our changes had caused the error we are experiencing. It appears not.

The error occurs after successfully having subscribed a team to a plan, when attempting to switch to a different plan. On screen, the error produces this message:

We were unable to update your subscription. Please contact customer support.

In the logs, the error looks like this (sorry, it's quite lengthy):

[2017-08-25 10:41:34] local.ERROR: Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function taxPercentage() on null in /var/www/vhosts/site.com/laravel/vendor/laravel/cashier-braintree/src/Subscription.php:143
Stack trace:
#0 /var/www/vhosts/site.com/laravel/spark/src/Http/Controllers/Settings/Teams/Subscription/PlanController.php(58): Laravel\Cashier\Subscription->swap(Object(Braintree\Plan))
#1 [internal function]: Laravel\Spark\Http\Controllers\Settings\Teams\Subscription\PlanController->update(Object(Laravel\Spark\Http\Requests\Settings\Teams\Subscription\UpdateSubscriptionRequest), Object(App\Team))
#2 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(55): call_user_func_array(Array, Array)
#3 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(44): Illuminate\Routing\Controller->callAction('update', Array)
#4 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Route.php(189): Illuminate\Routing\ControllerDispatcher->dispatch(Object(Illuminate\Routing\Route), Object(Laravel\Spark\Http\Controllers\Settings\Teams\Subscription\PlanController), 'update')
#5 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Route.php(144): Illuminate\Routing\Route->runController()
#6 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(653): Illuminate\Routing\Route->run(Object(Illuminate\Http\Request))
#7 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\Routing\Router->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#8 /var/www/vhosts/site.com/laravel/spark/src/Http/Middleware/CreateFreshApiToken.php(40): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#9 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Laravel\Spark\Http\Middleware\CreateFreshApiToken->handle(Object(Illuminate\Http\Request), Object(Closure))
#10 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#11 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php(41): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#12 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Routing\Middleware\SubstituteBindings->handle(Object(Illuminate\Http\Request), Object(Closure))
#13 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#14 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php(43): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#15 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Auth\Middleware\Authenticate->handle(Object(Illuminate\Http\Request), Object(Closure))
#16 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#17 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php(65): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#18 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Foundation\Http\Middleware\VerifyCsrfToken->handle(Object(Illuminate\Http\Request), Object(Closure))
#19 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#20 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/View/Middleware/ShareErrorsFromSession.php(49): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#21 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\View\Middleware\ShareErrorsFromSession->handle(Object(Illuminate\Http\Request), Object(Closure))
#22 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#23 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php(64): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#24 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Session\Middleware\StartSession->handle(Object(Illuminate\Http\Request), Object(Closure))
#25 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#26 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php(37): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#27 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse->handle(Object(Illuminate\Http\Request), Object(Closure))
#28 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#29 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php(59): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#30 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Cookie\Middleware\EncryptCookies->handle(Object(Illuminate\Http\Request), Object(Closure))
#31 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#32 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#33 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(655): Illuminate\Pipeline\Pipeline->then(Object(Closure))
#34 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(629): Illuminate\Routing\Router->runRouteWithinStack(Object(Illuminate\Routing\Route), Object(Illuminate\Http\Request))
#35 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Router.php(607): Illuminate\Routing\Router->dispatchToRoute(Object(Illuminate\Http\Request))
#36 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(268): Illuminate\Routing\Router->dispatch(Object(Illuminate\Http\Request))
#37 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(53): Illuminate\Foundation\Http\Kernel->Illuminate\Foundation\Http\{closure}(Object(Illuminate\Http\Request))
#38 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/CheckForMaintenanceMode.php(46): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#39 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(137): Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode->handle(Object(Illuminate\Http\Request), Object(Closure))
#40 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php(33): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(Illuminate\Http\Request))
#41 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\Routing\Pipeline->Illuminate\Routing\{closure}(Object(Illuminate\Http\Request))
#42 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(150): Illuminate\Pipeline\Pipeline->then(Object(Closure))
#43 /var/www/vhosts/site.com/laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php(117): Illuminate\Foundation\Http\Kernel->sendRequestThroughRouter(Object(Illuminate\Http\Request))
#44 /var/www/vhosts/site.com/laravel/public/index.php(53): Illuminate\Foundation\Http\Kernel->handle(Object(Illuminate\Http\Request))
#45 {main}

In case it helps, to save you having to dig about, line 143 of /vendor/laravel/cashier-braintree/src/Subscription.php is part of a function called swap, some of which I have pasted below.

    $response = BraintreeSubscription::update($subscription->id, [
        'planId' => $plan->id,
        'price' => (string) round($plan->price * (1 + ($this->owner->taxPercentage() / 100)), 2), //line 143
        'neverExpires' => true,
        'numberOfBillingCycles' => null,
        'options' => [
            'prorateCharges' => $this->prorate,
        ],
    ]);

As far as we can work out, $this->owner is not being set anywhere, hence why we get Call to a member function taxPercentage() on null

Please help! It'd be much appreciated and kind thoughts and thumbs up will be sent your way if you answer.

With kind regards,

Mark

0 likes
15 replies
SYPOMark's avatar

I'm sorry to disturb you, but, is there anyone that can help with this?

What seems to be happening in laravel/vendor/laravel/cashier-braintree/src/Subscription.php at line 143, when Team billing is not enabled, is that all the user's information is assigned to $this->owner. Also, $this refers to the subscription.

So, it seems to me that, when Team billing is enabled, $this->owner should refer to the user who set up the team that is subscribed.

I just don't know how to achieve this. Something seems to be missing in the code, a bug perhaps?

SYPOMark's avatar

Has ANYBODY read this??!?!!

Is there nobody out there who at least has a question to ask in order to point us in the right direction of fixing this? Or, even a reprimand for missing something obvious, or for setting up Spark wrongly in the first place?!? Nothing?

Have I discovered a bug?!?

What's going on!!

Cronix's avatar

I have read it, I just simply haven't encountered this problem. I'm also using Stripe and current version of Spark with Laravel 5.4.

I vaguely remember they did some fixes to tax stuff in 4.x, which doesn't affect me, so I didn't pay attention to what they changed.

I'd suggest upgrading to Spark 4.x. You can also compare the github repo for spark 3 vs spark 4 to see what they changed and see if it might be helpful for your case.

Here are the changes: https://github.com/laravel/spark/compare/v3.0.0...v4.0.13

1 like
SYPOMark's avatar

@Cronix Thanks for your reply. I will take a look at the changes.

In the docs, when talking about teams, there seems to be some confusion between whether Spark should be launched with teams from the outset, or whether they can be, 'added in.' So, I thought it might have been because in our initial install, we were dealing with individual users. No?

With kind regards,

Mark

Cronix's avatar

Adding the CanJoinTeams trait to the User class does the exact same thing as if you use the --team-billing flag when installing. That's actually what it does behind the scenes is add that trait in the user class.

SYPOMark's avatar

@Cronix

Sorry to bother you again, but the link doesn't seem to work. Just ends in a rather funky 404 error page.

Cronix's avatar

It's because spark is a commercial project...so the repo is private. You just need to add your github account to your spark account on spark.laravel.com.

Log into your account, go to your profile and there is a "github" section on the left to add your account. Then you'll have access (if you're logged into github, of course)

SYPOMark's avatar

@Cronix

Thanks for your continued help. I've spent some of this morning downloading laravel/cashier-braintree version 2.0 and laravel/spark version 3.0 from Github (so, the versions that our site's using) and comparing these with what's currently in our site. And, bizarrely, there are some differences. I'm a little confused by this, but I am hoping that by uploading the 'correct' versions of the erroneous files, our problems will just disappear.

Thank-you though, because it is looking like you have pointed us in the right direction, towards a solution.

SYPOMark's avatar

@Cronix

I've uploaded the version of laravel/cashier-braintree from Github and the same error still persists. Still not sure what's going on. I've started comparing our laravel\spark files with those from Github, and only found one minor difference. I'll keep looking, or do you think this might be a wild-goose chase?

Cronix's avatar

I really couldn't tell you, I haven't used anything tax-related with spark.

SYPOMark's avatar

Hi @Cronix

Thanks for continuing to take time out of your day to comment here.

It's not really a tax-related issue, well, I don't think it is anyway!

I've put some logging into the swap function in vendor\laravel\cashier-braintree\src\Subscription.php, only used when a user wants to switch subscriptions:

Log::debug("Subscription this: " . $this);
Log::debug("Subscription this owner: " . $this->owner);
Log::debug("Subscription this owner taxPercentage: " . $this->owner->taxPercentage());

When user billing is enabled, this is what is logged:

[2017-09-06 09:31:52] local.DEBUG: Subscription this:
    {"id":59, "user_id":166, "name":"default", "braintree_id":"24xhgm", "braintree_plan":"Van-Ind-1", "quantity":1, "trial_ends_at":null, "ends_at":null, "created_at":"2017-09-06 09:31:38", "updated_at":"2017-09-06 09:31:38", "provider_plan":"Van-Ind-1"} // so,  the subscription info from the subscriptions table on the database

[2017-09-06 09:31:52] local.DEBUG: Subscription this owner:
    {"id":166, "name":"Peter Parker", "first_name":null, "last_name":null, "company":null, "address_line_1":null, "address_line_2":null, "city":null, "county":null, "postcode":null, "email":"[email protected]", "verified":0, "verify_token":null, "photo_url":"https:\/\/www.gravatar.com\/avatar\/37e453f2edf64ba0e2a0fd7fcda793cf.jpg?s=200&d=mm", "uses_two_factor_auth":false, "two_factor_reset_code":null, "current_team_id":null, "braintree_id":"658162114", "address_id":null, "user_braintree_id":null, "user_address_id":null, "current_billing_plan":"Van-Ind-1", "paypal_email":null, "trial_ends_at":null, "last_read_announcements_at":"2017-09-06 09:31:08", "created_at":"2017-09-06 09:31:08", "updated_at":"2017-09-06 09:31:38, "tax_rate":0}
// so,  the user info from the database including an added tax_rate of 0. No idea how that got there!

[2017-09-06 09:31:52] local.DEBUG: Subscription this owner taxPercentage: 0
// so,  works fine,  because there is an owner.

But, with team billing enabled, this is what is logged instead:

[2017-09-06 09:41:30] local.DEBUG: Subscription this:
    {"id":130, "team_id":72, "name":"default", "braintree_id":"844ngm", "braintree_plan":"Van-Team-1", "quantity":1, "trial_ends_at":null, "ends_at":null, "created_at":"2017-09-06 09:39:28", "updated_at":"2017-09-06 09:39:28", "provider_plan":"Van-Team-1"}
// so,  the subscription info from the team_subscriptions table on the database

[2017-09-06 09:41:30] local.DEBUG: Subscription this owner:
// so,  completely empty 

[2017-09-06 09:41:30] local.ERROR:
    Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function taxPercentage() on null in /var/www/vhosts/rmindr.uk/vanilla-laravel/vendor/laravel/cashier-braintree/src/Subscription.php:146
// fails because the subscription has no owner.

With both types of billing, $this returns the correct subscription from the appropriate database table. With user billing $this->owner returns the user associated with the subscription, with an added tax_rate. Because of there being an owner, the taxPercentage function works fine and returns a value of 0. But, with team billing, $this->owner returns an empty result, it's not even null, so the taxPercentage function fails to run because it has nothing to work with.

What seems to be missing, and, by the looks of it, has ALWAYS been missing, is anything that tells the swap function in the vendor\laravel\cashier-braintree\src\Subscription.php what $this->owner is. It feels like it ought to be the user that is marked in the team_users database table as the appropriate team's owner. But how to tell this function that, and why it doesn't know, 'out of the box,' I am lost!

Any ideas?

With kind regards,

Mark

SYPOMark's avatar

It's taken me a while, but I've finally found a solution.

And, as is my want, I'm posting the code here in case it is of use to someone. It all goes into vendor\laravel\cashier-braintree\src\Subscription.php:

<?php

namespace Laravel\Cashier;

...

use Laravel\Spark\User;
use Laravel\Spark\Team;

...

public function swap($plan)
    {
        ...

        $subscription = $this->asBraintreeSubscription();
        
        /* new code */
        $userId = Team::where('id', $this->team_id)->value('owner_id');
        $Owner = User::where('id', $userId)->first();
        /* end new code */

        $response = BraintreeSubscription::update($subscription->id, [
            'planId' => $plan->id,
            
            /* modified code */
            'price' => (string) round($plan->price * (1 + ($Owner->taxPercentage() / 100)), 2),
            /* end modified code */

            'neverExpires' => true,
            'numberOfBillingCycles' => null,
            'options' => [
                'prorateCharges' => $this->prorate,
            ],
        ]);

It's not the most elegant of solutions, and, if we ever revert back to user billing, we'll have to return this file back to it's original condition, but, it works. If anyone can think of a better solution, I'm all ears!

Thanks @Cronix for your help.

With kind regards,

Mark

EDIT:

I've chosen this, my own post as the best answer, just because it contains code that solves this issue, so, for quick reference for anyone else. BUT it would be worth anyone following this to read @Cronix's post below about it not being recommended to edit vendor files.

Cronix's avatar

I'm glad you found a solution, but you really shouldn't alter vendor files. It will mess upgrades up (overwrite your changes).

What I usually do when I need to alter the core functionality:

  1. copy the file you want to alter to /App/SparkExtensions (or wherever)
  2. change the class definition, to something like:
use Laravel\Spark\Interactions\Settings\Profile\UpdateContactInformation as SparkUpdateContactInformation;

class UpdateContactInformation extends SparkUpdateContactInformation
{

I used their class, but aliased it to 'SparkUpdateContactInformation, and then used that name to extend my own from.

  1. Register your class in the ioc in the SparkServiceProvider's register method
public function register()
    {
        $this->app->singleton(
 'Laravel\Spark\Contracts\Interactions\Settings\Profile\UpdateContactInformation',
            'App\SparkExtensions\UpdateContactInformation'
        );
    }

Now spark (or any laravel app) would use your implementation instead of the native one when it tries to resolve that class from the container.

Edit: I'd also raise this issue on github

2 likes
nikhil_webfosters's avatar
Level 4

@SYPOMark @Cronix : I was having the exact same problem with Spark - Braintree Team Subscription. The solution was simple to change

"BRAINTREE_MODEL=App\Team" .env file. By default, you must have configured it as "App\User". Change it and let me know if that worked fine or now.

It worked fine for me after making those changes.

1 like
SYPOMark's avatar

@nikhil_webfosters Thanks very much for coming back to me on this.

The project is now parked, but when we come back to it, I'll certainly be making the change you suggest.

Why altering the .env file in this small way is not included in the list of suggested files to change when switching between User and Team is a bit beyond me!

Thanks again,

Mark

1 like

Please or to participate in this conversation.