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, check out our answers to common questions or 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 Stripe.js 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_human_percentage %></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.