Laravel Cashier Webhook: Get current user

3.7k views Asked by At

I'm using the following to override the default handleCustomerSubscriptionDeleted method by placing the following in app/Http/Controllers/WebHookController.php:

<?php namespace App\Http\Controllers;

use Laravel\Cashier\WebhookController;
use Symfony\Component\HttpFoundation\Response;

class StripeWebhooks extends WebhookController {

  /**
   * Handle stripe subscription webhook
   *
   * @param  array  $payload
   * @return Response
   */
  public function handleCustomerSubscriptionDeleted($payload)
  {
    $billable = $this->getBillable($payload['data']['object']['customer']);

    if ($billable && $billable->subscribed()) {
      $billable->subscription()->cancel();

    }

    return new Response('Webhook Handled', 200);
  }

}

In order for this to actually override the default, should I be updating my route to my extended controller, or still reference the default? I don't think that leaving it at the defaults would even let Laravel know my controller exists, but wanted to confirm as well.

Current:

Route::post('stripe/webhook', '\Laravel\Cashier\WebhookController@handleWebhook');

The other part of this is within handleCustomerSubscriptionDeleted I'd also like to be able to grab the current user this is referencing and perform other actions; in this case, automatically setting records to unpublished. What would be the best way to go about retrieving the user? It looks like $payload['data']['object']['customer'] could potentially hold and relate to the stripe_id column in the users table, but would like to confirm. Thanks for any help!

Update

I believe (based on the docs), it should be more like this:

<?php namespace App\Http\Controllers;

use App\Models\Dashboard\Listing;
use App\User;
use Symfony\Component\HttpFoundation\Response;
use Laravel\Cashier\WebhookController as BaseController;

class WebhookController extends BaseController
{
  /**
   * Handle stripe subscription webhook
   *
   * @param  array  $payload
   * @return Response
   */
  public function handleCustomerSubscriptionDeleted($payload)
  {
    $billable = $this->getBillable($payload['data']['object']['customer']);

    if ($billable && $billable->subscribed()) {
      $billable->subscription()->cancel();

      // Get current user
      $user = User::find($billable);

      // Set each listing to draft
      foreach ($user->listings as $listing) {
        $current_listing = Listing::find($listing->id);
        if ($current_listing->status == 'published') {
          $current_listing->status = 'draft';
          $current_listing->save();
        }
      }
    }

    return new Response('Webhook Handled', 200);
  }
}

I then updated my route to the following:

Route::post('stripe/webhook', 'WebhookController@handleWebhook');

But it's still not firing. BUT, what I'm also wondering is if handleCustomerSubscriptionDeleted is called "when they cancel" or after their grace period is over and the actual subscription is cancelled. Is there a more reliable way for me to test this than to play the waiting game locally?

Update #2

I have updated my override class to the following:

<?php namespace App\Http\Controllers;

use App\Models\Dashboard\Listing;
use App\User;
use Symfony\Component\HttpFoundation\Response;
use Laravel\Cashier\WebhookController as BaseController;

class WebhookController extends BaseController
{

  /**
   * Handle stripe subscription webhook
   *
   * @param  array  $payload
   * @return Response
   */
  public function handleCustomerSubscriptionDeleted(array $payload)
  {
    $billable = $this->getBillable($payload['data']['object']['customer']);

    if ($billable && $billable->subscribed()) {
      $billable->subscription()->cancel();

      // Get current user
      $user = User::where('stripe_id', $billable)->firstOrFail();

      // Set each listing to draft
      foreach ($user->listings as $listing) {
        $current_listing = Listing::find($listing->id);
        if ($current_listing->status == 'published') {
          $current_listing->status = 'draft';
          $current_listing->save();
        }
      }
    }

    return new Response('Webhook Handled', 200);
  }
}

The part I changed was changing $billable to search for the user by, as that's what the response returns -- not the user ID as I once thought. I did end up trying localtunnel.me as @Shaz mentioned which did allow me to send a request to it, BUT without being able to pass in the customer ID I'd like impact, I'm not sure how I can verify everything is actually working. I may try to dig in to see if I can manually run an event through Cashier, but still seems a bit odd.

Update #3

Tried doing something a little simpler as far as listening to customer.subscription.created (since that fires immediately):

<?php namespace App\Http\Controllers;

use App\Models\Dashboard\Listing;
use App\User;
use Symfony\Component\HttpFoundation\Response;
use Laravel\Cashier\WebhookController as BaseController;

class WebhookController extends BaseController
{

  /**
   * @param array $payload
   */
  public function handleCustomerSubscriptionCreated(array $payload) {
    $billable = $this->getBillable($payload['data']['object']['customer']);

    if ($billable) {
      // Get current user
      $user = User::where('stripe_id', $billable)->firstOrFail();

      $user->first_name = 'Helloooooo';
      $user->save();
    }
  }

  /**
   * Handle stripe subscription webhook
   *
   * @param  array  $payload
   * @return Response
   */
  public function handleCustomerSubscriptionDeleted(array $payload)
  {
    $billable = $this->getBillable($payload['data']['object']['customer']);

    if ($billable && $billable->subscribed()) {
      $billable->subscription()->cancel();

      // Get current user
      $user = User::where('stripe_id', $billable)->firstOrFail();

      // Set each listing to draft
      foreach ($user->listings as $listing) {
        $current_listing = Listing::find($listing->id);
        if ($current_listing->status == 'published') {
          $current_listing->status = 'draft';
          $current_listing->save();
        }
      }
    }

    return new Response('Webhook Handled', 200);
  }
}

I used localtunnel.me to setup the webhook, but it doesn't appear that it is correctly responding to the webhook, as I see 500 errors in the Stripe response logs even though my "Test Webhooks" event fired from the Stripe dashboard (with no customer ID set obviously) is fine. The response I'm getting about the 500 error is unfortunately lost/cut-off in the jumbled mess of source code that Laravel is spitting out:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta name="robots" content="noindex,nofollow" />
        <style>
            /* Copyright (c) 2010, Yahoo! Inc. All rights reserved. Code licensed under the BSD License: http://developer.yahoo.com/yui/license.html */
            html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var{font-style:normal;font-weight:normal;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:text-top;}sub{vertical-align:text-bottom;}input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit;}input,textarea,select{*font-size:100%;}legend{color:#000;}

            html { backgr...
3

There are 3 answers

6
Shaz MJ On

Reading the answer to this question - the event is fired on the date/time the subscription actually ends, not when the customer initiates the cancellation.

Based off Laravels Cashier Docs - your route should actually be:

Route::post('stripe/webhook', 'Laravel\Cashier\WebhookController@handleWebhook');

Make sure your webhook settings in stripe actually point to yourapp.com/stripe/webhook

Based on the handleWebhook method in the Laravel\Cashier code - yes, any appropriately named method (begins with handle, camelcased to a strip event) will override an existing one.


If you want to use your extended controller in the routes; remember to add a constructor at the beginning of the class:

public function __construct(){
  parent::__construct();
}

So the handleWebhook method is available when you do

Route::post('stripe/webhook', 'WebhookController@handleWebhook');

instead of calling the Laravel\Cashier\WebhookController in the route.

0
Mantas D On
$billable = $this->getBillable($payload['data']['object']['customer']);
$user = $billable; // billable is User object
1
dranieri On

I have this working and I am pointing the route to my Controller.

Route::post(
    'stripe/webhook',
    'WebhookController@handleWebhook'
);

Since we are extending the base controller any methods in the base controller are already available. The point of this controller is to have access to all of the base controller methods. This way you can extend any new methods or overwrite existing ones.

I also had an issue with getting my code to catch webhook events. Something to note is that if you are in the test environment of stripe and triggering webhook events make sure that you add the env variable.

CASHIER_ENV=testing

In the base webhook controller it checks first to make sure the events exists and the event wont exist if the webhook is being generated within the test environment.

public function handleWebhook(Request $request)
{
    $payload = json_decode($request->getContent(), true);

    if (! $this->isInTestingEnvironment() && ! $this->eventExistsOnStripe($payload['id'])) {
        return;
    }

    $method = 'handle'.studly_case(str_replace('.', '_', $payload['type']));

    if (method_exists($this, $method)) {
        return $this->{$method}($payload);
    } else {
        return $this->missingMethod();
    }
}