Implementing Your Own Coupon System for Standalone Charges

    Create a coupon system that adjusts amounts sent to Stripe for standalone charges. If you need help after reading this, search our documentation or check out answers to common questions. You can even chat live with other developers in #stripe on freenode.

    Stripe makes it easy to use coupons for subscriptions. Applying coupons to standalone charges is typically a bit more nuanced. For instance, in an online store, coupons might depend on the cart total, might affect just shipping, or might only apply to certain items. As such, a coupon system for standalone charges is best implemented in your app. Fortunately, implementing a basic coupon system is easy.

    The first part of this recipe describes a simple and straightforward approach to implement a basic coupon system by:

    1. Adding a form field for a coupon to your checkout page
    2. Hard-coding coupon codes in your app, and applying any discount associated with these coupons before charges are sent to Stripe
    3. Adding a final order confirmation page so the user can see the discount applied

    The second part of the recipe builds off this basic system for a more nuanced solution. It does so by moving the coupons into the database and adding features like expirations and redemption tracking.

    The implementations in this recipe use Rails and build off of the Rails Checkout tutorial, but the concepts can be easily ported to other languages and frameworks. The code examples use Stripe Checkout for easy and secure card collection, but you could also use Elements and your own form just the same.

    Basic coupon system

    The recipe starts by creating a simple coupon framework, which later can be expanded to something more feature-rich.

    Adding a form field

    To get started, simply add a coupon field to your checkout page. The coupon will be submitted to your server, along with the stripeToken, after the user submitted their card details via Checkout. While you’re at it, ensure you have a field where errors can be displayed:

    <%= form_tag charges_path do %>
      <% if flash[:error].present? %>
        <div id="error_explanation">
          <p><%= flash[:error] %></p>
        </div>
      <% end %>
      <p>
        <label class="amount">
          <span>Amount: $5.00</span>
        </label>
      </p>
      <p>
        <%= label_tag(:couponCode, 'Coupon:') %>
        <%= text_field_tag(:couponCode) %>
      </p>
    
      <script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
              data-key="<%= Rails.configuration.stripe[:publishable_key] %>"
              data-description="Your Purchase"
              data-locale="auto"></script>
    <% end %>

    For simplicity’s sake, data-amount is not being set on Checkout. Because the charge amount may change depending on an entered coupon, it is best to refrain from displaying the amount in the Checkout modal.

    Managing coupons on the server

    On the server, you want to verify the validity and value of the submitted coupon, then update the amount to be charged before sending the charge request to Stripe. A simple approach is to have a hard-coded hash table, COUPONS, that stores all current coupons and their respective discounts. You can then look up the user-submitted couponCode in this hash, and apply any discount accordingly.

    Note that in keeping with the Rails tutorial this example builds off of, a customer is created first and then charged. You can just make a charge directly too.

    Modify the code in the create method of charges_controller:

    def create
      # Amount in cents
      @amount = 500
      @final_amount = @amount
    
      @code = params[:couponCode]
    
      if !@code.blank?
        @discount = get_discount(@code)
    
        if @discount.nil?
          flash[:error] = 'Coupon code is not valid or expired.'
          redirect_to new_charge_path
          return
        else
          @discount_amount = @amount * @discount
          @final_amount = @amount - @discount_amount.to_i
        end
    
        charge_metadata = {
          :coupon_code => @code,
          :coupon_discount => (@discount * 100).to_s + "%"
        }
      end
    
      charge_metadata ||= {}
    
      customer = Stripe::Customer.create(
        :email => params[:stripeEmail],
        :source  => params[:stripeToken]
      )
      Stripe::Charge.create(
        :customer    => customer.id,
        :amount      => @final_amount,
        :description => 'Rails Stripe customer',
        :currency    => 'usd',
        :metadata    => charge_metadata
      )
    rescue Stripe::CardError => e
      flash[:error] = e.message
      redirect_to new_charge_path
    end

    This example also adds the coupon used to the charge’s metadata in the Stripe call. Noting the coupon usage makes it easy to see at any later point that a coupon was used on the charge.

    For the above code to work, you want a hash that stores all of the coupons, and a mechanism to retrieve their respective discounts. Add a COUPONS hash and a get_discount helper method to the bottom of the same controller:

    private
    
    COUPONS = {
      'RAVINGSAVINGS' => 0.10,
      'SUMMERSALE' => 0.05
    }
    
    def get_discount(code)
      # Normalize user input
      code = code.gsub(/\s+/, '')
      code = code.upcase
      COUPONS[code]
    end

    Some normalization of code is performed, like stripping whitespace, before looking it up in the hash. If the user submitted a coupon but it was not found, no coupon is returned, and the calling code creates a Coupon code is not valid or expired. error, preventing the charge from being processed.

    Adding an order confirmation page

    Next, build an order confirmation page that displays the subtotal, the discount applied, the savings, and the total amount charged. You can use the handy Rails helper number_to_currency() to format the figures, converting them from Stripe-compatible cents into human-friendly dollars:

    <p>
      <label class="subtotal">
        <span>Subtotal: <%= number_to_currency(@amount * 0.01) %></span>
      </label>
    </p>
    <% if @discount.present? %>
      <p>
        <label class="coupon">
          <span>Coupon: <%= @code %></span>
        </label>
      </p>
      <p>
        <label class="savings">
          <span>Savings: <%= number_to_currency(@discount_amount * 0.01) %></span>
        </label>
      </p>
      <p>
        <label class="discount">
          <span>Discount: <%= (@discount * 100).to_s + '%' %></span>
        </label>
      </p>
    <% end %>
    <p>
      <label class="total">
        <span>Total: <%= number_to_currency(@final_amount * 0.01) %></span>
      </label>
    </p>

    In this basic implementation and the subsequent more advanced one, the user will not know the final charge amount before they submit their details via Checkout. This is because the discount is applied after the stripeToken is generated but before the charge call is made to Stripe. See the end of this recipe for a possible approach to improving this workflow.

    More advanced coupon system

    The coupon system outlined so far is relatively inflexible. Adding new coupons or removing expired ones requires a code change and a deploy. It would also be valuable to track coupon redemptions. With a bit more effort, you could improve the system by moving the coupons into the database.

    In this example, the Coupon model stores both the code and discount_percent, as well as an optional expires_at timestamp and an optional description for internal use. The example also stores a record of charges in a Charge model. Charge has amount and stripe_id properties, and reflects an associated coupon through coupon_id. This structure will enable you to track coupon redemptions.

    Generate the models and migrations:

    rails g model coupon code:string discount_percent:integer expires_at:timestamp description:string
    rails g model charges amount:integer coupon_id:integer stripe_id:integer

    Then migrate:

    bundle exec rake db:migrate

    Now you can move all coupon-related processing to the Coupon model. It has a get method that normalizes the code and searches for matching, non-expired coupons:

    class Coupon < ActiveRecord::Base
      has_many :charges
      validates_presence_of :code, :discount_percent
      validates_uniqueness_of :code
    
      def self.get(code)
        where(code: normalize_code(code)).
        where('expires_at > ? OR expires_at IS NULL', Time.now).
        take
      end
    
      def apply_discount(amount)
        discount = amount * (self.discount_percent * 0.01)
        (amount - discount.to_i)
      end
    
      def discount_percent_human
        if discount_percent.present?
          discount_percent.to_s + "%"
        end
      end
    
      private
    
      def self.normalize_code(code)
        code.gsub(/\s+/, '').upcase
      end
    end

    Note that has_many :charges enables you to retrieve the redemption count for a coupon by calling coupon.charges.count.

    The Charge model is very simple:

    class Charge < ActiveRecord::Base
      belongs_to :coupon
      validates_presence_of :amount, :stripe_id
    end

    Next, modify the application of the coupon in the controller to use the models:

    @amount = 500
    @final_amount = @amount
    
    code = params[:couponCode]
    
    if !code.blank?
      @coupon = Coupon.get(code)
    
      if @coupon.nil?
        flash[:error] = 'Coupon code is not valid or expired.'
        redirect_to new_charge_path
        return
      else
        @final_amount = @coupon.apply_discount(@amount.to_i)
        @discount_amount = (@amount - @final_amount)
      end
    
      charge_metadata = {
        :coupon_code => @coupon.code,
        :coupon_discount => @coupon.discount_percent_human
      }
    end

    Now, add code that creates a charge in the database after a request to Stripe is completed. If your Charge model grows in complexity, you’ll want to add some error handling around the #create! call, but it’s fine to keep it simple to begin:

      customer = Stripe::Customer.create(
        :email => params[:stripeEmail],
        :source  => params[:stripeToken]
      )
      stripe_charge = Stripe::Charge.create(
        :customer    => customer.id,
        :amount      => @final_amount,
        :description => 'Rails Stripe customer',
        :currency    => 'usd',
        :metadata    => charge_metadata
      )
      @charge = Charge.create!(amount: @final_amount, coupon: @coupon, stripe_id: stripe_charge.id)
    rescue Stripe::CardError => e
      flash[:error] = e.message
      redirect_to new_charge_path
    end

    Finally, modify the order confirmation page to use the new model-driven coupons and charges:

    <p>
      <label class="subtotal">
        <span>Subtotal: <%= number_to_currency(@amount * 0.01) %></span>
      </label>
    </p>
    <% if @coupon.present? %>
      <p>
        <label class="coupon">
          <span>Coupon: <%= @coupon.code %></span>
        </label>
      </p>
      <p>
        <label class="savings">
          <span>Savings: <%= number_to_currency(@discount_amount * 0.01) %></span>
        </label>
      </p>
      <p>
        <label class="discount">
          <span>Discount: <%= @coupon.discount_percent_human %></span>
        </label>
      </p>
    <% end %>
    <p>
      <label class="total">
        <span>Total: <%= number_to_currency(@charge.amount * 0.01) %></span>
      </label>
    </p>

    You can test your new system by simply creating a coupon in the database. Here’s one with an expiry:

    # in Rails console
    Coupon.create(code: 'SUMMERSALE', discount_percent: 5, expires_at: 1.week.from_now)

    Improving the user experience

    This recipe started off implementing a basic coupon system by adding a new field to a checkout page and some logic that modifies the charge amount before being sent to Stripe. It then gave an example of what an order confirmation page would look like. This system was then further improved by moving coupons to the database. Armed with this knowledge, you’re prepared to swiftly implement your own coupon system for your application.

    While these implementations are fully-functional, they do have one obvious opportunity to enhance the user experience. As noted previously, a limitation with these approaches is a user won’t see their order total reflecting an applied coupon until after the order has completed.

    An improvement would be allowing the user to submit a coupon and see the final charge amount before opening the Checkout modal. One method of accomplishing this would be to add a button next to the coupon field that submits an Ajax request to a /coupons endpoint on your server. The response from /coupons would indicate whether or not the coupon is valid and what discount should be applied. JavaScript would then update both the amount displayed on the page as well as the data-amount attribute on the Checkout script element. After the user completes Checkout, your server would then run a similar check as detailed above to double-check the math before making the charge call to Stripe.