Skip to content

Commit

Permalink
Introduce Spree::Discounts
Browse files Browse the repository at this point in the history
This module handles discounts from the order updater.

The system will check all line items and shipments for any applicable
discounts and select the best for each line item / shipment in an
understandable, performant way.
  • Loading branch information
mamhoff committed Mar 18, 2022
1 parent df0a3b6 commit 2c59874
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 15 deletions.
16 changes: 16 additions & 0 deletions core/app/models/spree/discounts/chooser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module Spree
module Discounts
class Chooser
def initialize(_discountable)
# This signature is here to provide context in case
# this needs to be customized
end

def call(discounts)
[discounts.max_by(&:amount)].compact
end
end
end
end
28 changes: 28 additions & 0 deletions core/app/models/spree/discounts/line_item_discounter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Spree
module Discounts
class LineItemDiscounter
attr_reader :promotions

def initialize(promotions:)
@promotions = promotions
end

def call(line_item)
discounts = promotions.select do |promotion|
promotion.line_item_discountable?(line_item)
end.flat_map do |promotion|
promotion.actions.select do |action|
action.discountable_class == Spree::LineItem
end.map do |action|
action.discount(line_item)
end
end

chosen_discounts = Spree::Config.discount_chooser_class.new(line_item).call(discounts)
line_item.discounts = chosen_discounts
end
end
end
end
67 changes: 67 additions & 0 deletions core/app/models/spree/discounts/order_discounter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

module Spree
module Discounts
class OrderDiscounter
attr_reader :order

def initialize(order)
@order = order
end

def call
discount_line_items
discount_shipments
end

private

def discount_line_items
line_item_promotions = promotions.select do |promotion|
promotion.actions.any? { |promotion_action| promotion_action.discountable_class == Spree::LineItem }
end
line_item_discounter = LineItemDiscounter.new(promotions: line_item_promotions)
order.line_items.each { |line_item| line_item_discounter.call(line_item) }
end

def discount_shipments
shipment_promotions = promotions.select do |promotion|
promotion.actions.any? { |promotion_action| promotion_action.discountable_class == Spree::Shipment }
end
shipment_discounter = ShipmentDiscounter.new(promotions: shipment_promotions)
order.shipments.each { |shipment| shipment_discounter.call(shipment) }
end

def promotions
@_promotions ||= begin
preloader = ActiveRecord::Associations::Preloader.new
(connected_order_promotions | sale_promotions).select do |promotion|
promotion.activatable?(order)
end.map do |promotion|
preloader.preload(promotion.rules.select { |r| r.type == "Spree::Promotion::Rules::Product" }, :products)
preloader.preload(promotion.rules.select { |r| r.type == "Spree::Promotion::Rules::Store" }, :stores)
preloader.preload(promotion.rules.select { |r| r.type == "Spree::Promotion::Rules::Taxon" }, :taxons)
preloader.preload(promotion.rules.select { |r| r.type == "Spree::Promotion::Rules::User" }, :users)
preloader.preload(promotion.actions.select { |a| a.respond_to?(:calculator) }, :calculator)
promotion
end
end.select { |promotion| promotion.order_discountable?(order) }
end

def connected_order_promotions
order.promotions.includes(promotion_includes).select(&:active?)
end

def sale_promotions
Spree::Promotion.where(apply_automatically: true).active.includes(promotion_includes)
end

def promotion_includes
[
:promotion_rules,
:promotion_actions,
]
end
end
end
end
28 changes: 28 additions & 0 deletions core/app/models/spree/discounts/shipment_discounter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Spree
module Discounts
class ShipmentDiscounter
attr_reader :promotions

def initialize(promotions:)
@promotions = promotions
end

def call(shipment)
shipment.discounts = promotions.select do |promotion|
promotion.shipment_discountable?(shipment)
end.flat_map do |promotion|
promotion.actions.select do |action|
action.discountable_class == Spree::Shipment
end.map do |action|
action.discount(shipment)
end
end

chosen_discounts = Spree::Config.discount_chooser_class.new(shipment).call(discounts)
shipment.discounts = chosen_discounts
end
end
end
end
14 changes: 11 additions & 3 deletions core/app/models/spree/order_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,12 @@ def recalculate_adjustments
# http://www.hmrc.gov.uk/vat/managing/charging/discounts-etc.htm#1
# It also fits the criteria for sales tax as outlined here:
# http://www.boe.ca.gov/formspubs/pub113/
update_item_promotions
update_order_promotions
if Spree::Config.promotion_system == :adjustments
update_item_promotions
update_order_promotions
elsif Spree::Promotion.order_activatable?(order)
Spree::Config.discounter_class.new(order).call
end
update_taxes
update_cancellations
update_item_totals
Expand Down Expand Up @@ -246,7 +250,11 @@ def update_item_totals
item.adjustment_total = item.adjustments.
select(&:eligible?).
reject(&:included?).
sum(&:amount)
sum(&:amount) + item.discounts.sum(&:amount)
item.promo_total = item.adjustments.
select(&:promotion?).
select(&:eligible?).
sum(&:amount) + item.discounts.sum(&:amount)

if item.changed?
item.update_columns(
Expand Down
6 changes: 6 additions & 0 deletions core/lib/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,12 @@ def default_pricing_options
# promotion_chooser_class allows extensions to provide their own PromotionChooser
class_name_attribute :promotion_chooser_class, default: 'Spree::PromotionChooser'

# promotion_handler_class allows extensions to provide their own Discount Order Updater
class_name_attribute :discounter_class, default: 'Spree::Discounts::OrderDiscounter'

# discount_chooser_class allows extensions to provide their own discount chooser
class_name_attribute :discount_chooser_class, default: 'Spree::Discounts::Chooser'

class_name_attribute :allocator_class, default: 'Spree::Stock::Allocator::OnHandFirst'

class_name_attribute :shipping_rate_sorter_class, default: 'Spree::Stock::ShippingRateSorter'
Expand Down
13 changes: 1 addition & 12 deletions core/spec/models/spree/order_contents_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@
include_context "discount changes order total"
end

context "with new discount-based promotion system", pending: "Waiting for implementation" do
context "with new discount-based promotion system" do
around do |example|
with_unfrozen_spree_preference_store do
Spree::Config.promotion_system = :discounts
Expand All @@ -122,17 +122,6 @@
end
end

context "one active order promotion" do
let!(:action) { Spree::Promotion::Actions::CreateAdjustment.create(promotion: promotion, calculator: calculator) }

it "creates valid discount on order" do
subject.add(variant, 1)
expect(subject.order.discounts.to_a.sum(&:amount)).not_to eq 0
end

include_context "discount changes order total"
end

context "one active line item promotion" do
let!(:action) { Spree::Promotion::Actions::CreateItemAdjustments.create(promotion: promotion, calculator: calculator) }

Expand Down

0 comments on commit 2c59874

Please sign in to comment.