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

mandys's avatar

Swap Stripe subscription to new plan doesn't swap name

The function swap($plan) in vendor/laravel/cashier/src/Subscription.php does the swap correctly but only updates stripe_plan but not 'name'.

eg:

create 2 plans -

starter (plan_code) => Starter (name) pro (plan_code ) => Pro ( name )

Upgrade to starter. All good.

From Starter upgrade to Pro - all good.

But, in database, subscriptions table 'name' is still set as Starter.

It should have been updated to Pro.

This causes issue if you are trying to build a billing page as that code is relying on pulling info based on plan name which is Starter but in actual user moved to Pro plan. So, it returns empty.

Can be fixed easily by updating name as well.

$this->fill([ 'stripe_plan' => $plan, 'ends_at' => null, ])->save();

Can be changed to

$this->fill([ 'name' => $planName ( needs to be passed here ) 'stripe_plan' => $plan, 'ends_at' => null, ])->save();

I am using latest version of laravel ( 5.2 ).

Thanks.

0 likes
10 replies
Francismori7's avatar

Cashier's name table is not the plan itself, it's the name of the subscription for your code to use. Think of it as a way to have multiple subscriptions, one for videos and another one for topics for an example:

$videoSubscription = $user->subscription('videos');
$topicSubscription = $user->subscription('topics');

Note that you must specify it when creating a new subscription using newSubscription(). By default, it will be default.

mandys's avatar

I understand that. And as shown in my example above, my 2 plans have a name of 'Starter' and 'Pro' and the plan_code in stripe is set to 'starter' and 'pro' ( see lower case for plan_code ).

And I do use the $user->subscription('planName') to get the subscription.

And if the planName is correct in db and is matched with stripe it works fine.

However, what you are missing is that

public function swap($plan) Subscription.php line 209

Doesn't update the name of the plan in the database but swaps the plan_code correctly.

So, next time the above method is called on the planName of the previous plan type rather than the update planName, hence returning user is not subscribed.

Makes sense ?

richard@gorbutt.com's avatar

I too have noticed this tonight. Perform a plan swap and the the database does not update with the new name. It remains as old name which gives me a headache in that the subscribed('planname") check comes back with old plan.

The Stripe Subscriptions does indeed show correctly.

Anyone else seen this? I haven't implemented webhooks yet, is this needed for this? Docs do not imply,

Also, I noticed that the swap does not need the CC token. Does it use the CC on file with Stripe?

Francismori7's avatar

You completely missed my point. The name column is an IDENTIFIER, not a "pretty printed name". It's used so you can have multiple subscriptions for each user.

skattabrain's avatar

Hmmm... so I'm running into this now and in any case I'm just looking to be clear.

This would mean I'm using it incorrectly when I do a...

$user->newSubscription($plan_title, $stripe_plan)->create($data['stripeToken']);

I was expecting that the "name" was the name of the plan itself. If it's not that name of the plan, what is the intent? Plan Type? So it's really less of a name and more of a type... where you can be subscribed to 1 type at a time.

Is the following correct?

You might have a site that sells widgets and you have all types of plans to sell widgets... you might have several plans for "listing widgets"... "1", "5", and "10" where the numbers reflect the number of widgets you can list on this site.

You might also have some plans for the "widgets book club" where you can have several plans for the number of books you can rent out at a time.

So basically in this case it would allow you to create 2 subscriptions... 1 for listing and 1 for the book club, when using ->swap().

Sound about right?

Francismori7's avatar

You would then use book-club-subscription or something like that and widget-listing-subscription;. Those are STATIC id's that you refer from within your code. The plan name "display name" is not part of Cashier itself, for now.

Feliciano's avatar

One solution is to change the return of the subscription function in Billable.php. By default, it returns $value->name === $subscription, where name is the column in the subscription table. By changing to $value->stripe_plan === $subscription you can get the stripe_plan column, which has the actual user active plan.

In Billable.php:


public function subscription($subscription = 'default')
    {
        return $this->subscriptions->sortByDesc(function ($value) {
            return $value->created_at->getTimestamp();
        })
        ->first(function ($key, $value) use ($subscription) {
           // This is the line:
            return $value->stripe_plan === $subscription;
        });
    }

If you don't want to change the vendor code you can create your own subscription function, but i guess the principle is the same.

1 like
ambitionphp's avatar

I came across this same issue. My method to fix was modifying Subscription.php to update the name column as well.

    public function swap($plan)
    {
        $subscription = $this->asStripeSubscription();

        $subscription->plan = $plan;

        $subscription->prorate = $this->prorate;

        if (! is_null($this->billingCycleAnchor)) {
            $subscription->billingCycleAnchor = $this->billingCycleAnchor;
        }

        // If no specific trial end date has been set, the default behavior should be
        // to maintain the current trial state, whether that is "active" or to run
        // the swap out with the exact number of days left on this current plan.
        if ($this->onTrial()) {
            $subscription->trial_end = $this->trial_ends_at->getTimestamp();
        } else {
            $subscription->trial_end = 'now';
        }

        // Again, if no explicit quantity was set, the default behaviors should be to
        // maintain the current quantity onto the new plan. This is a sensible one
        // that should be the expected behavior for most developers with Stripe.
        if ($this->quantity) {
            $subscription->quantity = $this->quantity;
        }

        $subscription->save();

        $this->user->invoice();

        $this->fill([
            // add name column here to fix
            'name' => $plan,
            'stripe_plan' => $plan,
            'ends_at' => null,
        ])->save();

        return $this;
    }
joeylott's avatar

Stripe does call the webhook with "type": "customer.subscription.updated" and all the correct information.

However, out of the box, it does not seem that Cashier handles that request. So nothing happens.

Also, because it is a webhook, it is async, and I assume that for most scenarios, most of us want the subscription name to get updated immediately.

Still, it is useful to know that the webhook does get called for this.

And you can test webhook on dev using ultrahook.com. I've found it to be useful for this purpose. I want to simulate the production environment as much as possible so that I don't introduce new elements (like webhook) when moving to staging or production.

On dev, it's easy to log all the webhook requests to see what they are and if I want the app to handle them (if they aren't already handled).

In any case, I do find it strange that Cashier doesn't synchronously update the subscription name. Maybe I have misunderstood the intention of the subscription name. But it certainly seems that it is linked to the subscription type. Why would I want to have a subscription name that doesn't change with the type? What is the use of that?

joeylott's avatar

Aha. Now I understand.

Obviously, this is a common mistake. But now I see what the intention is.

Francismori7 is correct. Go back and read his responses.

Whenever something that is being used by hundreds or thousands or hundreds of thousands of people seems to have a bug, I have to ask myself, "Is it the code or is it my understanding that is flawed?"

And logically, in such a scenario, it is very likely that it is my understanding that is flawed. Such was the case here.

The subscription name should NOT change when swapping the plan. Think about it: you call swap() from the (named) subscription object. So why would that object's name change? It shouldn't. What should change is only the Stripe plan.

The subscription object is, as Francismori7 pointed out, its own entity, and it should not be confused as merely a label for the Stripe plan.

In most scenarios a user will have just one subscription, but in some cases might have multiple subscriptions representing access to different things.

For example, a user might have a subscription to video lessons and a separate subscription to a forum. Each could have its own levels of access, determined by a stripe plan. For example, maybe (and this is a weak example, but all that comes to mind quickly) one could have read-only access to forums or one could have read-write access. Each is a different plan, but the subscription is the same - forum.

Want to upgrade from read-only to read-write? Okay, then call auth()->user()->subscription("forum")->swap("read-write")

Please or to participate in this conversation.