Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Distribute over eligible line items #2582

Merged
merged 6 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions core/app/models/spree/calculator/distributed_amount.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 28 additions & 26 deletions core/app/models/spree/distributed_amounts_handler.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -19,25 +19,27 @@ def amount
# @return [Hash<Integer, BigDecimal>] 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
48 changes: 48 additions & 0 deletions core/spec/models/spree/calculator/distributed_amount_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
32 changes: 17 additions & 15 deletions core/spec/models/spree/distributed_amounts_handler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]
Expand All @@ -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]
Expand Down