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

Distributed Amount Calculator #1949

Merged
merged 4 commits into from
May 23, 2017
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
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
<%= render partial: 'spree/admin/promotions/actions/promotion_calculators_with_custom_fields',
locals: { calculators: Spree::Promotion::Actions::CreateItemAdjustments.calculators, promotion_action: promotion_action, param_prefix: param_prefix } %>
<%= render(
"spree/admin/promotions/actions/promotion_calculators_with_custom_fields",
calculators: Spree::Promotion::Actions::CreateItemAdjustments.calculators,
promotion_action: promotion_action,
param_prefix: param_prefix
) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<%= fields_for "#{prefix}[calculator_attributes]", calculator do |f| %>
<%= f.label :preferred_amount %>
<%= render "spree/admin/shared/number_with_currency", f: f, amount_attr: :preferred_amount, currency_attr: :preferred_currency %>
<% end %>

<div class="field">
<p>
<%= admin_hint(
calculator.model_name.human,
"""
graygilmore marked this conversation as resolved.
Show resolved Hide resolved
<p>
This amount will be distributed to line items weighted relative to
their price. More expensive line items will receive a greater share
of the adjustment.
</p>

<p>
For example, with three line items and a preferred amount of $15 we
would end up with the following distribution:
</p>

<table>
<thead>
<tr>
<th></th>
<th>Price</th>
<th>Weighted Adj.</th>
</tr>
</thead>
<tbody>
<tr>
<td>Socks</td>
<td>$5</td>
<td>-$1.5</td>
</tr>
<tr>
<td>Shoes</td>
<td>$30</td>
<td>-$9</td>
</tr>
<tr>
<td>Slippers</td>
<td>$15</td>
<td>-$4.5</td>
</tr>
</tbody>
</table>
"""
) %>

This amount will be the <strong>total</strong> discount spread amongst all
graygilmore marked this conversation as resolved.
Show resolved Hide resolved
of the line items.
</p>
</div>
24 changes: 24 additions & 0 deletions core/app/models/spree/calculator/distributed_amount.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require_dependency 'spree/calculator'

# This is a calculator for line item adjustment actions. It accepts a line item
# and calculates its weighted adjustment amount based on the value of the
# preferred amount and the price of the other line items. More expensive line
# items will receive a greater share of the preferred amount.

module Spree
class Calculator::DistributedAmount < Calculator
preference :amount, :decimal, default: 0
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
end
end
end
end
43 changes: 43 additions & 0 deletions core/app/models/spree/distributed_amounts_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Spree
class DistributedAmountsHandler
attr_reader :line_item, :order, :total_amount

def initialize(line_item, total_amount)
@line_item = line_item
@order = line_item.order
@total_amount = total_amount
end

# @return [Float] the weighted adjustment for the initialized line item
def amount
distributed_amounts[@line_item.id].to_f
end

private

# @private
# @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
end
end
end
3 changes: 3 additions & 0 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,9 @@ en:
spree/calculator/default_tax:
one: Default Tax
other: Default Tax
spree/calculator/distributed_amount:
one: Distributed Amount
other: Distributed Amount
spree/calculator/flat_percent_item_total:
one: Flat Percent
other: Flat Percent
Expand Down
3 changes: 2 additions & 1 deletion core/lib/spree/core/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ class Engine < ::Rails::Engine
]

app.config.spree.calculators.promotion_actions_create_item_adjustments = %w[
Spree::Calculator::PercentOnLineItem
Spree::Calculator::DistributedAmount
Spree::Calculator::FlatRate
Spree::Calculator::FlexiRate
Spree::Calculator::PercentOnLineItem
Spree::Calculator::TieredPercent
]

Expand Down
32 changes: 32 additions & 0 deletions core/spec/models/spree/calculator/distributed_amount_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'spec_helper'
require 'shared_examples/calculator_shared_examples'

describe Spree::Calculator::DistributedAmount, type: :model do
describe "#compute_line_item" do
subject { calculator.compute_line_item(order.line_items.first) }

let(:calculator) { Spree::Calculator::DistributedAmount.new }

let(:order) do
FactoryGirl.create(
:order_with_line_items,
line_items_attributes: [{ price: 50 }, { price: 50 }, { price: 50 }]
)
end

before do
calculator.preferred_amount = 15
calculator.preferred_currency = currency
end

context "when the order currency matches the store's currency" do
let(:currency) { "USD" }
it { is_expected.to eq 5 }
end

context "when the order currency does not match the store's currency" do
let(:currency) { "CAD" }
it { is_expected.to eq 0 }
end
end
end
79 changes: 79 additions & 0 deletions core/spec/models/spree/distributed_amounts_handler_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require "spec_helper"

RSpec.describe Spree::DistributedAmountsHandler, type: :model do
let(:order) do
FactoryGirl.create(
:order_with_line_items,
line_items_attributes: line_items_attributes
)
end

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)
end
end

context "when there are multiple line items" do
let(:line_items_attributes) do
[{ price: 50 }, { price: 50 }, { price: 50 }]
end

context "and the line items are equally priced" do
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
]
).to eq(
[5, 5, 5]
)
end

context "and the total amount cannot be equally distributed" do
let(:total_amount) { 10 }

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
]
).to eq(
[3.33, 3.33, 3.34]
)
end
end
end

context "and the line items are not equally priced" do
let(:line_items_attributes) do
[{ price: 150 }, { price: 50 }, { price: 100 }]
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
]
).to eq(
[7.5, 2.5, 5]
)
end
end
end
end
end