From 66cece5903d0ca92dd77e9e0e7f8e39a9a3dbacb Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 30 Jul 2014 17:46:21 +1000 Subject: [PATCH] WIP: Extract order cycle fee calculations to EnterpriseFeeCalculator --- app/models/order_cycle.rb | 78 +--------- .../enterprise_fee_calculator.rb | 75 ++++++++++ .../enterprise_fee_applicator_spec.rb | 1 - .../enterprise_fee_calculator_spec.rb | 138 ++++++++++++++++++ spec/models/order_cycle_spec.rb | 101 ------------- 5 files changed, 217 insertions(+), 176 deletions(-) create mode 100644 lib/open_food_network/enterprise_fee_calculator.rb create mode 100644 spec/lib/open_food_network/enterprise_fee_calculator_spec.rb diff --git a/app/models/order_cycle.rb b/app/models/order_cycle.rb index f2ad8fee3e..d5a1371876 100644 --- a/app/models/order_cycle.rb +++ b/app/models/order_cycle.rb @@ -1,5 +1,3 @@ -require 'open_food_network/enterprise_fee_applicator' - class OrderCycle < ActiveRecord::Base belongs_to :coordinator, :class_name => 'Enterprise' has_and_belongs_to_many :coordinator_fees, :class_name => 'EnterpriseFee', :join_table => 'coordinator_fees' @@ -165,77 +163,17 @@ class OrderCycle < ActiveRecord::Base exchange_for_distributor(distributor).andand.pickup_instructions end - - # -- Fees - - # TODO: The boundary of this class is ill-defined here. OrderCycle should not know about - # EnterpriseFeeApplicator. Clients should be able to query it for relevant EnterpriseFees. - # This logic would fit better in another service object. - - def fees_for(variant, distributor) - per_item_enterprise_fee_applicators_for(variant, distributor).sum do |applicator| - # Spree's Calculator interface accepts Orders or LineItems, - # so we meet that interface with a struct. - # Amount is faked, this is a method on LineItem - line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price - applicator.enterprise_fee.compute_amount(line_item) - end + def exchanges_carrying(variant, distributor) + exchanges.supplying_to(distributor).with_variant(variant) end - def create_line_item_adjustments_for(line_item) - variant = line_item.variant - distributor = line_item.order.distributor - - per_item_enterprise_fee_applicators_for(variant, distributor).each do |applicator| - applicator.create_line_item_adjustment(line_item) - end - end - - def create_order_adjustments_for(order) - per_order_enterprise_fee_applicators_for(order).each do |applicator| - applicator.create_order_adjustment(order) - end + def exchanges_supplying(order) + exchanges.supplying_to(order.distributor).with_any_variant(order.variants) end private - # -- Fees - def per_item_enterprise_fee_applicators_for(variant, distributor) - fees = [] - - exchanges_carrying(variant, distributor).each do |exchange| - exchange.enterprise_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) - end - end - - coordinator_fees.per_item.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') - end - - fees - end - - def per_order_enterprise_fee_applicators_for(order) - fees = [] - - exchanges_supplying(order).each do |exchange| - exchange.enterprise_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) - end - end - - coordinator_fees.per_order.each do |enterprise_fee| - fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') - end - - fees - end - - - # -- Misc - # If a product without variants is added to an order cycle, and then some variants are added # to that product, then the master variant is still part of the order cycle, but customers # should not be able to purchase it. @@ -246,12 +184,4 @@ class OrderCycle < ActiveRecord::Base distributed_variants.include?(product.master) && (product.variants & distributed_variants).empty? end - - def exchanges_carrying(variant, distributor) - exchanges.supplying_to(distributor).with_variant(variant) - end - - def exchanges_supplying(order) - exchanges.supplying_to(order.distributor).with_any_variant(order.variants) - end end diff --git a/lib/open_food_network/enterprise_fee_calculator.rb b/lib/open_food_network/enterprise_fee_calculator.rb new file mode 100644 index 0000000000..bbd220cfad --- /dev/null +++ b/lib/open_food_network/enterprise_fee_calculator.rb @@ -0,0 +1,75 @@ +require 'open_food_network/enterprise_fee_applicator' + +module OpenFoodNetwork + class EnterpriseFeeCalculator + def initialize(distributor=nil, order_cycle=nil) + @distributor = distributor + @order_cycle = order_cycle + end + + + def fees_for(variant) + per_item_enterprise_fee_applicators_for(variant).sum do |applicator| + # Spree's Calculator interface accepts Orders or LineItems, + # so we meet that interface with a struct. + # Amount is faked, this is a method on LineItem + line_item = OpenStruct.new variant: variant, quantity: 1, amount: variant.price + applicator.enterprise_fee.compute_amount(line_item) + end + end + + def create_line_item_adjustments_for(line_item) + variant = line_item.variant + @distributor = line_item.order.distributor + @order_cycle = line_item.order.order_cycle + + per_item_enterprise_fee_applicators_for(variant).each do |applicator| + applicator.create_line_item_adjustment(line_item) + end + end + + def create_order_adjustments_for(order) + @distributor = order.distributor + @order_cycle = order.order_cycle + + per_order_enterprise_fee_applicators_for(order).each do |applicator| + applicator.create_order_adjustment(order) + end + end + + + private + + def per_item_enterprise_fee_applicators_for(variant) + fees = [] + + @order_cycle.exchanges_carrying(variant, @distributor).each do |exchange| + exchange.enterprise_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_item.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, variant, 'coordinator') + end + + fees + end + + def per_order_enterprise_fee_applicators_for(order) + fees = [] + + @order_cycle.exchanges_supplying(order).each do |exchange| + exchange.enterprise_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, exchange.role) + end + end + + @order_cycle.coordinator_fees.per_order.each do |enterprise_fee| + fees << OpenFoodNetwork::EnterpriseFeeApplicator.new(enterprise_fee, nil, 'coordinator') + end + + fees + end + end +end diff --git a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb index d10a5e9b72..9ad1d222b3 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -27,7 +27,6 @@ module OpenFoodNetwork it "creates an adjustment for an order" do order = create(:order) - #line_item = create(:line_item) enterprise_fee = create(:enterprise_fee) product = create(:simple_product) diff --git a/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb new file mode 100644 index 0000000000..697bb3c702 --- /dev/null +++ b/spec/lib/open_food_network/enterprise_fee_calculator_spec.rb @@ -0,0 +1,138 @@ +require 'open_food_network/enterprise_fee_calculator' + +module OpenFoodNetwork + describe EnterpriseFeeCalculator do + describe "integration" do + let(:coordinator) { create(:distributor_enterprise) } + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle) } + let(:product) { create(:simple_product, price: 10.00) } + + describe "calculating fees for a variant via a particular distribution" do + it "sums all the per-item fees for the variant in the specified hub + order cycle" do + enterprise_fee1 = create(:enterprise_fee, amount: 20) + enterprise_fee2 = create(:enterprise_fee, amount: 3) + enterprise_fee3 = create(:enterprise_fee, + calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) + + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 23 + end + + it "sums percentage fees for the variant" do + enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) + + create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, + enterprise_fees: [enterprise_fee1], variants: [product.master]) + + product.master.price.should == 10.00 + EnterpriseFeeCalculator.new(distributor, order_cycle).fees_for(product.master).should == 2.00 + end + end + + describe "creating adjustments" do + let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } + let!(:line_item) { create(:line_item, order: order, variant: product.master) } + let(:enterprise_fee_line_item) { create(:enterprise_fee) } + let(:enterprise_fee_order) { create(:enterprise_fee, calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) } + let!(:exchange) { create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, variants: [product.master]) } + + before { order.reload } + + it "creates adjustments for a line item" do + exchange.enterprise_fees << enterprise_fee_line_item + + EnterpriseFeeCalculator.new.create_line_item_adjustments_for line_item + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_line_item.name + end + + it "creates adjustments for an order" do + exchange.enterprise_fees << enterprise_fee_order + + EnterpriseFeeCalculator.new.create_order_adjustments_for order + + a = Spree::Adjustment.last + a.metadata.fee_name.should == enterprise_fee_order.name + end + end + end + + describe "creating adjustments for a line item" do + let(:oc) { OrderCycle.new } + let(:variant) { double(:variant) } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + let(:line_item) { double(:line_item, variant: variant, order: order) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_line_item_adjustment).with(line_item) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant) { [applicator] } + + efc.create_line_item_adjustments_for line_item + end + + it "makes fee applicators for a line item" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } + + oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_item_enterprise_fee_applicators_for, line_item.variant).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] + end + end + + describe "creating adjustments for an order" do + let(:oc) { OrderCycle.new } + let(:distributor) { double(:distributor) } + let(:order) { double(:order, distributor: distributor, order_cycle: oc) } + + it "creates an adjustment for each fee" do + applicator = double(:enterprise_fee_applicator) + applicator.should_receive(:create_order_adjustment).with(order) + + efc = EnterpriseFeeCalculator.new + efc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } + + efc.create_order_adjustments_for order + end + + it "makes fee applicators for an order" do + distributor = double(:distributor) + ef1 = double(:enterprise_fee) + ef2 = double(:enterprise_fee) + ef3 = double(:enterprise_fee) + incoming_exchange = double(:exchange, role: 'supplier') + outgoing_exchange = double(:exchange, role: 'distributor') + incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } + outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } + + oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } + oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } + + efc = EnterpriseFeeCalculator.new(distributor, oc) + efc.send(:per_order_enterprise_fee_applicators_for, order).should == + [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), + OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] + end + end + end +end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index e4f979dc5b..9785d5ed2d 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -368,107 +368,6 @@ describe OrderCycle do end end - describe "calculating fees for a variant via a particular distributor" do - it "sums all the per-item fees for the variant in the specified hub + order cycle" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20) - enterprise_fee2 = create(:enterprise_fee, amount: 3) - enterprise_fee3 = create(:enterprise_fee, - calculator: Spree::Calculator::FlatRate.new(preferred_amount: 2)) - product = create(:simple_product) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1, enterprise_fee2, enterprise_fee3], variants: [product.master]) - - order_cycle.fees_for(product.master, distributor).should == 23 - end - - - it "sums percentage fees for the variant" do - coordinator = create(:distributor_enterprise) - distributor = create(:distributor_enterprise) - order_cycle = create(:simple_order_cycle) - enterprise_fee1 = create(:enterprise_fee, amount: 20, fee_type: "admin", calculator: Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 20)) - product = create(:simple_product, price: 10.00) - - create(:exchange, order_cycle: order_cycle, sender: coordinator, receiver: distributor, incoming: false, - enterprise_fees: [enterprise_fee1], variants: [product.master]) - - product.master.price.should == 10.00 - order_cycle.fees_for(product.master, distributor).should == 2.00 - end - end - - describe "creating adjustments for a line item" do - let(:oc) { OrderCycle.new } - let(:variant) { double(:variant) } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - let(:line_item) { double(:line_item, variant: variant, order: order) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_line_item_adjustment).with(line_item) - oc.should_receive(:per_item_enterprise_fee_applicators_for).with(variant, distributor) { [applicator] } - - oc.send(:create_line_item_adjustments_for, line_item) - end - - it "makes fee applicators for a line item" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_item) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_item) { [ef2] } - - oc.stub(:exchanges_carrying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_item) { [ef3] } - - oc.send(:per_item_enterprise_fee_applicators_for, line_item.variant, distributor).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, line_item.variant, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, line_item.variant, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, line_item.variant, 'coordinator')] - end - end - - describe "creating adjustments for an order" do - let(:oc) { OrderCycle.new } - let(:distributor) { double(:distributor) } - let(:order) { double(:order, distributor: distributor) } - - it "creates an adjustment for each fee" do - applicator = double(:enterprise_fee_applicator) - applicator.should_receive(:create_order_adjustment).with(order) - oc.should_receive(:per_order_enterprise_fee_applicators_for).with(order) { [applicator] } - - oc.send(:create_order_adjustments_for, order) - end - - it "makes fee applicators for an order" do - distributor = double(:distributor) - ef1 = double(:enterprise_fee) - ef2 = double(:enterprise_fee) - ef3 = double(:enterprise_fee) - incoming_exchange = double(:exchange, role: 'supplier') - outgoing_exchange = double(:exchange, role: 'distributor') - incoming_exchange.stub_chain(:enterprise_fees, :per_order) { [ef1] } - outgoing_exchange.stub_chain(:enterprise_fees, :per_order) { [ef2] } - - oc.stub(:exchanges_supplying) { [incoming_exchange, outgoing_exchange] } - oc.stub_chain(:coordinator_fees, :per_order) { [ef3] } - - oc.send(:per_order_enterprise_fee_applicators_for, order).should == - [OpenFoodNetwork::EnterpriseFeeApplicator.new(ef1, nil, 'supplier'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef2, nil, 'distributor'), - OpenFoodNetwork::EnterpriseFeeApplicator.new(ef3, nil, 'coordinator')] - end - end - describe "finding recently closed order cycles" do it "should give the most recently closed order cycle for a distributor" do distributor = create(:distributor_enterprise)