How to create custom promotions and coupons in Spree 2

Reading Time: 5 minutes

Promotions and Discounts are an effective way for you to market your products and services, and eCommerce systemS don't have to limit the types of discounts the sellers can offer. With that in mind, in this tutorial I’m going to show you how to create a custom Spree promotional coupon. Ready?

Let's say I want to create a coupon that applies for all the products that have a specific option value (Color, size, material), and also be able to chose a new price for those promotional products; in other words:

I wanted to offer the following promotional coupon:

 All the size S T-shirts at $10.00 USD.
AND
All  Green sunglasses at $5.00 USD

Based on the Spree documentation we can use the following resources:

  1. Spree::PromotionRule (Indicates the condition/conditions required to apply the discount)
  2. Spree::PromotionAction (Sets the adjustment or discount to apply and also applies some validation for the discounts)
  3. Spree::Calculator ( if you need a specific calculation that doesn't have Spree by default, you can create your own calculator)

So let's get started with the creation of our new promotional coupon code that applies for products with a specific option value.

Create the promotion Action

app/models/spree/promotion/actions/product_with_option_value_action.rb

module Spree
  class Promotion
    module Actions
      class ProductWithOptionValueAction < Spree::PromotionAction

        delegate :eligible?, to: :promotion

        include Spree::Core::CalculatedAdjustments

        has_many :adjustments, as: :source

        before_validation :ensure_action_has_calculator

        def perform(payload = {})
          order = payload[:order]
          result = false
          items = get_line_items_with_discount(order)

          items.find_each do |line_item|
            current_result = create_adjustment(line_item, order)
            result ||= current_result
          end
          return result
        end

        def compute_amount(calculable)
          amount = self.calculator.compute(calculable).to_f.abs
          amount * -1
        end

        private

          def create_adjustment(adjustable, order)
            amount = self.compute_amount(adjustable)
            return if amount == 0
            return if promotion.product_ids.present? and 
            !promotion.product_ids.include?(adjustable.product.id)
           self.adjustments.create!(
             amount: amount,
             adjustable: adjustable,
             order: order,
             label: promotion_name,
           )
           true
         end

         def promotion_name
           self.promotion.name
         end

         def get_line_items_with_discount(order)
           already_adjusted_line_items = [0] + self.adjustments.pluck(:adjustable_id)
           Spree::LineItem.joins(:option_values)
                          .where('spree_option_values.id in (?)
                          AND
                          order_id = ?
                          AND
                          spree_line_items.id NOT IN (?)',
                          get_option_values_with_discount,
                          order.id,
                          already_adjusted_line_items)

         end

        def get_option_values_with_discount
          rule_name = 'Spree::Promotion::Rules::ProductWithOptionValueRule'
          promotion_rule = self.promotion.rules.find_by(type: rule_name)
          promotion_rule.option_values.map(&:id).uniq
        end

        def promotion_credit_exists?(adjustable)
          self.adjustments.where(order_id: adjustable.id).exists?
        end

        def ensure_action_has_calculator
          return if self.calculator
          self.calculator = Calculator::ProductWithOptionValueCalculator.new
        end
      end
     end
   end
 end

Now it is time to create our Promotion Rule:
app/models/spree/promotion/rules/product_with_option_value_rule.rb

module Spree
  class Promotion
    module Rules
      class ProductWithOptionValueRule < PromotionRule

        has_and_belongs_to_many :option_values, 
            class_name: '::Spree::OptionValue', 
            join_table: 'spree_option_values_promotion_rules', 
            foreign_key: 'promotion_rule_id'

        def applicable?(promotable)
          promotable.is_a?(Spree::Order)
        end

        def eligible?(order, options = {})
          option_values_ids = option_values.map(&:id).uniq
          items = Spree::LineItem.joins(:option_values)
                                  .where('spree_option_values.id in (?) AND order_id = ?',
                                   option_values_ids, order.id)
          items.any?
        end

        def option_value_ids_string
          option_values.pluck(:id).join(',')
        end

        def option_value_ids_string=(s)
          ids = s.to_s.split(',').map(&:strip)
          self.option_values = Spree::OptionValue.find(ids)
        end
      end
    end
  end
end
If you noticed the following line:
 has_and_belongs_to_many :option_values, 
    class_name: '::Spree::OptionValue', 
    join_table: 'spree_option_values_promotion_rules', 
    foreign_key: 'promotion_rule_id'
It means that we are going to use an extra table. This is the migration you have to add and run in order to create that table:

You have to create an empty migration with the following command:

rails generate migration CreateSpreePromotionOptionRule

And override the content of the generated file with the below content:

class CreateSpreeOptionValuesPromotionRules < ActiveRecord::Migration
  def change
    create_table :spree_option_values_promotion_rules do |t|
      t.references :option_value
      t.references :promotion_rule
    end

    add_index :spree_option_values_promotion_rules, [:option_value_id], 
        :name => 'index_option_values_promotion_rules_on_option_value_id'
    add_index :spree_option_values_promotion_rules, [:promotion_rule_id], 
        :name => 'index_option_values_promotion_rules_on_promotion_rule_id'
  end
end
So, what's the next step?

Now we have to create the Spree:Calculator

app/models/spree/calculator/product_with_option_value_calculator.rb

module Spree
  class Calculator::ProductWithOptionValueCalculator < Calculator
    preference :product_price, :decimal, default: 0

    def self.description
      'Change Product price'
    end

    def compute(line_item = nil)
      discount = line_item.quantity * preferred_product_price
      (line_item.price * line_item.quantity) - discount
    end
  end
end

Following this, we need a relation between option values and line items. For that reason we have to create the following decorator:

app/decorators/models/spree/line_item_decorator.rb

Spree::LineItem.class_eval do
  has_many :option_values, through: :variant
end

As I mention earlier, we are going to be able to select from the Admin Section the option value or the option values that will have this promotional discount.

Now it's time to modify the default Option Values Controller in order to create an Ajax request for option values. So, we have to override the index method in the mentioned controller:

app/decorators/controllers/spree/api/option_values_controller_decorator.rb

Spree::Api::OptionValuesController.class_eval do
   def index
     if params[:ids]
       @option_values = scope.where(:id => params[:ids].split(','))
     else
       @option_values = scope.ransack(params[:q]).result
     end
     respond_with(@option_values)
   end
end

And our new route:

config/routes.rb

Spree::Core::Engine.add_routes do
  namespace :api, :defaults => { :format => 'json' } do
    resources :option_values
  end
end

Important!: don't forget that we have to tell Spree to load our new action, rule and the calculator, so let's ask Spree to load them:

config/initializers/spree.rb

Rails.application.config.spree.promotions.actions <<
    Spree::Promotion::Actions::ProductWithOptionValueAction
Rails.application.config.spree.promotions.rules << 
    Spree::Promotion::Rules::ProductWithOptionValueRule
Rails.application.config.spree.calculators.promotion_actions_create_adjustments << 
    Spree::Calculator::ProductWithOptionValueCalculator

And the last part is all about Views and Javascript code

If you didn’t already know it, the Select2 plugin is used in the Spree Admin Section. We're going to use the autocomplete functionality and select the multiple options from this plugin.

The following Javascript code adds the functionality to use the select2 and creates the ajax request for the option values:

app/assets/javascripts/spree/backend/option_value_picker.js.coffee

$.fn.optionValueAutocomplete = ->
  'use strict'
  @select2
    minimumInputLength: 1
    multiple: true
    initSelection: (element, callback) ->
      $.get Spree.routes.option_values_search,
        ids: element.val().split(',')
      , (option_values) ->
        callback option_values
        return

       return

    ajax:
      url: Spree.routes.option_values_search
      datatype: 'json'
      data: (term, page) ->
        q:
          name_cont: term
          sku_cont: term

        m: 'OR'
        token: Spree.api_key

      results: (data, page) ->
        option_values = (if data then data else [])
        results: option_values

    formatResult: (option_value) ->
      option_value.name

    formatSelection: (option_value) ->
      option_value.name

  return

After creating the JavaScript code, we have to require the file in order to use it, as we are using Spree 2-2-stable, we have to add the following code in the file:


      //= require spree/backend/option_value_picker

Finally let's add the Admin Views:

```  app/views/spree/admin/promotions/actions/_product_with_option_value_action.html.erb ```

    <div class="calculator-fields row option-value-action-js">
      <div class="settings field omega four columns">
        <% promotion_action.calculator.preferences.keys.map do |key| %>
          <% field_name = "#{param_prefix}[calculator_attributes][preferred_#{key}]" %>
          <%= label_tag field_name, Spree.t(key.to_s) %>
          <%= preference_field_tag(field_name,
                                    promotion_action.calculator.get_preference(key),
                                   :type => promotion_action.calculator.preference_type(key)) %>
          <% end %>
          <%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", 
                promotion_action.calculator.id %>
      </div>
    </div>


##### and
``` app/views/spree/admin/promotions/rules/_product_with_option_value_rule.html.erb ```

      <div class="field alpha omega eight columns">
        <%= label_tag "#{param_prefix}_option_value_ids_string", 
                Spree.t('product_rule.choose_option_value') %>
        <%= hidden_field_tag "#{param_prefix}[option_value_ids_string]",
                              promotion_rule.option_value_ids.join(','),
                              class: 'option_value_picker fullwidth' %>
      </div>

    <%= javascript_tag do %>
      Spree.routes.option_values_search = "<%=spree.api_option_values_url(:format => 'json') %>";
      $('.option_value_picker').optionValueAutocomplete();
    <% end %>

Finally, add the translations used in the admin section:

``` config/locales/en.yml  ````

    en:
      hello: "Hello world"
      spree:
        product_rule:
          choose_option_value: 'Select the option values'
        promotion_rule_types:
          product_with_option_value_rule:
            name: Apply for all the products that have one of the following option values
            description: 'Rules for the discount in a product 
                with an option value (eg: small, blue, large, extra large, etc)'
        promotion_action_types:
          product_with_option_value_action:
            name: Set a price for the product
            description: Action to select the price for the product that apply this new discount


##### And there you go, we have finished with the required code.  Now you just have to run the pending migrations. To do so, run the following command from console:

     run rake db:migrate

##### and don't forget to restart your server!

By the way, I put the whole code into a Spree Extension, so if you don't have time to implement this tutorial step by step, you can use the extension to get the functionality quickly. If you also like testing you can find a couple of rspec tests in the extension.  ;-)

``` The spree extension ```

[Spree Extension with custom promotion coupon example](https://github.com/heridev/spree_extra_promotion_coupons)

If you want to see how the functionality of the promotional coupon that we have developed in this tutorial works, have a look at this video:

<iframe width="800" height="500" src="//www.youtube.com/embed/1syikOrPDtk" frameborder="0" allowfullscreen></iframe>

##### That's all from me for now, leave your comments bellow, and remember to follow us for more Tech and Development post.  Don't forget to share the knowledge and spread the love and peace!

``` H. ``` 

0 Shares:
You May Also Like
Read More

Setting Up EasyPost on Solidus

Reading Time: 5 minutes Solidus provides a flexible system to calculate shipping by accommodating a wide range of shipment pricing: from simple flat…
Read More

What’s Vtex?

Reading Time: 2 minutes VTEX is one of the best well-known Ecommerce platforms in LATAM because of its exponential growth in the…
Read More

Magento TDD with PHPSpec

Reading Time: 4 minutes Today, we are implementing the MageTest/MageSpec Module based on PHPSpec toolset. In this implementation, we have two different…