Welcome to End Point’s blog

Ongoing observations by End Point people

Spree: Gift Certificates and Coupons

In a recent Spree project, I've been working with Bill to add gift certificate functionality. According to the Spree documentation, gift certificate functionality is trivial to implement using the existing coupon architecture. Here are some of the changes we went through as we tried to use the coupon architecture for gift certificate implementation - we found that it wasn't so simple after all.

Here is a very simplified visualization of the coupon and adjustment data model in Spree. Coupons use polymorphic calculators to compute the applicable discount.

First, Bill and I brainstormed to come up with an initial set of changes required for implementing gift certificates as coupons after we reviewed the data model shown above:

  1. Add logic to create a coupon during checkout finalization, which was done with the following:
  2. # coupon object class method
    def self.generate_coupon_code
      # some method to generate an unused random coupon code beginning in 'giftcert-'
    # inside order model during checkout finalization { |li| li.variant.product.is_gift_cert? }.each do |line_item|
      line_item.quantity.times do
        coupon = Coupon.create(:code => Coupon.generate_coupon_code,
                               :description => "Gift Certificate",
                               :usage_limit => 1,
                               :combine => false,
                               :calculator =>
        coupon.calculator.update_attribute(:preferred_amount, line_item.variant.price)
  3. Add logic to decrease a coupon amount during checkout finalization if used:
    # order model during checkout finalization{ |cc| cc.adjustment_source.code.include?('giftcert-') }.each do |coupon_credit|
      coupon = coupon_credit.adjustment_source
      amount = coupon.calculator.preferred_amount - item_total
      coupon.calculator.update_attribute(:preferred_amount, amount < 0 ? 0 : amount)
  4. Add relationship between line item and coupon because we'd want to have a way to associate coupons with line items. The intention here was to limit a gift certificate line item to a quantity of 1 since the gift certificate line item might include personal information like an email in the future.
    LineItem.class_eval do
      has_one :line_item_coupon
      has_one :coupon, :through => :line_item_coupon
    class LineItemCoupon < ActiveRecord::Base
      belongs_to :line_item
      belongs_to :coupon
      validates_presence_of :line_item_id
      validates_presence_of :coupon_id
  5. Create the sample data for a gift certificate (coupon) - the implementation offers a master variant for a fixed cost of $25.00. In addition to the code below, Bill created sample data to assign a product property is_gift_cert to the product.
    # products.yml
      name:          Gift Certificate
      description:   Gift Certificate
      available_on:  <%= %>
      permalink:     gift-certificate
      count_on_hand: 100000
    # variants.yml
      product:       gift_certificate
      sku:           giftcert
      price:         25.00
      is_master:     true
      count_on_hand: 10000
      cost_price:    25.00
      is_extension:  false
  6. Finally, Bill edited the order mailer view to include gift certificate information

After the above changes were implemented, additional changes were required for our particular Spree application.

  1. Adjust the shipping API so it doesn't include gift certificates in the shipping request, because gift certificates aren't shippable. Below is an excerpt of the XML builder code that generates the XML request made to the shipping API:
    # shipping calculator
    -order.line_items.each do |li| { |li| !li.variant.product.is_gift_cert? }
       x.item {
         x.weight(li.variant.weight != 0.0 ? li.variant.weight : Spree::MyShipping::Config[:default_weight])
         x.length(li.variant.depth ? li.variant.depth : Spree::MyShipping::Config[:default_depth])
         x.width(li.variant.width ? li.variant.width : Spree::MyShipping::Config[:default_width])
         x.height(li.variant.height ? li.variant.height : Spree::MyShipping::Config[:default_height])
  2. Create a new calculator for free shipping applicable to orders with gift certificate line items only, using the is_gift_cert product property:
    # registering the calculator inside Spree site_extension.rb (required for all calculators to be used in Spree)
    ].each{ |c_model|
        c_model.register if c_model.table_exists?
      rescue Exception => e
        $stderr.puts "Error registering calculator #{c_model}"
    # shipping method and calculator creation in sample data
    s = => 16, :name => 'Gift Certificate Shipping')
    c =
    c.calculable = s
    c.type = 'Calculator::GiftCertificateShipping' 
    # calculator for free gift cert shipping
    class Calculator::GiftCertificateShipping < Calculator
      def available?(order)
        order.line_items.inject(0) { |sum, li| sum += li.quantity if !li.variant.product.is_gift_cert?; sum } == 0
      def compute(line_items)

After Bill implemented these changes, I contemplated the following code more:{ |coupon_credit| coupon_credit.adjustment_source.code =~ /^giftcert-/}.each do |coupon_credit|
  coupon = coupon_credit.adjustment_source
  amount = coupon.calculator.preferred_amount - item_total
  coupon.calculator.update_attribute(:preferred_amount, amount < 0 ? 0 : amount)

I wondered why the coupon amount being decremented by the item_total and not the order total. What about shipping and sales tax? I verified by looking at the the Spree Coupon class that a coupon's amount will only take into account the item total and not shipping or tax, which would present a problem since gift certificates traditionally apply to tax and shipping costs.

In the Spree core, coupons are never applied to shipping or tax costs.

I investigated the following change to separate coupon and gift certificate credit calculation:

def site_calculate_coupon_credit
  return 0 if order.line_items.empty?
  amount = adjustment_source.calculator.compute(order.line_items).abs
  order_total = adjustment_source.code.include?('giftcert-') ? order.item_total + : order.item_total
  amount = order_total if amount > order_total
  -1 * amount

After this change, I found that when arriving on the payment page where the gift certificate has covered the entire order including tax and shipping, the payment logic isn't set up handle orders with a total cost of 0. Additional customization on payment implementation, validation and checkout flow would be required to handle orders where gift certificates cover the entire cost. However, rather than implementing these additional customizations, our client was satisfied with the implementation where gift certs don't cover tax and shipping, so I did not pursue this further.

In the future, I'd recommend creating a new model for gift certificate and gift certificate credit management rather than combining the business logic with coupons, because:

  1. The coupon implementation in Spree doesn't have a whole lot to it. It uses several custom Spree calculators, has a backend CRUD interface, and credits are applied to orders. Grabbing the coupon implementation and copying and modifying it for gift certificates shouldn't be daunting.
  2. It will likely be more elegant to separate coupon logic from gift certificate logic. Coupons and gift certificates share a few business rules, but not all. Gift certificates traditionally apply to tax and shipping and multiple gift certificates can be used on one order (but this part can be configurable). Coupons may have more complex logic to apply to items and do not traditionally get applied to tax and shipping (however, in some cases a free shipping coupon may be needed that covers the cost of shipping only). Additionally, a big difference in business logic is that gift certificates should probably be treated as a payment, where checkout accepts gift certificates as a form of payment, and the backend provides reporting on the gift certificate driven payments. Rather than dirtying-up the the coupon logic with checks for gift certificates versus coupon behavior, it'll be more elegant to separate the logic into classes that address the individual business needs.

Besides "hindsight is 20/20", the takeaway for me here is that you have to understand business rules and requirements for coupon and gift certificate implementation in ecommerce, which can get tricky quickly. We were lucky because the client was satisfied with the resulting behavior of using the coupon architecture for gift certificates. Hopefully, the takeaway for someone not familiar with Spree is that gift certificate implementation might require things like functionality for creating gift certificates after checkout completion, decrementing the gift certificate after it's used, backend reporting to show gift certificates purchase and use and coding for the impact of gift certificate purchase on shipping.

Note that all of the changes described here apply to the latest stable version of Spree (0.11.0). After taking a look at the Spree edge code, I'll mention that there is a bit of an overhaul on coupons (to be called promotions). However, it looks many of the customizations described here would be needed for gift certificate implementation as the edge promotions still apply to item totals only and do not include any core modifications in accepting a credit as a payment.

Learn more about End Point's Ecommerce Development or Ruby on Rails Ecommerce Services.


Anonymous said...

Hi Steph,
Good article on gift certificates. I too have struggled with this. It looks straight-forward but soon is not.
Added complications are:
--Some states consider gift-certificates taxable, most do not. The customer redeeming the gc usually pays the tax.
--You know who bought the gc but you don't know who will redeem it.
--You need to cover the event that the gc is greater than the order so the customer redeeming it has a credit that can be applied to another order. So you have to store the balance and associate it with the customer.
--A customer may buy more than one gift certificate in one order.
--As you say, a gc is really a form of payment and breaks default Spree rules.
--Outstanding gift certificates have to show as liabilities on the store's books so they need to be reported as such.
--Old unredeemed gc's need to get removed after some period of time.

All in all they're a pain in the ass but customers love them so I guess we have to deal with it.

I'm interested in hearing more on how you and other developers deal with them.

--Will Emerson

Steph Powell said...

Yes - you brought up some good points. Using the Spree coupon architecture should address the last point, since there is an expiration date on coupons. Also, the code I went through should address a couple of your concerns (how to tie a gift certificate to a user, the ability to purchase multiple gift certificates, adjusting the credit after a gift certificate has been used). Both the point on tax and liabilities are good ones to keep in mind that could get tricky. Thanks for adding!

Rahul said...

Hi Steph,

That's a really nice article on Gift Certificates. As Will pointed out earlier a customer may want to buy more than 1 gift certificate in one order. A credit system using the redeemer's email address or phone number as a UUID may be necessary. Has there been an update on this recently?

PS: I'm new to spree & RoR. Any pointers for getting started on the above.

Thanks & Best Regards,
Rahul Jayaraman