Cut credit card thief chargebacks in Laravel Cashier

Chargebacks - a customer "reverse charge" that your credit card gateway will then essentially fine you for - are never any fun, even when legitimate. But as my friend Bemmu at Candy Japan discovered, when a credit card thief uses your subscription service to test the validity of thousands of cards, it can become a nightmare. A very expensive nightmare.

Of course the thief wasn't interested in buying Japanese candy. He just wanted to see if the card would be accepted, as hassle free as possible. "Hassle free". That was the key to fighting him, I thought.

The idea is simple - create a hassle small enough that your real customers won't mind, but one that there is no way he'll go through hundreds or thousands of times. Something like...an opt-in email! This is a subscription site, after all, and a subscription to a service is much like a subscription to a newsletter.

What we'll do is collect the credit card and other info on sign up, pass it all to our billing gateway for safekeeping, but don't actually assign them a subscription until they confirm the order from their email box. No charge to the card means no chargeback from the angry victim of the fraud.

Let me show you how to set this up in Laravel with the Cashier libraries. I'll assume you already know about how to get Cashier working, and have a Stripe testing account up and running. I am only going to show actual code where it is specific to making it work in the fashion I described above.

To help explain things, I will use "user" to mean the user's table record in your application, and "customer" to refer to the Stripe customer record. Therefore, when accessing or updating the customer, you should be thinking about the record you can see in your Stripe account.

The first change to how we normally do things will occur in your method to create the new user with subscription. We will still create the user, but when we make the customer we will not assign them to a subscription. The psuedo-code:

// before any of this, with a migration, create a field on users to store // the plan name they have signed up for & a nonce for the email // This code can go anywhere, but a method on your User model is probably easiest public function createCustomer($stripeToken) { // from the Billable trait; a plan is actually optional //returns a new \Laravel\Cashier\StripeGateway $stripeGateway = $this->subscription(); //calls Stripe API & creates customer $customer = $stripeGateway->createStripeCustomer($stripeToken, [ 'email' => $this->email ]); // as it says, updates your users table record return $stripeGateway->updateLocalStripeData($customer); }

With your users table setup and this new method created, we are ready for the controller functions.

// Your input comes in with user info, a subscription plan and stripe token // First, create the user, ie $user = \App\User::create(['userinfo']); // Usually you would do this: //$user->subscription('monthly')->create($stripeToken, ['email' => $user->email]); // Instead, only create the customer (stripe token came from form input): $user->createCustomer($stripeToken); // Send an email with a link that has their nonce // Redirect to a page explaining they should check for the confirmation email

At this point you can confirm that the following have happened: you have a new user record, it has a stripe_id & last_four value; in your Stripe dashboard you have a new customer record with that email, but it does not have an assigned subscription. Your customer has received a "thank you" email with a link to confirm the purchase. Now let's prepare for their link click:

// From the nonce, find the user $user = App\User::whereNonce($nonce); $stripeGateway = $user->subscription(); // This is calling Stripe to pull customer record $customer = $stripeGateway->getStripeCustomer($user->stripe_id); $payload = ['plan' => $user->plan, 'prorate' => false, 'quantity' => 1, ]; // Working from inside out, the payload is sent to Stripe to update the customer. // The customer record is returned, which includes the subscription id, // which we then update our local user record $user->setStripeSubscription( $customer->updateSubscription($payload)->id ); // This will update the user stripe_subscription and stripe_plan fields $stripeGateway->updateLocalStripeData($customer, $user->plan);

That's it! Pretty simple, as I promised. Of course, we are only "out-running you, not the bear" - we aren't able to permanently stop the credit card thief, only make our site too big a pain to bother with. Seeing that they have to do the extra work to create and check separate email inboxes for each credit card they want to use, they will find another victim. Sadly, there isn't much we can do about that. Until authorities find a way to better combat cybercrime, this will keep our own site relatively safe.