Recently a client of ours came to us with a proposition of adding some discounts on their e-commerce store for customers that met certain conditions. They needed this implemented before the holiday season. The application in question is built on top of Spree Commerce so the path of least resistance was to expand on Spree's promotion class. Spree's promotion rules/actions section of the engine is one of the more complicated interactions that is handled in the platform but it is easily modified to fit most applications’ needs.
While adding promotion rules is well documented, I struggled with finding anything about the more complicated promotion action part of the equation. Google turned up some old posts based in Spree 1.X but nothing for spree 2+. Here I will outline the steps I took to get this up and running for Spree 2.4.
Create the custom action
This class will handle actually applying the adjustments we want to make on orders. By following Spree's guidelines for extending promotions, we know to create our new action at: app/models/spree/promotion_handler/discount_shipping.rb
module Spree
class Promotion
module Actions
class DiscountShipping < PromotionAction
include Spree::CalculatedAdjustments
before_validation :ensure_action_has_calculator
def perform(payload={})
order = payload[:order]
results = order.shipments.map do |shipment|
return false if promotion_credit_exists?(shipment)
shipment.adjustments.create!(
order: shipment.order,
amount: compute_amount(shipment),
source: self,
label: label,
)
true
end
results.any? { |r| r == true }
end
def label
"#{Spree.t(:promotion)} (#{promotion.name})"
end
def compute_amount(shipment)
amount = self.calculator.compute(shipment).to_f.abs
[shipment.cost, amount].min * -1
end
private
def promotion_credit_exists?(shipment)
shipment.adjustments.where(:source_id => self.id).exists?
end
def ensure_action_has_calculator
return if self.calculator
self.calculator = Spree::Calculator::FlatPercentItemTotal.new
end
end
end
end
end
Getting things working in the CMS
Spree needs to know about the new action that we just created. Add this line to your spree.rb
file located at: config/initializers/spree.rb
Rails.application.config.spree.promotions.actions << Spree::Promotion::Actions::DiscountShipping
Now we need to give Spree some text to render when we choose our new action in the CMS. Alter your en.yml file to look something like:
en:
spree:
promotion_action_types:
discount_shipping:
name: "Discount Shipping"
description: "Add a discounted shipping rate"
The last step is to create a partial for spree to use in the CMS. For this action, add a partial at: app/views/spree/admin/promotions/actions/_discount_shipping.html.erb
<div class="calculator-fields row">
<div class="field alpha four columns">
<% field_name = "#{param_prefix}[calculator_type]" %>
<%= label_tag field_name, Spree.t(:calculator) %>
<%= select_tag field_name,
options_from_collection_for_select(@calculators, :to_s, :description, promotion_action.calculator.type),
:class => 'type-select select2 fullwidth' %>
<% if promotion_action.calculator.respond_to?(:preferences) %>
<span class="warning info"><%= Spree.t(:calculator_settings_warning) %></span>
<% end %>
</div>
<% unless promotion_action.new_record? %>
<div class="settings field omega four columns">
<% type_name = promotion_action.calculator.type.demodulize.underscore %>
<% if lookup_context.exists?("fields",
["spree/admin/promotions/calculators/#{type_name}"], true) %>
<%= render "spree/admin/promotions/calculators/#{type_name}/fields",
calculator: promotion_action.calculator, prefix: param_prefix %>
<% else %>
<%= render "spree/admin/promotions/calculators/default_fields",
calculator: promotion_action.calculator, prefix: param_prefix %>
<% end %>
<%= hidden_field_tag "#{param_prefix}[calculator_attributes][id]", promotion_action.calculator.id %>
</div>
<% end %>
</div>
This will give you access to all Spree's built-in calculators. If your new action requires no user input then this can simply be a blank file.
Now we should be able to successfully add our new action to any promotion! But we aren't done yet.
Tying it all together
We need to instruct Spree when to actually apply our promotion. For this example, a shipment isn't generated until a user is in a certain part of the checkout process. We want to apply our promotion earlier in the process, when a user transitions to the delivery checkout step, so a shipment record exists for our promotion.
Create a file at: app/models/spree/promotion_handler/discount_shipping.rb
module Spree
module PromotionHandler
class DiscountShipping
attr_reader :order, :order_promo_ids
attr_accessor :error, :success
def initialize(order)
@order = order
@order_promo_ids = order.promotions.pluck(:id)
end
def activate
promotions.each do |promotion|
next if promotion.code.present? && !order_promo_ids.include?(promotion.id)
if promotion.eligible?(order)
promotion.activate(order: order)
end
end
end
private
def promotions
Spree::Promotion.active.where(
id: Spree::Promotion::Actions::DiscountShipping.pluck(:promotion_id),
path: nil
)
end
end
end
end
The final step is telling Spree where in the order process to apply our action. If you don't have an extension class set up for the order model, go ahead and create that now:
app/models/spree/order_decorator.rb
Spree::Order.class_eval do
state_machine.before_transition to: :delivery do |order|
order.apply_shipping_discounts
end
def apply_shipping_discounts
Spree::PromotionHandler::DiscountShipping.new(self).activate
shipments.each { |shipment| Spree::ItemAdjustments.new(shipment).update }
updater.update_shipment_total
persist_totals
end
end
So now when a user is eligible for our promotion with our new action, Spree will create an adjustment on the order's shipments based on the action's calculator and the values that were input in the CMS for the calculator to use.
That's a Wrap!
We now have a functioning new action that will apply to eligible orders on the delivery step.
Happy coding!
This was built with:
- Spree 2.4.9
- Ruby 2.2.2
- Rails 4.1.11