diff --git a/core/app/models/spree/calculator/distributed_amount.rb b/core/app/models/spree/calculator/distributed_amount.rb index aff818e79a2..c42ef3bb7df 100644 --- a/core/app/models/spree/calculator/distributed_amount.rb +++ b/core/app/models/spree/calculator/distributed_amount.rb @@ -11,13 +11,20 @@ class Calculator::DistributedAmount < Calculator preference :currency, :string, default: -> { Spree::Config[:currency] } def compute_line_item(line_item) - if line_item && preferred_currency.casecmp(line_item.currency).zero? - Spree::DistributedAmountsHandler.new( - line_item, - preferred_amount - ).amount - else - 0 + return 0 unless line_item + return 0 unless preferred_currency.casecmp(line_item.currency).zero? + return 0 unless calculable.promotion.line_item_actionable?(line_item.order, line_item) + Spree::DistributedAmountsHandler.new( + actionable_line_items(line_item.order), + preferred_amount + ).amount(line_item) + end + + private + + def actionable_line_items(order) + order.line_items.select do |line_item| + calculable.promotion.line_item_actionable?(order, line_item) end end end diff --git a/core/app/models/spree/distributed_amounts_handler.rb b/core/app/models/spree/distributed_amounts_handler.rb index 4d09c9da770..9a5c774b409 100644 --- a/core/app/models/spree/distributed_amounts_handler.rb +++ b/core/app/models/spree/distributed_amounts_handler.rb @@ -1,16 +1,16 @@ module Spree class DistributedAmountsHandler - attr_reader :line_item, :order, :total_amount + attr_reader :line_items, :total_amount - def initialize(line_item, total_amount) - @line_item = line_item - @order = line_item.order + def initialize(line_items, total_amount) + @line_items = line_items @total_amount = total_amount end - # @return [Float] the weighted adjustment for the initialized line item - def amount - distributed_amounts[@line_item.id].to_f + # @param [LineItem] one of the line_items distributed over + # @return [BigDecimal] the weighted adjustment for this line_item + def amount(line_item) + distributed_amounts[line_item.id].to_d end private @@ -19,25 +19,27 @@ def amount # @return [Hash] a hash of line item IDs and their # corresponding weighted adjustments def distributed_amounts - remaining_amount = @total_amount - - @order.line_items.each_with_index.map do |line_item, i| - if i == @order.line_items.length - 1 - # If this is the last line item on the order we want to use the - # remaining preferred amount to ensure our total adjustment is what - # has been set as the preferred amount. - [line_item.id, remaining_amount] - else - # Calculate the weighted amount by getting this line item's share of - # the order's total and multiplying it with the preferred amount. - weighted_amount = ((line_item.amount / @order.item_total) * total_amount).round(2) - - # Subtract this line item's weighted amount from the total. - remaining_amount -= weighted_amount - - [line_item.id, weighted_amount] - end - end.to_h + Hash[line_item_ids.zip(allocated_amounts)] + end + + def line_item_ids + line_items.map(&:id) + end + + def elligible_amounts + line_items.map(&:amount) + end + + def subtotal + elligible_amounts.sum + end + + def weights + elligible_amounts.map { |amount| amount.to_f / subtotal.to_f } + end + + def allocated_amounts + total_amount.to_money.allocate(weights).map(&:to_money) end end end diff --git a/core/spec/models/spree/calculator/distributed_amount_spec.rb b/core/spec/models/spree/calculator/distributed_amount_spec.rb index d96cc2a8123..edff82ad6dd 100644 --- a/core/spec/models/spree/calculator/distributed_amount_spec.rb +++ b/core/spec/models/spree/calculator/distributed_amount_spec.rb @@ -2,10 +2,56 @@ require 'shared_examples/calculator_shared_examples' RSpec.describe Spree::Calculator::DistributedAmount, type: :model do + context 'applied to an order' do + let(:calculator) { Spree::Calculator::DistributedAmount.new } + let(:promotion) { + create :promotion, + name: '15 spread' + } + let(:order) { + create :completed_order_with_promotion, + promotion: promotion, + line_items_attributes: [{ price: 20 }, { price: 30 }, { price: 100 }] + } + + before do + calculator.preferred_amount = 15 + Spree::Promotion::Actions::CreateItemAdjustments.create!(calculator: calculator, promotion: promotion) + order.recalculate + end + + it 'correctly distributes the entire discount' do + expect(order.promo_total).to eq(-15) + expect(order.line_items.map(&:adjustment_total)).to eq([-2, -3, -10]) + end + + context 'with product promotion rule' do + let(:first_product) { order.line_items.first.product } + + before do + rule = Spree::Promotion::Rules::Product.create!( + promotion: promotion, + product_promotion_rules: [ + Spree::ProductPromotionRule.new(product: first_product), + ], + ) + promotion.rules << rule + promotion.save! + order.recalculate + end + + it 'still distributes the entire discount' do + expect(order.promo_total).to eq(-15) + expect(order.line_items.map(&:adjustment_total)).to eq([-15, 0, 0]) + end + end + end + describe "#compute_line_item" do subject { calculator.compute_line_item(order.line_items.first) } let(:calculator) { Spree::Calculator::DistributedAmount.new } + let(:promotion) { create(:promotion) } let(:order) do FactoryBot.create( @@ -17,11 +63,13 @@ before do calculator.preferred_amount = 15 calculator.preferred_currency = currency + Spree::Promotion::Actions::CreateItemAdjustments.create!(calculator: calculator, promotion: promotion) end context "when the order currency matches the store's currency" do let(:currency) { "USD" } it { is_expected.to eq 5 } + it { is_expected.to be_a BigDecimal } end context "when the order currency does not match the store's currency" do diff --git a/core/spec/models/spree/distributed_amounts_handler_spec.rb b/core/spec/models/spree/distributed_amounts_handler_spec.rb index c66b218ab6f..2bda51537a8 100644 --- a/core/spec/models/spree/distributed_amounts_handler_spec.rb +++ b/core/spec/models/spree/distributed_amounts_handler_spec.rb @@ -8,17 +8,19 @@ ) end + let(:handler) { + described_class.new(order.line_items, total_amount) + } + describe "#amount" do let(:total_amount) { 15 } - subject { described_class.new(line_item, total_amount).amount } - context "when there is only one line item" do let(:line_items_attributes) { [{ price: 100 }] } let(:line_item) { order.line_items.first } it "applies the entire amount to the line item" do - expect(subject).to eq(15) + expect(handler.amount(line_item)).to eq(15) end end @@ -31,9 +33,9 @@ it "evenly distributes the total amount" do expect( [ - described_class.new(order.line_items[0], total_amount).amount, - described_class.new(order.line_items[1], total_amount).amount, - described_class.new(order.line_items[2], total_amount).amount + handler.amount(order.line_items[0]), + handler.amount(order.line_items[1]), + handler.amount(order.line_items[2]) ] ).to eq( [5, 5, 5] @@ -46,28 +48,28 @@ it "applies the remainder of the total amount to the last item" do expect( [ - described_class.new(order.line_items[0], total_amount).amount, - described_class.new(order.line_items[1], total_amount).amount, - described_class.new(order.line_items[2], total_amount).amount + handler.amount(order.line_items[0]), + handler.amount(order.line_items[1]), + handler.amount(order.line_items[2]) ] - ).to eq( + ).to match_array( [3.33, 3.33, 3.34] ) end end end - context "and the line items are not equally priced" do + context "and the line items do not have equal subtotal amounts" do let(:line_items_attributes) do - [{ price: 150 }, { price: 50 }, { price: 100 }] + [{ price: 50, quantity: 3 }, { price: 50, quantity: 1 }, { price: 50, quantity: 2 }] end it "distributes the total amount relative to the item's price" do expect( [ - described_class.new(order.line_items[0], total_amount).amount, - described_class.new(order.line_items[1], total_amount).amount, - described_class.new(order.line_items[2], total_amount).amount + handler.amount(order.line_items[0]), + handler.amount(order.line_items[1]), + handler.amount(order.line_items[2]) ] ).to eq( [7.5, 2.5, 5]