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

DanielRønfeldt's avatar

Laravel Cashier - how to handle an "insufficient funds" exception?

Hello again everyone :)

I'm using Laravel Cashier with Stripe - test mode enabled.

Subscribing a user works well for a successful payment, but it turns out that even when a card with insufficient funds 4000008260003178, according to Stripe documentation is being used, the user still gets subscribed. What I obviously want, is to prevent this from happening, and provide the user with insighful feedback if such situation occurs in production mode.

Long story short, I'm trying to figure out why any of the code within the try{} block that comes after the $user->newSubscription('Testing Premium', 'plan_identifier')->create('payment_identifier'); line is never reached. It's like that line is exiting the try{} block. What am I missing?

Here's my updateSubscription() method in the controller:

public function updateSubscription( Request $request ) {
    
  $user = $this->eloquentUser(); // coming in from a protected method within the controller
  
  $plan_id = env('STRIPE_PREMIUM_PLAN_ID'); // gets the chosen plan the user is subscribing to
  $payment_id = $request->get('payment'); // gets the payment method the user is paying with
  
  if( ! $user->subscribed('Testing Premium') ) { // If the user is NOT subscribed yet...
    try { // ... trying to CREATE a NEW subscription
      $user->newSubscription( 'Testing Premium', $plan_id )->create( $payment_id );
      
      // Everything else within this try{} block is not executed
      // CODE BELOW IS NEVER EXECUTED
      
      \Debugbar::info( 'This part of code is never reached. WHY?!?' );
      
      if( $user->subscribed('Testing Premium') ) {
        \Debugbar::info( 'Hey, a new subscription was created and confirmed! :-)' );
        $user->am_plan = 'premium';
        $user->save();
      } else {
        \Debugbar::info( 'Handle exception: subscription creation FAILURE ' );
        return response()->json([
          'subscription_updated' => false
        ]);
      }
      
      // CODE ABOVE IS NEVER EXECUTED
      
    } catch( PaymentActionRequired $e ) {
      // Handle exception: PAYMENT ACTION REQUIRED
      \Debugbar::info( 'Handle exception: PAYMENT ACTION REQUIRED ' . $e );
    } catch( PaymentFailure $e ) {
      // Handle exception: PAYMENT FAILURE
      \Debugbar::info( 'Handle exception: PAYMENT FAILURE ' . $e );
    } catch( \Exception $e ) {
      // Handle exception: OTHER EXCEPTION
      \Debugbar::info( 'Handle exception: OTHER EXCEPTION ' . $e );
    }
  } else { // Otherwise, i.e. the user is ALREADY SUBSCRIBED...
    try { // ... try swapping the plans
      \Debugbar::info( 'The user is ALREADY SUBSCRIBED, trying swapping the plans' );
      $user->am_plan = 'premium';
      $user->save();
      $user->subscription( 'Testing Premium' )->swap( $plan_id );
    } catch( SubscriptionUpdateFailure $e ) {
      // Handle exception: SUBSCRIPTION UPDATE FAILURE
      \Debugbar::info( 'Handle exception: SUBSCRIPTION UPDATE FAILURE ' . $e );
    }
  }
  
  return response()->json([
    'subscription_updated' => true
  ]);
}
0 likes
7 replies
martinbean's avatar

@danielrønfeldt I don’t really understand your code to be honest. If this a controller action to update a subscription, why are you checking and letting a user create a brand-new one?

You should have two distinct endpoints: one for subscribing, and one for swapping plans if your application allows that.

public function store(CreateSubscriptionRequest $request)
{
    $paymentMethod = $request->input('payment_method_id');

    $subscription = $request->user()
        ->subscription('sub_name', $request->input('plan_id'))
        ->create($paymentMethod);

    return SubscriptionResource::make($subscription)
        ->response()
        ->setStatusCode(201);
}

public function update(UpdateSubscriptionRequest $request)
{
    $subscription = $request->user()->subscription('sub_name');

    $subscription->swap($request->input('new_plan_id'));

    return new SubscriptionResource($subscription);
}

Any Stripe exceptions, you can handle in the exception handler and return an appropriate response:

use Illuminate\Validation\ValidationException;
use Stripe\Exception\CardException;

public function register()
{
    $this->renderable(function (CardException $e, $request) {
        throw ValidationException::withMessages([
            'payment_method_id' => [
                $e->getMessage(),
            ],
        ]);
    });
}

You can then render any issues with the payment method (card) next to your Stripe Element input as if it were a “real” validation error message:

<div id="card-element"></div>
@foreach($errors->get('payment_method_id') as $error)
    <div class="d-block invalid-feedback" role="alert">{{ $error }}</div>
@endforeach

Relevant Cashier documentation:

2 likes
DanielRønfeldt's avatar

@martinbean I agree that my naming convention is misleading. I am actually only using a single paid plan, and therefore there's no need for updating/swapping subscriptions. I only need to create them.

Your code makes sense, but I believe it's important for me to mention that I am using Laravel 6.x - not a personal choice, but upgrading to 8.x is unfortunately a no-go.

I see that you're suggesting I'd make use of SubscriptionResources, but can you please elaborate on that? An example of one, would be great! Or is this an embedded Laravel 8.x capability?

Thank you so much!

martinbean's avatar

I am actually only using a single paid plan, and therefore there's no need for updating/swapping subscriptions. I only need to create them.

@danielrønfeldt Then I don’t understand why you have code for swapping a subscription? 🤷‍♂️

Your code makes sense, but I believe it's important for me to mention that I am using Laravel 6.x - not a personal choice, but upgrading to 8.x is unfortunately a no-go.

I never mentioned anything about Laravel versions?

I see that you're suggesting I'd make use of SubscriptionResources, but can you please elaborate on that? An example of one, would be great! Or is this an embedded Laravel 8.x capability?

They’re just neat little classes to create consistent representations of models. They‘re available in Laravel 6.x: https://laravel.com/docs/6.x/eloquent-resources

DanielRønfeldt's avatar

@martinbean I wrote the code for swapping subscriptions just in case I needed it at a later time. Which now I see that it was a bit silly for me to do it, since all it did, it was adding unnecessary complexity to my code.

I wrongfully assumed that, since you linked the documentation for the Laravel 8.x, your suggested code was tailored for functionality that's specific to that version.

Regarding Eloquent Resources, I'm now a bit more familiar with them, thank you. But after reading through their documentation, I still can't understand what's the benefit of using one for my specific use-case scenario.

In any case, what I'm trying to achieve, is to return bespoke results out of the subscription creation event, into my controller, so that I can "enable" the "premium" features to the new subscriber. Those results must of course depend on the success or failure of the actual payment. And then, of course, I would very much prefer to have access to the specific messages which are (hopefully) provided by Stripe in case the payment is unsuccessful for some reason, like, for instance - as my original post says - "insufficient funds".

The $request comes in from a Vue-driven View, which is using Axios to the API's store() method within the UserPaymentController.php Controller. Here's my revised method in the controller:

public function store( Request $request ) {
  $paymentMethod = $request->get('payment_method_id');
  $user = $this->eloquentUser(); // coming in from a protected method within the controller
  $plan_id = env('STRIPE_PREMIUM_PLAN_ID'); // gets the chosen plan the user is subscribing to
  
  $subscription = $user->subscription('test_premium', $plan_id)->create($paymentMethod);
  // ANY CODE BELOW IS NOT EXECUTED FOR SOME REASON
  \Debugbar::info( 'Subscription was created, this info should show up in the debug bar.' );
  return SubscriptionResource::make($subscription)->response()->setStatusCode(201);
}
jlrdw's avatar

@danielrønfeldt look to stripe for some of these things:

https://stripe.com/docs/api/payouts/failures#payout_failures-insufficient_funds

See if Stripe has a "playground" or "sandbox" like paypal, and practice some of these things.

Edit: In particular read this part:

https://stripe.com/docs/payouts#payout-failures

Note also unless customer is using one of those prepaid cards, it's more rare to have insufficient funds. But I guess more people these day are using prepaid cards.

2 likes
DanielRønfeldt's avatar

@jlrdw I admit my fault for my question not to be focused on the primary issue. But thank you nevertheless for your insigtful answer.

My problem is actually caused by the fact that no code is being executed after the following line, which makes me wonder whether or not that is the correct way of subscribing a user.

$user->newSubscription( 'Testing Premium', $plan_id )->create( $payment_id ); // no more code is executed beyond this line

In other words, is there any way of returning a result out of the new subscription creation event? Maybe chain some other method to it?

Thank you!

Please or to participate in this conversation.