diff --git a/lib/open_food_network/enterprise_fee_applicator.rb b/lib/open_food_network/enterprise_fee_applicator.rb index 45905bfaca..9a2bd657b5 100644 --- a/lib/open_food_network/enterprise_fee_applicator.rb +++ b/lib/open_food_network/enterprise_fee_applicator.rb @@ -2,12 +2,16 @@ module OpenFoodNetwork class EnterpriseFeeApplicator < Struct.new(:enterprise_fee, :variant, :role) def create_line_item_adjustment(line_item) a = enterprise_fee.create_locked_adjustment(line_item_adjustment_label, line_item.order, line_item, true) + AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role end def create_order_adjustment(order) a = enterprise_fee.create_locked_adjustment(order_adjustment_label, order, order, true) + AdjustmentMetadata.create! adjustment: a, enterprise: enterprise_fee.enterprise, fee_name: enterprise_fee.name, fee_type: enterprise_fee.fee_type, enterprise_role: role + + a.set_absolute_included_tax! adjustment_tax(order, a) end @@ -24,6 +28,46 @@ module OpenFoodNetwork def base_adjustment_label "#{enterprise_fee.fee_type} fee by #{role} #{enterprise_fee.enterprise.name}" end + + def adjustment_tax(order, adjustment) + enterprise_fee.tax_category.tax_rates.match(order).sum do |rate| + compute_tax rate, adjustment.amount + end + end + + # Apply a TaxRate to a particular amount. TaxRates normally compute against + # LineItems or Orders, so we mock out a line item here to fit the interface + # that our calculator (usually DefaultTax) expects. + def compute_tax(tax_rate, amount) + product = OpenStruct.new tax_category: tax_rate.tax_category + line_item = Spree::LineItem.new quantity: 1 + line_item.define_singleton_method(:product) { product } + line_item.define_singleton_method(:price) { amount } + + # The enterprise fee adjustments for which we're calculating tax are always inclusive of + # tax. However, there's nothing to stop an admin from setting one up with a tax rate + # that's marked as not inclusive of tax, and that would result in the DefaultTax + # calculator generating a slightly incorrect value. Therefore, we treat the tax + # rate as inclusive of tax for the calculations below, regardless of its original + # setting. + with_tax_included_in_price(tax_rate) do + tax_rate.calculator.compute line_item + end + end + + def with_tax_included_in_price(tax_rate) + old_included_in_price = tax_rate.included_in_price + + tax_rate.included_in_price = true + tax_rate.calculator.calculable.included_in_price = true + + result = yield + + tax_rate.included_in_price = old_included_in_price + tax_rate.calculator.calculable.included_in_price = old_included_in_price + + result + 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 9ad1d222b3..6703f844ab 100644 --- a/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb +++ b/spec/lib/open_food_network/enterprise_fee_applicator_spec.rb @@ -65,4 +65,33 @@ module OpenFoodNetwork efa.send(:order_adjustment_label).should == "Whole order - packing fee by distributor Ballantyne" end end + + describe "ensuring that tax rate is marked as tax included_in_price" do + let(:efa) { EnterpriseFeeApplicator.new nil, nil, nil } + let(:tax_rate) { create(:tax_rate, included_in_price: false, calculator: Spree::Calculator::DefaultTax.new) } + + it "sets included_in_price to true" do + efa.send(:with_tax_included_in_price, tax_rate) do + tax_rate.included_in_price.should be_true + end + end + + it "sets the included_in_price value accessible to the calculator to true" do + efa.send(:with_tax_included_in_price, tax_rate) do + tax_rate.calculator.calculable.included_in_price.should be_true + end + end + + it "passes through the return value of the block" do + efa.send(:with_tax_included_in_price, tax_rate) do + 'asdf' + end.should == 'asdf' + end + + it "restores both values to their original afterwards" do + efa.send(:with_tax_included_in_price, tax_rate) {} + tax_rate.included_in_price.should be_false + tax_rate.calculator.calculable.included_in_price.should be_false + end + end end diff --git a/spec/models/spree/adjustment_spec.rb b/spec/models/spree/adjustment_spec.rb index acd0fa64ac..d329db9546 100644 --- a/spec/models/spree/adjustment_spec.rb +++ b/spec/models/spree/adjustment_spec.rb @@ -79,6 +79,57 @@ module Spree end end + describe "EnterpriseFee adjustments" do + let!(:zone) { create(:zone, default_tax: true) } + let!(:zone_member) { ZoneMember.create!(zone: zone, zoneable: Country.find_by_name('Australia')) } + let(:tax_rate) { create(:tax_rate, included_in_price: true, calculator: Calculator::DefaultTax.new, zone: zone, amount: 0.1) } + let(:tax_category) { create(:tax_category, tax_rates: [tax_rate]) } + let(:tax_category_untaxed) { create(:tax_category) } + + let(:coordinator) { create(:distributor_enterprise) } + let(:enterprise_fee) { create(:enterprise_fee, enterprise: coordinator, tax_category: tax_category, calculator: Calculator::FlatRate.new(preferred_amount: 50.0)) } + let(:order_cycle) { create(:simple_order_cycle, coordinator: coordinator, coordinator_fees: [enterprise_fee], distributors: [coordinator]) } + let!(:order) { create(:order, order_cycle: order_cycle, distributor: coordinator) } + let!(:line_item) { create(:line_item, order: order) } + let(:adjustment) { order.adjustments(:reload).enterprise_fee.first } + + before do + order.update_distribution_charge! + end + + context "when enterprise fees are taxed" do + it "records the tax on the enterprise fee adjustments" do + # The fee is $50, tax is 10%, and the fee is inclusive of tax + # Therefore, the included tax should be 0.1/1.1 * 50 = $4.55 + + adjustment.included_tax.should == 4.55 + end + + context "when the tax rate does not include the tax in the price" do + before do + tax_rate.update_attribute :included_in_price, false + order.update_distribution_charge! + end + + it "treats it as inclusive anyway" do + adjustment.included_tax.should == 4.55 + end + end + end + + context "when enterprise fees have no tax" do + before do + enterprise_fee.tax_category = tax_category_untaxed + enterprise_fee.save! + order.update_distribution_charge! + end + + it "records no tax as charged" do + adjustment.included_tax.should == 0 + end + end + end + describe "setting the included tax by tax rate" do let(:adjustment) { Adjustment.new label: 'foo', amount: 50 }