diff --git a/core/app/models/spree/calculator/default_tax.rb b/core/app/models/spree/calculator/default_tax.rb index 433d859f91b..4f074ec4f86 100644 --- a/core/app/models/spree/calculator/default_tax.rb +++ b/core/app/models/spree/calculator/default_tax.rb @@ -18,7 +18,8 @@ def compute_order(order) line_items_total = matched_line_items.sum(&:discounted_amount) if rate.included_in_price - round_to_two_places(line_items_total - ( line_items_total / (1 + rate.amount) ) ) + order_tax_amount = round_to_two_places(line_items_total - ( line_items_total / (1 + rate.amount) ) ) + refund_if_necessary(order_tax_amount, order.tax_zone) else round_to_two_places(line_items_total * rate.amount) end @@ -49,7 +50,22 @@ def round_to_two_places(amount) def deduced_total_by_rate(item, rate) unrounded_net_amount = item.discounted_amount / (1 + sum_of_included_tax_rates(item)) - round_to_two_places(unrounded_net_amount * rate.amount) + refund_if_necessary( + round_to_two_places(unrounded_net_amount * rate.amount), + item.order.tax_zone + ) + end + + def refund_if_necessary(amount, order_tax_zone) + if default_zone_or_zone_match?(order_tax_zone) + amount + else + amount * -1 + end + end + + def default_zone_or_zone_match?(order_tax_zone) + Zone.default_tax.try!(:contains?, order_tax_zone) || rate.zone.contains?(order_tax_zone) end end end diff --git a/core/app/models/spree/tax_rate.rb b/core/app/models/spree/tax_rate.rb index b6ddf574816..83bc0fcf15c 100644 --- a/core/app/models/spree/tax_rate.rb +++ b/core/app/models/spree/tax_rate.rb @@ -104,20 +104,11 @@ def adjust(order_tax_zone, item) # This method is used by Adjustment#update to recalculate the cost. def compute_amount(item) - if included_in_price && !default_zone_or_zone_match?(item.order.tax_zone) - # In this case, it's a refund. - calculator.compute(item) * - 1 - else - calculator.compute(item) - end + calculator.compute(item) end private - def default_zone_or_zone_match?(order_tax_zone) - Zone.default_tax.try!(:contains?, order_tax_zone) || zone.contains?(order_tax_zone) - end - def adjustment_label(amount) Spree.t translation_key(amount), scope: "adjustment_labels.tax_rates", diff --git a/core/spec/models/spree/calculator/default_tax_spec.rb b/core/spec/models/spree/calculator/default_tax_spec.rb index 5ae8695e491..ece65d54fc8 100644 --- a/core/spec/models/spree/calculator/default_tax_spec.rb +++ b/core/spec/models/spree/calculator/default_tax_spec.rb @@ -1,30 +1,39 @@ require 'spec_helper' describe Spree::Calculator::DefaultTax, type: :model do - let!(:address) { create(:address) } - let!(:zone) { create(:zone, name: "Country Zone", default_tax: true, countries: [address.country]) } - let!(:tax_category) { create(:tax_category) } + let(:address) { create(:address) } + let!(:zone) { create(:zone, name: "Country Zone", default_tax: true, countries: [tax_rate_country]) } + let(:tax_rate_country) { address.country } + let(:tax_category) { create(:tax_category) } let!(:rate) { create(:tax_rate, tax_category: tax_category, amount: 0.05, included_in_price: included_in_price, zone: zone) } let(:included_in_price) { false } - let!(:calculator) { Spree::Calculator::DefaultTax.new(calculable: rate ) } - let!(:order) { create(:order, ship_address: address) } - let!(:line_item) { create(:line_item, price: 10, quantity: 3, tax_category: tax_category) } - let!(:shipment) { create(:shipment, cost: 15) } + subject(:calculator) { Spree::Calculator::DefaultTax.new(calculable: rate ) } context "#compute" do context "when given an order" do - let!(:line_item_1) { line_item } - let!(:line_item_2) { create(:line_item, price: 10, quantity: 3, tax_category: tax_category) } + let(:order) do + create( + :order_with_line_items, + line_items_attributes: [ + { price: 10, quantity: 3, tax_category: tax_category }.merge(line_item_one_options), + { price: 10, quantity: 3, tax_category: tax_category }.merge(line_item_two_options) + ], + ship_address: address + ) + end + let(:line_item_one_options) { {} } + let(:line_item_two_options) { {} } - before do - allow(order).to receive_messages line_items: [line_item_1, line_item_2] + context "when all items matches the rate's tax category" do + it "should be equal to the sum of the item totals * rate" do + expect(calculator.compute(order)).to eq(3) + end end context "when no line items match the tax category" do - before do - line_item_1.tax_category = nil - line_item_2.tax_category = nil - end + let(:other_tax_category) { create(:tax_category) } + let(:line_item_one_options) { { tax_category: other_tax_category } } + let(:line_item_two_options) { { tax_category: other_tax_category } } it "should be 0" do expect(calculator.compute(order)).to eq(0) @@ -32,20 +41,15 @@ end context "when one item matches the tax category" do - before do - line_item_1.tax_category = tax_category - line_item_2.tax_category = nil - end + let(:other_tax_category) { create(:tax_category) } + let(:line_item_two_options) { { tax_category: other_tax_category } } it "should be equal to the item total * rate" do expect(calculator.compute(order)).to eq(1.5) end context "correctly rounds to within two decimal places" do - before do - line_item_1.price = 10.333 - line_item_1.quantity = 1 - end + let(:line_item_one_options) { { price: 10.333, quantity: 1 } } specify do # Amount is 0.51665, which will be rounded to... @@ -54,12 +58,6 @@ end end - context "when more than one item matches the tax category" do - it "should be equal to the sum of the item totals * rate" do - expect(calculator.compute(order)).to eq(3) - end - end - context "when tax is included in price" do let(:included_in_price) { true } @@ -70,54 +68,149 @@ # 60 - 57.14 = $2.86 expect(calculator.compute(order).to_f).to eql 2.86 end + + context "when the order's tax address is outside the default VAT zone" do + let(:order_zone) { create(:zone, countries: [address.country]) } + let(:default_vat_country) { create(:country, iso: "DE") } + + before do + rate.zone.update(countries: [default_vat_country]) + # The order has to be reloaded here because of tax zone caching. + order.reload + end + + it 'creates a negative amount, indicating a VAT refund' do + expect(subject.compute(order)).to eq(-2.86) + end + end end end + end + + shared_examples_for 'computing any item' do + let(:promo_total) { 0 } + let(:order) { build_stubbed(:order, ship_address: address) } context "when tax is included in price" do let(:included_in_price) { true } + context "when the variant matches the tax category" do + it "should be equal to the item's full price * rate" do + expect(calculator.compute(item)).to eql 1.43 + end + context "when line item is discounted" do - before do - line_item.promo_total = -1 - end + let(:promo_total) { -1 } it "should be equal to the item's discounted total * rate" do - expect(calculator.compute(line_item)).to eql 1.38 + expect(calculator.compute(item)).to eql 1.38 end end - it "should be equal to the item's full price * rate" do - expect(calculator.compute(line_item)).to eql 1.43 + context "when the order's tax address is outside the default VAT zone" do + let!(:order_zone) { create(:zone, countries: [address.country]) } + let(:default_vat_country) { create(:country, iso: "DE") } + + before do + rate.zone.update(countries: [default_vat_country]) + end + + it 'creates a negative amount, indicating a VAT refund' do + expect(subject.compute(item)).to eq(-1.43) + end end end end context "when tax is not included in price" do context "when the line item is discounted" do - before { line_item.promo_total = -1 } + let(:promo_total) { -1 } it "should be equal to the item's pre-tax total * rate" do - expect(calculator.compute(line_item)).to eq(1.45) + expect(calculator.compute(item)).to eq(1.45) end end context "when the variant matches the tax category" do it "should be equal to the item pre-tax total * rate" do - expect(calculator.compute(line_item)).to eq(1.50) + expect(calculator.compute(item)).to eq(1.50) end end end + end - context "when given a shipment" do - it "should be 5% of 15" do - expect(calculator.compute(shipment)).to eq(0.75) - end + describe 'when given a line item' do + let(:item) do + build_stubbed( + :line_item, + price: 10, + quantity: 3, + promo_total: promo_total, + order: order, + tax_category: tax_category + ) + end - it "takes discounts into consideration" do - shipment.promo_total = -1 - # 5% of 14 - expect(calculator.compute(shipment)).to eq(0.7) - end + it_behaves_like 'computing any item' + end + + describe 'when given a shipment' do + let(:shipping_method) do + build_stubbed( + :shipping_method, + tax_category: tax_category + ) end + + let(:shipping_rate) do + build_stubbed( + :shipping_rate, + selected: true, + shipping_method: shipping_method + ) + end + + let(:item) do + build_stubbed( + :shipment, + cost: 30, + promo_total: promo_total, + order: order, + shipping_rates: [shipping_rate] + ) + end + + it_behaves_like 'computing any item' + end + + describe 'when given a shipping rate' do + let(:shipping_method) do + build_stubbed( + :shipping_method, + tax_category: tax_category + ) + end + + let(:shipment) do + build_stubbed( + :shipment, + order: order + ) + end + + let(:item) do + # cost and discounted_amount for shipping rates are the same as they + # can not be discounted. for the sake of passing tests, the cost is + # adjusted here. + build_stubbed( + :shipping_rate, + cost: 30 + promo_total, + selected: true, + shipping_method: shipping_method, + shipment: shipment + ) + end + + it_behaves_like 'computing any item' end end