Adding Coupon Codes to a Rails App

微信扫一扫,分享到朋友圈

Adding Coupon Codes to a Rails App

We recently decided to add coupon codes to preventalemon.com. I wanted to share a quick walk through of how I did it, and I’d love to see how others have done it or get your feedback on my approach.

Some quick background. Prevent A Lemon is a used vehicle inspection service. Customers book online, select the time and place, and select a nearby mechanic to perform the inspection. It’s a Rails 5 app, and we use Stripe for payment processing and ActiveAdmin for our administrative backend. Stripe offers their own coupon code functionality but it’s only available for recurring paymentssubscriptions and not one time payments like we needed. There’s also the Coupons gem, but it hasn’t been updated in a few years so I wasn’t super confident with its Rails 5 compatibility or its ongoing maintenance.

Let’s get into it!

I stated by outline the requirements:

  • We must be able to create codes on the fly, for sending automated emails with unique codes.
  • We must have a UI for creating codes, so non-developers on the team can make them.
  • We need to be able to track usage.
  • We need to be able to limit them by number of uses and expiration dates.
  • We need to support both $ and % discounts.

So I wrote the migration and created the table. Here’s what the schema ends up being:

# /db/schema.rb

create_table "coupons", force: :cascade do |t|
    t.float    "amount",     default: 0.0
    t.integer  "limit",      default: 0
    t.date     "expiration"
    t.string   "code"
    t.boolean  "percentage", default: false
    t.datetime "created_at",                 null: false
    t.datetime "updated_at",                 null: false
    t.integer  "used",       default: 0,     null: false
end

Next, I created the model with some basic validations and associations.

# /models/coupon.rb

class Coupon < ApplicationRecord
    validates_numericality_of :amount, on: :create, message: "is not a number"
    validates_uniqueness_of :code, on: :create, message: "must be unique", case_sensitive: false

    # :inspection_infos is our "order" model
    has_many :inspection_infos
end

Now, I needed two main functions: one to check if a code was valid, and another to calculate the discounted price. PAL’s service is always the same base price, so that makes thing simple.

# /models/coupon.rb

    # returns true if the code is valid
    # false otherwise
    def is_valid?
        # Expiration date is nil, or equal to or greater than today?
        (self.expiration.nil? || self.expiration >= Date.current) &&
        # Limit is set to 0 (for unlimited) or limit is greater than the current used count.
        (self.limit == 0 || self.limit > self.used)
    end

    # Calculates the discounted price
    # Returns full price if the code is not valid
    # PRICE is set in an initializer, 
    # based on a environment variable.
    def discounted_price
        price = if is_valid?
                  if percentage
                    PRICE - (PRICE * (amount/100))
                  else
                    (PRICE - amount)
                  end
                else
                  PRICE
                end
        return price.floor
    end

That’s the core model functionality that’s needed to make our simple coupon system work. Now we need to be able to apply it to orders. This will happen in two stages: A single CouponsController method so we can check for valid codes and provide visual confirmation for the customer, and then some changes to our order’s controller.

Let’s start with the CouponsController, and the AJAX call that calls it.

class CouponsController < ApplicationController

  def validate
    coupon = Coupon.find_code(params[:code])
    if coupon.present?
      response = { valid: coupon.is_valid?, 
                   discounted_price: coupon.discounted_price }
    else
      response = { valid: false, discounted_price: PRICE }
    end
    respond_to do |format|
      format.json { render json: response }
    end
  end

end

Now on the checkout page I’ve added a Coupon text field, with an Apply button. When that apply button is clicked, it makes this AJAX call. This sends the code to the controller method, which returns the discounted price if it’s valid. The invalidCode() function mentioned here just applies a red border to the input if the code is not valid.

$('#coupon-apply').click(function (e) {
    e.preventDefault();
    e.stopPropagation();

    let code = $('#coupon_code').val();
    if (code) {
      $.get("/coupons/validate/"+code, function(data){
        if (data["valid"]) {
          $('#coupon_code').css("border-color", "green");
          $('#total').text(data["discounted_price"]);
          $('#coupon-msg').text("Coupon applied").css("color", "green");
        } else {
          invalidCode();
        }
      })
    } else {
      invalidCode();
    }
  });

This provides the customer with visual feedback, so they know whether the code was applied or not:

A valid code!

A invalid code 🙁

But it doesn’t actually change the price they’re being billed, because that all happens in the orders controller. In the update method where the Stripe charge is created, I added this:

# get the coupon code from the request parameters.
code = params["coupon_code"]

# if there is a code, find the Coupon record.
coupon = code ? Coupon.find_code(code) : nil

# if there's a Coupon, get the discounted price, or else get the full price.
price = coupon ? coupon.discounted_price : PRICE

# convert price to cents, as per Stripe's requirements
price = price*100

And this price variable is used to create the Stripe charge.

That’s all for the functionality of the codes. But we still needed a way to manage them. We’re already using ActiveAdmin , so it’s the natural choice. I created an Coupon ActiveAdmin file, setup the index, show page, and form for creatingediting coupons.

And finally, we needed to be able to easily track some basic metrics on coupon usage. We use the Counter Culture gem to count some other associations, so it was an easy choice. To achieve this I made Coupon a has_many :inspection_infos association, and InspectionInfo a belongs_to :coupon, optional: true association. Now by adding counter_culture :coupon, column_name: 'used' to the InspectionInfo model, it automatically increments the used count on the coupon each time it’s used.

That’s my MPV for coupon codes on Prevent A Lemon! It gives us all the functionality we need to get started testing coupons, and it only took an afternoon to build. I always build the simplest shippable product first, and then we can iterate on it as we learn how its used.

I’d like to see how others have done this, or if you have any feedback on my approach. Thanks for reading!

微信扫一扫,分享到朋友圈

Adding Coupon Codes to a Rails App

How to develop Android UI Component for React Native

上一篇

微软Surface Pro 6传言汇总

下一篇

你也可能喜欢

Adding Coupon Codes to a Rails App

长按储存图像,分享给朋友