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:
- Spree::PromotionRule (Indicates the condition/conditions required to apply the discount)
- Spree::PromotionAction (Sets the adjustment or discount to apply and also applies some validation for the discounts)
- 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. ```