title: 100% discount on a one off charge with Stripe [Laravel]
body: <p>In my Laravel application I have a page where users must pay £150 for a membership fee. To process this payment I chose Stripe.</p>
  
  <p>I store all of the charges in a payments table, along with a user's ID.</p>
  
  <p><strong>Payments table</strong></p>
  
  <pre><code>Schema::create('payments', function (Blueprint $table) {
      $table-&gt;increments('id');
      $table-&gt;uuid('user_id');
      $table-&gt;string('transaction_id');
      $table-&gt;string('description');
      $table-&gt;string('amount');
      $table-&gt;string('currency');
      $table-&gt;datetime('date_recorded');
      $table-&gt;string('card_brand');
      $table-&gt;string('card_last_4', 4);
      $table-&gt;string('status');
      $table-&gt;timestamps();
  });
  </code></pre>
  
  <p>I also implemented a voucher system of my own as I am not using subscriptions.</p>
  
  <p><strong>Voucher table</strong></p>
  
  <pre><code>Schema::create('vouchers', function (Blueprint $table) {
      $table-&gt;increments('id');
      $table-&gt;string('code');
      $table-&gt;integer('discount_percent');
      $table-&gt;dateTime('expires_on');
      $table-&gt;timestamps();
  });
  </code></pre>
  
  <p><strong>Payment Controller</strong></p>
  
  <pre><code>&lt;?php
  
  namespace App\Http\Controllers;
  
  use Illuminate\Http\Request;
  use Carbon\Carbon;
  use App\User;
  use App\Payment;
  use App\Voucher;
  use App\Mail\User\PaymentReceipt;
  use App\Mail\Admin\UserMembershipPaid;
  use Log;
  use Mail;
  use Validator;
  use Stripe;
  use Stripe\Error\Card;
  
  class PaymentController extends Controller
  {
      /**
       * Set an initial amount to be used by the controller
       *
       * @var float
       */
      private $amount = 150.00;
  
      /**
       * Create a new controller instance.
       *
       * @return void
       */
      public function __construct()
      {
          $this-&gt;middleware('auth');
          $this-&gt;middleware('verified');
          $this-&gt;middleware('is_investor');
          $this-&gt;middleware('is_passive_member');
      }
  
      /**
       * Display a form allowing a user to make a payment
       *
       * @return void
       */
      public function showPaymentForm()
      {
          return view('user.payment');
      }
  
      /**
       * Handle an entered voucher code by the user
       * Either calculate a discount or skip the payment form 
       *
       * @param [type] $request
       * @return void
       */
      public function processVoucher(Request $request)
      {
          $rules = [
              'code' =&gt; 'required|exists:vouchers',
          ];
  
          $messages = [
              'code.required' =&gt; 'You submitted a blank field',
              'code.exists' =&gt; 'This voucher code is not valid'
          ];
  
          Validator::make($request-&gt;all(), $rules, $messages)-&gt;validate();
  
          $entered_voucher_code = $request-&gt;get('code');
  
          $voucher = Voucher::where('code', $entered_voucher_code)-&gt;where('expires_on', '&gt;', Carbon::now())-&gt;first();
  
          // If the voucher exists
          if ($voucher) {
              $discount_percent = $voucher-&gt;discount_percent;
              $new_amount = $this-&gt;amount - ($discount_percent / 100 * $this-&gt;amount);
  
              // As Stripe won't handle charges of 0, we need some extra logic
              if ($new_amount &lt;= 0.05) {
                  $this-&gt;upgradeAccount(auth()-&gt;user());
  
                  Log::info(auth()-&gt;user()-&gt;log_reference . " used voucher code {$voucher-&gt;code} to get a 100% discount on their Active membership");
  
                  return redirect()-&gt;route('user.dashboard')-&gt;withSuccess("Your membership has been upgraded free of charge.");
              }
              // Apply the discount to this session 
              else {
                  Log::info(auth()-&gt;user()-&gt;log_reference . " used voucher code {$voucher-&gt;code} to get a {$voucher-&gt;discount_percent}% discount on their Active membership");
  
                  // Store some data in the session and redirect
                  session(['voucher_discount' =&gt; $voucher-&gt;discount_percent]);
                  session(['new_price' =&gt; $this-&gt;amount - ($voucher-&gt;discount_percent / 100) * $this-&gt;amount]);
  
                  return redirect()-&gt;back()-&gt;withSuccess([
                      'voucher' =&gt; [
                          'message' =&gt; 'Voucher code ' . $voucher-&gt;code . ' has been applied. Please fill in the payment form',
                          'new_price' =&gt; $new_amount
                      ]
                  ]);
              }
          }
          // Voucher has expired
          else {
              return redirect()-&gt;back()-&gt;withError('This voucher code has expired.');
          }
      }
  
      /**
       * Handle a Stripe payment attempt from the Stripe Elements form
       * Takes into account voucher codes if they are less than 100%
       *
       * @param Request $request
       * @return void
       */
      public function handleStripePayment(Request $request)
      {
          // Retreive the currently authenticated user
          $user = auth()-&gt;user();
  
          // Get the Stripe token from the request
          $token = $request-&gt;get('stripeToken');
  
          // Set the currency for your country
          $currency = 'GBP';
  
          // Set an initial amount for Stripe to use with the charge
          $amount = $this-&gt;amount;
  
          // A description for this payment
          $description = "Newable Private Investing Portal - Active Membership fee";
  
          // Initialize Stripe with given public key
          $stripe = Stripe::make(config('services.stripe.secret'));
  
          // Attempt a charge via Stripe
          try {
              Log::info("{$user-&gt;log_reference} attempted to upgrade their membership to Active");
  
              // Check that token was sent across, if it wasn't, stop
              if (empty($token)) {
                  return redirect()-&gt;back()-&gt;withErrors([
                      'error' =&gt; "Token error, do you have JavaScript disabled?"
                  ]);
              }
  
              // Check whether a discount should be applied to this charge
              if (session()-&gt;has('voucher_discount')) {
                  $discount_percentage = session()-&gt;pull('voucher_discount');
  
                  $discount = ($discount_percentage / 100) * $amount;
  
                  $amount = $amount - $discount;
  
                  session()-&gt;forget('new_price');
              }
  
              // Create a charge with an idempotent id to prevent duplicate charges
              $charge = $stripe-&gt;idempotent(session()-&gt;getId())-&gt;charges()-&gt;create([
                  'amount' =&gt; $amount,
                  'currency' =&gt; $currency,
                  'card' =&gt; $token,
                  'description' =&gt; $description,
                  'statement_descriptor' =&gt; 'Newable Ventures',
                  'receipt_email' =&gt; $user-&gt;email
              ]);
  
              //If the payment is successful, store the payment, send some emails and upgrade this user
              if ($charge['status'] == 'succeeded') {
                  $this-&gt;storePayment($charge);
  
                  Mail::send(new PaymentReceipt($user));
                  Mail::send(new UserMembershipPaid($user));
  
                  $this-&gt;upgradeAccount($user);
  
                  return redirect()-&gt;route('user.dashboard')-&gt;withSuccess("Your payment was successful, you will soon recieve an email receipt.");
              // If the payment was unsuccessful
              } else {
                  $this-&gt;storePayment($charge);
  
                  Log::error("Stripe charge failed for {$user-&gt;log_reference}");
  
                  return redirect()-&gt;back()-&gt;withErrors([
                      'error' =&gt; "Unfortunately, your payment was unsuccessful."
                  ]);
              }
          } catch (Exception $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - Exception - error details {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\MissingParameterException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - MissingParameterException - error details {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\CardErrorException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - CardErrorException - error details {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\ApiLimitExceededException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - ApiLimitExceededException - error details {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\BadRequestException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - BadRequestException -  error details {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\ServerErrorException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - ServerErrorException - error details: {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          } catch (\Cartalyst\Stripe\Exception\UnauthorizedException $e) {
              Log::error("Error attempting Stripe Charge for {$user-&gt;log_reference} - UnauthorizedException - error details: {$e-&gt;getMessage()}");
  
              return redirect()-&gt;back()-&gt;withErrors([
                  'error' =&gt; $e-&gt;getMessage()
              ]);
          }
      }
  
      /**
       * Store a Stripe chargee in our database so we can reference it later if necessary
       * Charges stored against users for cross referencing and easy refunds
       *
       * @return void
       */
      private function storePayment(array $charge)
      {
          $payment = new Payment();
  
          $payment-&gt;transaction_id = $charge['id'];
          $payment-&gt;description = $charge['description'];
          $payment-&gt;amount = $charge['amount'];
          $payment-&gt;currency = $charge['currency'];
          $payment-&gt;date_recorded = Carbon::createFromTimestamp($charge['created']);
          $payment-&gt;card_brand = $charge['source']['brand'];
          $payment-&gt;card_last_4 = $charge['source']['last4'];
          $payment-&gt;status = $charge['status'];
  
          auth()-&gt;user()-&gt;payments()-&gt;save($payment);
  
          if ($payment-&gt;status === "succeeded") {
              Log::info("Successful Stripe Charge recorded for {$user-&gt;log_reference} with Stripe reference {$payment-&gt;transaction_id} using card ending {$payment-&gt;card_last_4}");
          } else {
              Log::info("Failed Stripe Charge recorded for {$user-&gt;log_reference} with Stripe reference {$payment-&gt;transaction_id} using card ending {$payment-&gt;card_last_4}");
          }
      }
  
      /**
       * Handle a user account upgrade from whatever to Active
       *
       * @param User $user
       * @return void
       */
      private function upgradeAccount(User $user)
      {
          $current_membership_type = $user-&gt;member_type;
  
          $user-&gt;member_type = "Active";
  
          $user-&gt;save();
  
          Log::info("{$user-&gt;log_reference} has been upgraded from a {$current_membership_type} member to an Active Member.");
      }
  }
  </code></pre>
  
  <p><code>processVoucher()</code> takes a string entered by the user, checks to see if it exists in the <code>vouchers</code> table and then applies the discount percentage to the fee of <code>150.00</code>.</p>
  
  <p>It then adds the new value to the session and I use that in the Stripe Charge.</p>
  
  <p><strong>The issue</strong></p>
  
  <p>The issue is that Stripe's minimum chargable amount is <code>0.05</code>, so to circumvent this issue I've just called a method that upgrades the account.</p>
  
  <p>I should, in theory, store the free upgrades in the <code>charges</code> table but I would end up with multiple null values.</p>
  
  <p>Is this a horrible solution?</p>
  
  <p>In the <code>User</code> model I also have the following methods:</p>
  
  <pre><code>/**
   * Relationship to payments
   */
  public function payments()
  {
      return $this-&gt;hasMany(Payment::class, 'user_id', 'id');
  }
  
  /**
   * Relationship to payments to get most recent payment
   *
   * @return void
   */
  public function latest_payment()
  {
      return $this-&gt;hasOne(Payment::class, 'user_id', 'id')-&gt;latest();
  } 
  </code></pre>
  
  <p>These are used so I can calculate when a user last made a payment, as I needed to bill them annually without using subscriptions as users can also use 100% off vouchers to upgrade.</p>
  
  <p>I made this console command:</p>
  
  <pre><code>&lt;?php
  
  namespace App\Console\Commands;
  
  use Illuminate\Console\Command;
  
  use Carbon\Carbon;
  use App\User;
  use App\Payment;
  use Log;
  
  class ExpireMembership extends Command
  {
      /**
       * The name and signature of the console command.
       *
       * @var string
       */
      protected $signature = 'membership:expire';
  
      /**
       * The console command description.
       *
       * @var string
       */
      protected $description = 'Expire user memberships after 1 year of being Active.';
  
      /**
       * Create a new command instance.
       *
       * @return void
       */
      public function __construct()
      {
          parent::__construct();
      }
  
      /**
       * Execute the console command.
       *
       * @return mixed
       */
      public function handle()
      {
          //Retrieve all users who are an active member with their list of payments
          $activeUsers = User::where('member_type', 'Active')-&gt;get();
  
          //Get current date
          $current_date = Carbon::now();
  
          foreach($activeUsers as $user){
              $this-&gt;info("Checking user {$user-&gt;log_reference}");
  
              // If a user has at least one payment recorded
              if($user-&gt;payments()-&gt;exists()){
                  //Get membership end date (latest payment + 1 year added)
                  $membership_end_date = $user-&gt;payments
                      -&gt;where('description', 'Newable Private Investing Portal - Active Membership fee')
                      -&gt;sortByDesc('created_at')
                      -&gt;first()-&gt;created_at-&gt;addYear();
              }
              // If the user has no payments but is an active member just check if they're older than a year
              else{
                  $membership_end_date = $user-&gt;created_at-&gt;addYear();
              }
              //If the membership has gone over 1 year, expire the membership.
              if ($current_date-&gt;lessThanOrEqualTo($membership_end_date)) {
                  $user-&gt;member_type = "Passive";
                  $user-&gt;save();
  
                  $this-&gt;info($user-&gt;log_reference . "membership has expired and membership status has been set to Passive.");
  
                  Log::info($user-&gt;log_reference . "membership has expired and membership status has been set to Passive.");
  
              }
          }
  
          $this-&gt;info("Finished checking user memberships.");
  
      }
  }
  </code></pre>
  
  <p>Users who use vouchers do not have payments, so figuring out when to bill them automatically is tricky.</p>