Subscription with coupon using Laravel Cashier & Stripe

We developer always find our selfs to roll some sort of subscription system for web apps running on SaaS (Software as a service) model, it can be simple as subscription for a music store, for mailing app etc. and everyone is offering some sort of coupon to boost their sale, in order to achieve these functionalities we are going to use Strip and Laravel Cashier which make handling subscription a breeze.

Here is little sneak peek what we are going to build, check out the demo for more.

TrainRider using Laravel Cashier & Stripe

 

Source Code Demo

Setup Stripe

If you don’t have an account on stripe please create one, it’s free for testing, it will take one minute, visit https://dashboard.stripe.com/register and sign up.

Create Stripe Plan

Now login into stripe and click Plans under Subscriptions, add some plans, I am adding a Small, Medium and Large plan.

create a stripe plan

Let’s have a look at the plan options:

  • ID: As its sounds, it’s a unique plan identifier, you can create any name like silver, gold, platinum etc.
  • Name: A user-friendly name for the plan, two plan can have the same name, generally, we keep both id and name same.
  • Amount: Amount in selected currency for this plan.
  • Interval: Plan can be billed monthly, yearly weekly etc.
  • Trial period days: If you enter some number here plan will offer a Free trial option for specified days.

Add Stripe Coupons

Now we have plans let’s also add some coupons, click on Coupons to add some coupons.

create coupons in stripe

Let’s have a look at some fields:

  • Percent Off and Amount Off: Enter in only one of this field, two fields allow you to define a discount according to a specific dollar amount or percentage.
  • Duration: This setting allows you to define how the coupon lasts, once applied. The options are once, multi-month, or forever.
  • ID Code: This is the code the user will enter to take advantage of the discount.
  • Max Redemptions: How many time coupon can be redeemed.
  • Redeem By: For short term offer you can set a date here, for example till New Year.

Stripe API Key

We will need API keys to configure stripe in Laravel, go to your account at the top right and click account settings. Switch tab to API keys and copy the test keys.

Stripe API keys

Laravel

Now we have all the things needed for subscription added in Stripe control panel, let’s create our app in Laravel 5.3, I am going to make a TrailRider web app where you can subscribe to any of Small, Medium or Large plan to access keep your train running. After subscribing to a plan you can visit the dashboard and you can see what’s plan you are currently on, invoices, option to cancel your subscription or upgrade/downgrade plans.

We have a lot to cover so let’s get started by creating the app and setting it up with Stripe API keys.

laravel new TrainRider

If you don’t have Laravel installed you can use Composer to create project composer create-project --prefer-dist laravel/laravel TrainRider

Once composer pulls all the dependencies, go to .env file and update the database credentials, add Stripe Keys.

STRIPE_SECRET=sk_test_5zAiwAc3U1GMP4p8a5O9u0b8
STRIPE_KEY=pk_test_frD0Nvi72TXM84hcpFi8RF5d

Now install the laravel cashier, open composer.json and add “laravel/cashier": "~7.0"  in required block, run composer update to pull it. Next, register the Laravel\Cashier\CashierServiceProvider service provider in your config/app.php configuration file.

Cashier Migrations

Let’s add migration so cashier can work its magic, run php artisan make:migration add_cashiers_migrations and add below schema in generated migration file.

Schema::table('users', function ($table) {
    $table->string('stripe_id')->nullable();
    $table->string('card_brand')->nullable();
    $table->string('card_last_four')->nullable();
    $table->timestamp('trial_ends_at')->nullable();
});

Schema::create('subscriptions', function ($table) {
    $table->increments('id');
    $table->integer('user_id');
    $table->string('name');
    $table->string('stripe_id');
    $table->string('stripe_plan');
    $table->integer('quantity');
    $table->timestamp('trial_ends_at')->nullable();
    $table->timestamp('ends_at')->nullable();
    $table->timestamps();
});

Migrate the db by running php artisan migrate command. Now we just need to add Billable Trait on our User model. Open the app/User.php and use Billable Trait.

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Laravel Auth

Laravel comes with authentication out of the box, to scaffold basic login and registration views and routes we need to run php artisan make:auth. Now we can serve app and visit http://localhost:8000/register  and register to see authentication is working. If everything is working now we can get into the fun and main part of this application.

Dashboard

Once user logged in let’s show the subscription status on the dashboard with plans to activate the train, for this lets first get the status in app/Http/Controllers/HomeController.php  index method.

// Get all plans from stripe api
$plans = Plan::getStripePlans();

// Check is subscribed
$is_subscribed = Auth::user()->subscribed('main');

// If subscribed get the subscription
$subscription = Auth::user()->subscription('main');

return view('home', compact('plans', 'is_subscribed', 'subscription'));

I am fetching all the Stripe plans using Stripe’s official php library stripe/stripe-php  which is a dependency of Cashier, so it’s already installed. I have one Plan class which has only one method to fetch the plans from stripe API and cache it for one day since plans are not going to change that frequently.

use Illuminate\Support\Facades\Cache;
use Stripe\Stripe;

class Plan
{
    public static function getStripePlans()
    {
        // Set the API Key
        Stripe::setApiKey(User::getStripeKey());

        try {
            // Fetch all the Plans and cache it
            return Cache::remember('stripe.plans', 60*24, function() {
                return \Stripe\Plan::all()->data;
            });
        } catch ( \Exception $e ) {
            return false;
        }
    }
}

Next, update the resources/views/home.blade.php and add some conditional to show an appropriate message for subscribed & no subscribed user.

<h1>Welcome <span class="text-primary">{{ Auth::user()->name }}</span></h1>

@if( $is_subscribed )

    <img width="180" src="{{ asset('img/train-active.png') }}" alt="Train Active">

    <h1 class="pulse"><span>Choo</span> <span>Choo...</span></h1>

    <h3 class="text-success">
         Who whoo! your train is running  <br>
        <small>
            it has <span class="text-primary">{{ $subscription->stripe_plan }}</span> plan.
        </small>
    </h3>

    @if( $subscription->onGracePeriod() )

        <div class="alert alert-warning">
            <h3 class="modal-title">Subscription expiring at {{ $subscription->ends_at->toFormattedDateString() }}</h3>
        </div>

        <form method="post" action="{{ route('subscriptionResume') }}">
            {{ csrf_field() }}
            <button type="submit" class="btn btn-success">Resume Subscription</button>
        </form>
        <br>

    @else
        <a href="{{ route('confirmCancellation') }}" class="btn btn-danger">Cancel Subscription</a>
    @endif

@else

    <img width="180" src="{{ asset('img/train.png') }}" alt="Train Disabled">
    <h3 class="text-danger">Your train is out of fuel. <br>
        <small>You need to a coal delivery subscription to keep your train running!</small>
    </h3>

@endif

Below this greetings screen, I am listing all the plans fetched from Stripe with a button to subscribe.

@foreach($plans as $plan)
    <div class="col-sm-4">
        <div class="panel {{ ( $is_subscribed && $subscription->stripe_plan ==  $plan->id ) ? 'panel-success' :  'panel-primary' }}">
            <div class="panel-heading text-uppercase">{{ $plan->id }}</div>
            <div class="panel-body text-center">
                <h3 class="modal-title">
                    {{ $plan->name }}
                </h3>
                <img class="img-responsive" src="{{ asset('img/coal.png') }}" alt="{{ $plan->name }} Coal">

                <p>{{ $plan->currency }} {{ $plan->amount / 100 }} / {{ $plan->interval }}</p>
            </div>
            <div class="panel-footer">
                @if( $is_subscribed &&  ( $subscription->stripe_plan ==  $plan->id ) )
                    <a href="#" class="btn btn-default btn-block">
                        Current Plan
                    </a>
                @else
                    <a href="{{ route('plan', $plan->id) }}" class="btn btn-success btn-block">
                        Subscribe
                    </a>
                @endif
            </div>
        </div>
    </div>
@endforeach

Subscribe to a Plan

Subscribing to a plan is very easy with the help of stripe.js we will be posting our credit card info to the stripe, then stripe will send us back a stripeToken which will be required by our laravel backend to charge the card.

Stripe needs cards number, exp_month, exp_year & cvc fields in order to return a token against which we can charge the card.

When the user clicks on any plans subscribe button, we will show plan detail and credit card form to process the subscription request. let’s create our PlanController and add below code in it.

public function show($id)
    {
        // get the plan by id from cache
        $plan = $this->getPlanByIdOrFail($id);

        return view('plan', compact('plan'));
    }

Now we need plan view plan.blade.php, create the file and add below code.

<form action="{{ route('subscribe') }}" method="POST" id="payment-form">
    {{ csrf_field() }}

    <h3 class="text-center">
        <span class="payment-errors label label-danger"></span>
    </h3>

    <div class="row">
        <div class='form-row'>
            <div class='col-xs-12 form-group card required'>
                <label class='control-label'>Card Number</label>
                <input autocomplete='off' value="4242 4242 4242 4242" class='form-control card-number' data-stripe="number" size='20' type='text' required>
            </div>
        </div>
        <div class='form-row'>
            <div class='col-xs-4 form-group cvc required'>
                <label class='control-label'>CVC</label>
                <input autocomplete='off' class='form-control card-cvc' placeholder='ex. 311' data-stripe="cvc" size='4' type='text' required>
            </div>
            <div class='col-xs-4 form-group expiration required'>
                <label class='control-label'>Expiration Month</label>
                <input class='form-control card-expiry-month' placeholder='MM' value="{{ date('d') }}" data-stripe="exp_month" size='2' type='text' required>
            </div>
            <div class='col-xs-4 form-group expiration required'>
                <label class='control-label'> Year</label>
                <input class='form-control card-expiry-year' placeholder='YY' data-stripe="exp_year" size='2'  value="{{ date( 'y', strtotime('+ 4 year')) }}" type='text' required>
            </div>
        </div>

        <div class="form-row">
            <div class="col-md-4">
                <div class='form-group cvc required'>
                    <label class='control-label'>Coupon Code</label>
                    <input autocomplete='off' class='form-control' placeholder='Coupon code' name="coupon" type='text'>
                </div>
            </div>
        </div>
    </div>
    <input type="hidden" name="plan" value="{{ $plan['id'] }}">
    <input type="submit" class="submit btn btn-success btn-lg btn-block" value="Make $ {{ $plan['amount'] / 100 }} Payment">
</form>

It’s credit card form with hidden input for chosen plan by the user. If you noticed there are no name attribute on input associated with the card, instead data-stripe is used so that when we submit this form to our app, no card data will touch our server, that’s what make it secure. Form processing is done using jQuery.

<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script type="text/javascript" src="https://js.stripe.com/v2/"></script>
<script type="text/javascript">

    Stripe.setPublishableKey("{{ config('services.stripe.key') }}");

    $(function() {
        var $form = $('#payment-form');
        $form.submit(function(event) {
            // Disable the submit button to prevent repeated clicks:
            $form.find('.submit').prop('disabled', true);

            // Request a token from Stripe:
            Stripe.card.createToken($form, stripeResponseHandler);

            // Prevent the form from being submitted:
            return false;
        });
    });

    function stripeResponseHandler(status, response) {
        // Grab the form:
        var $form = $('#payment-form');

        if (response.error) { // Problem!

            // Show the errors on the form:
            $form.find('.payment-errors').text(response.error.message);
            $form.find('.submit').prop('disabled', false); // Re-enable submission

        } else { // Token was created!

            // Get the token ID:
            var token = response.id;

            // Insert the token ID into the form so it gets submitted to the server:
            $form.append($('<input type="hidden" name="stripeToken">').val(token));

            // Submit the form:
            $form.get(0).submit();
        }
    };
</script>

Now when you submit the form stripe.js will make a request to API and get us a token back, which we send to our application to charge the card and start a subscription for currently logged user.

Stripe.js should be loaded directly from https://js.stripe.com/v2/.

Let’s add our subscription handling code in our PlanController, thanks to Cashier its pretty easy to do, we have already used Billable Trait on the User model, which gives us many methods to deal with subscription handling.

public function subscribe(Request $request)
{
    // Validate request
     $this->validate( $request, [ 'stripeToken' => 'required', 'plan' => 'required'] );

    // User chosen plan
    $pickedPlan = $request->get('plan');

    // Current logged in user
    $me = Auth::user();

    try {
        // check already subscribed and if already subscribed with picked plan
        if( $me->subscribed('main') && ! $me->subscribedToPlan($pickedPlan, 'main') ) {

            // swap if different plan attempt
            $me->subscription('main')->swap($pickedPlan);

        } else {
            // Its new subscription

            // if user has a coupon, create new subscription with coupon applied
            if( $coupon = $request->get('coupon') ) {

                $me->newSubscription( 'main', $pickedPlan)
                    ->withCoupon($coupon)
                    ->create($request->get('stripeToken'), [
                        'email' => $me->email
                    ]);

            } else {

                // Create subscription
                $me->newSubscription( 'main', $pickedPlan)->create($request->get('stripeToken'), [
                    'email' => $me->email,
                    'description' => $me->name
                ]);
            }

        }
    } catch (\Exception $e) {
        // Catch any error from Stripe API request and show
        return redirect()->back()->withErrors(['status' => $e->getMessage()]);
    }

    return redirect()->route('home')->with('status', 'You are now subscribed to ' . $pickedPlan . ' plan.');
}

This subscribe method does two things, if the user already has a subscription it allows to change it, and it can also subscribe new with Coupon or without a coupon.

Always wrap your stripe api related code in try {} catch {} blocks, its very possible something can go wrong in connection to Stripe api, you can always show error message to user.

I have called newSubscription method one currently logged in user to start a subscription with the plan selected by user. I have also passed email to Stripe since Cashier will create a Stripe customer with it and associate the subscription. You can pass additional data also.

Once user Subscribed we will redirect him/her to the dashboard with the train running.

Apply Coupon

We have already covered this in above code snippet but here’s how it works, we just need to chain the withCoupon() before calling create() to newSubscription method while subscribing the user.

$me->newSubscription('main', 'small')
     ->withCoupon('RIDE20')
     ->create($creditCardToken);

Canceling a Subscription

Cancellation is very simple, you just get the user and call cancel() on it $me->subscription('main')->cancel(); Cashier will set the user as canceled but user can still access if some time is left as grace period. But user will be not charged on next billing period.

Resume a Subscription

User can always resume subscription while he is on grace period. you just need to call you guessed it $me->subscription('main')->resume();.

One off Charge

If some reason you want to make a “one off” charge on the customer for the given amount you can do it by using cashiers $me->charge($amount)  method.

Refund

Cashier also provides a way to issue a refund  by using cashiers $me->refund($charge_id)  method, this method takes the second argument with $options array, if you want a partial refund you need to pass amount in options array to refund, by default complete refund will be granted.

Stripe Invoices

Now we just need to list invoices for user. Create a InvoiceController and add below code.

public function index()
{
   try {
       $invoices = Auth::user()->invoicesIncludingPending();
   } catch ( \Exception $e ) {
       session()->flash('status', $e->getMessage());
   }

   return view('invoice', compact('invoices'));
}

View is simple table with invoice list.

@foreach ($invoices as $invoice)
    <tr>
       <td>{{ $invoice->date()->toFormattedDateString() }}</td>
       <td>{{ $invoice->total() }}</td>
       <td><a href="{{ route('downloadInvoice', $invoice->id) }}">Download</a></td>
    </tr>
@endforeach

Handling Stripe Webhooks

Stripe notify your application of a variety of events via webhooks, to handle Stripe webhooks, define a route that points to Cashier’s webhook controller. This controller will handle all incoming webhook requests and dispatch them to the proper controller method.

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

Now configure the webhook URL in your Stripe control panel under your account settings.

stripe webhook url

Now all the events will be sent to you using a post call from stripe, but wait we have cross-site request forgery ( CSRF ) protection on, we need to allow stripe/* to do that open the app/Http/Middleware/VerifyCsrfToken.php and add this end-point in except array.

protected $except = [
     'stripe/*',
];

This will not run CSRF middleware on stripe webhook calls, Cashier automatically handles subscription cancellation on failed charges events, but you can handle other events also by extending Webhook controller, methods should be prefixed with handle and the “camel case” name of the Stripe webhook you wish to handle. For example, if you wish to handle the invoice.payment_succeeded webhook, you should add a handleInvoicePaymentSucceeded method to the controller:

use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhookController extends CashierController
{
    /**
     * Handle a Stripe webhook.
     *
     * @param  array  $payload
     * @return Response
     */
    public function handleInvoicePaymentSucceeded($payload)
    {
        // Handle The Event
    }
}

Source Code Demo

For brevity I haven’t shared all of the code in the article, you can always check our source code on GitHub repo. I hope you enjoyed this post and learned something. Please check out the demo. I will be happy to hear from you in comments.

17 Responses to “Subscription with coupon using Laravel Cashier & Stripe”

  1. Sarvesh Acharya

    How can we send invoice of the recent subscription?

    $me->newSubscription( ‘main’, $pickedPlan)->create($request->get(‘stripeToken’), [
    ’email’ => $me->email,
    ‘description’ => $me->name
    ]);

    • Hi Sarvesh, You can use webhook event to listen for invoice.payment_succeeded event and send email with a link to download invoice. When you subscribe a user to a plan, the Stripe API calls Creates an invoice, Attempts to pay the invoice & Closes the invoice actions.

      You can send user an email by implementing this in WebhookController


      public function handleInvoicePaymentSucceeded($payload)
      {
      // Get the user and invoice from $payload
      $user = $this->getUserByStripeId($payload['data']['object']['customer']);
      $invoiceId = $payload['data']['object']['id'];

      if ($user) {
      //Mail::to($user)->send(new InvoiceCreated($user, $invoiceId));
      }
      }

      I didnt test it but it should get you going.

  2. Olu Dice

    Please I would love to use this with Laravel 5.4 and Angular 1 project that I’ve been working on. Its not making sense on how to do the front end part with angular. I already have the login and register part of my project working with Jwt-Auth. I need to know how to hook it up with angular using ng-model. Please your help would be really appreciated. Thanks in advance

  3. the home page display :

    No Plan found on Stripe Account!
    It could be Network error or you don’t have plans defined in Stripe Panel.

    what is solution?

    • Saqueib

      You need to add Stripe key and secret in config/services.php


      'stripe' => [
      'model' => AppUser::class,
      'key' => env('STRIPE_KEY'),
      'secret' => env('STRIPE_SECRET'),
      ],

      After that whatever plan you define on stripe it will be shown on plans page automatically, as you can see we have cached plans with `stripe.plans` key, you should flush cache using php artisan cache:forget stripe.plans if nothing shows up.

      hope this helps

  4. Stanley Bonhomme

    Great and informative tutorial however I have followed your instructions exactly and the plans for whatever reason do not show up in the dashboard. I have even tried to clone the repo to test if its not an issue with my code and even when you run the version from the github repo the plans still don’t show up. Can anyone help?

  5. AMAZING tutorial – you are the ONLY one on the entire Internet who took the time to help explain the updated and most recent Laravel/Cashier model. Thank you a million times!

    I wonder — is there a way to limit the plans we grab? I have extra plans that aren’t associated with this app…thank you again.