diff --git a/core/app/models/spree/discounts/chooser.rb b/core/app/models/spree/discounts/chooser.rb new file mode 100644 index 00000000000..9e631e9cd54 --- /dev/null +++ b/core/app/models/spree/discounts/chooser.rb @@ -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 diff --git a/core/app/models/spree/discounts/line_item_updater.rb b/core/app/models/spree/discounts/line_item_updater.rb new file mode 100644 index 00000000000..1ab58fc1481 --- /dev/null +++ b/core/app/models/spree/discounts/line_item_updater.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Spree + module Discounts + class LineItemUpdater + attr_reader :promotions + + def initialize(promotions:) + @promotions = promotions + end + + def call(line_item) + discounts = promotions.select do |promotion| + promotion.line_item_eligible?(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 diff --git a/core/app/models/spree/discounts/order_updater.rb b/core/app/models/spree/discounts/order_updater.rb index 486f5bf999e..b7d5db9a74c 100644 --- a/core/app/models/spree/discounts/order_updater.rb +++ b/core/app/models/spree/discounts/order_updater.rb @@ -10,7 +10,57 @@ def initialize(order) end def call - order + update_line_items + update_shipments + end + + private + + def update_line_items + line_item_promotions = promotions.select do |promotion| + promotion.actions.any? { |promotion_action| promotion_action.discountable_class == Spree::LineItem } + end + line_item_promo_updater = LineItemUpdater.new(promotions: line_item_promotions) + order.line_items.each { |line_item| line_item_promo_updater.call(line_item) } + end + + def update_shipments + shipment_promotions = promotions.select do |promotion| + promotion.actions.any? { |promotion_action| promotion_action.discountable_class == Spree::Shipment } + end + shipment_promo_updater = ShipmentUpdater.new(promotions: shipment_promotions) + order.shipments.each { |shipment| shipment_promo_updater.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 diff --git a/core/app/models/spree/discounts/shipment_updater.rb b/core/app/models/spree/discounts/shipment_updater.rb new file mode 100644 index 00000000000..45cccd267cd --- /dev/null +++ b/core/app/models/spree/discounts/shipment_updater.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Spree + module Discounts + class ShipmentUpdater + attr_reader :promotions + + def initialize(promotions:) + @promotions = promotions + end + + def call(shipment) + shipment.discounts = promotions.select do |promotion| + promotion.shipment_eligible?(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.promo_total = chosen_discounts.sum(&:amount) + shipment.discounts = chosen_discounts + end + end + end +end diff --git a/core/app/models/spree/order_updater.rb b/core/app/models/spree/order_updater.rb index 836b46e35ab..bcb5d84e804 100644 --- a/core/app/models/spree/order_updater.rb +++ b/core/app/models/spree/order_updater.rb @@ -114,7 +114,7 @@ def recalculate_adjustments update_item_promotions update_order_promotions else - Spree::Config.promotion_handler_class.new(order).call + Spree::Config.discount_updater_class.new(order).call end update_taxes update_cancellations @@ -250,7 +250,7 @@ def update_item_totals item.adjustment_total = item.adjustments. select(&:eligible?). reject(&:included?). - sum(&:amount) + sum(&:amount) + item.discounts.sum(&:amount) if item.changed? item.update_columns( diff --git a/core/lib/spree/app_configuration.rb b/core/lib/spree/app_configuration.rb index b0287ad0608..fc59137c3c9 100644 --- a/core/lib/spree/app_configuration.rb +++ b/core/lib/spree/app_configuration.rb @@ -321,8 +321,11 @@ 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 Promotion Handler - class_name_attribute :promotion_handler_class, default: 'Spree::PromotionHandler::Order' + # promotion_handler_class allows extensions to provide their own Discount Order Updater + class_name_attribute :discount_updater_class, default: 'Spree::Discounts::OrderUpdater' + + # 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' diff --git a/core/spec/models/spree/order_contents_spec.rb b/core/spec/models/spree/order_contents_spec.rb index 40bd5aaf5f8..7a3dffa8ee6 100644 --- a/core/spec/models/spree/order_contents_spec.rb +++ b/core/spec/models/spree/order_contents_spec.rb @@ -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 @@ -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) }