diff --git a/core/app/models/spree/line_item.rb b/core/app/models/spree/line_item.rb index 2b0b815c631..cc96247f22e 100644 --- a/core/app/models/spree/line_item.rb +++ b/core/app/models/spree/line_item.rb @@ -141,6 +141,7 @@ def set_pricing_attributes self.currency ||= variant.currency self.cost_price ||= variant.cost_price self.price ||= variant.price + self.initial_price ||= price end def handle_copy_price_override @@ -173,7 +174,7 @@ def recalculate_adjustments end def update_tax_charge - Spree::TaxRate.adjust(order.tax_zone, [self]) + Spree::Tax::ItemAdjuster.new(self).adjust! end def ensure_proper_currency diff --git a/core/app/models/spree/order.rb b/core/app/models/spree/order.rb index 8ee9d9b17e1..0fab7786483 100644 --- a/core/app/models/spree/order.rb +++ b/core/app/models/spree/order.rb @@ -321,10 +321,7 @@ def line_item_options_match(line_item, options) # Creates new tax charges if there are any applicable rates. If prices already # include taxes then price adjustments are created instead. def create_tax_charge! - # We want to only look up the applicable tax zone once and pass it to TaxRate calculation to avoid duplicated lookups. - order_tax_zone = tax_zone - Spree::TaxRate.adjust(order_tax_zone, line_items) - Spree::TaxRate.adjust(order_tax_zone, shipments) if shipments.any? + Spree::Tax::OrderAdjuster.new(self).adjust! end def outstanding_balance diff --git a/core/app/models/spree/tax/item_adjuster.rb b/core/app/models/spree/tax/item_adjuster.rb new file mode 100644 index 00000000000..53906ede006 --- /dev/null +++ b/core/app/models/spree/tax/item_adjuster.rb @@ -0,0 +1,51 @@ +module Spree + module Tax + class ItemAdjuster + attr_reader :item, :order + + include TaxHelpers + + def initialize(item, options = {}) + @item = item + @order = @item.order + # set caching instance variables so order-wide calculations are only done + # once per order + @rates_for_order_zone = options[:rates_for_order_zone] + @rates_for_default_zone = options[:rates_for_default_zone] + @order_tax_zone = options[:order_tax_zone] + @default_tax_zone = options[:default_tax_zone] + @outside_default_vat_zone = options[:outside_default_vat_zone] + end + + def adjust! + return unless order_tax_zone + # Using .destroy_all to make sure callbacks fire + item.adjustments.tax.destroy_all + + if outside_default_vat_zone? + adjust_item_price + end + + TaxRate.store_pre_tax_amount(@item, rates_for_item) + + rates_for_item.map { |rate| rate.adjust(order_tax_zone, item) } + end + + private + + def adjust_item_price + default_vat_amounts = default_rates_for_item.map(&:amount).sum + included_item_rate_amounts = rates_for_item.select(&:included_in_price).map(&:amount).sum + item.price = item.initial_price / (1 + default_vat_amounts) * (1 + included_item_rate_amounts) + end + + def default_rates_for_item + rates_for_default_zone.included_in_price.select { |rate| rate.applicable_for?(item) } + end + + def rates_for_item + @rates_for_item ||= rates_for_order_zone.select { |rate| rate.applicable_for?(item) } + end + end + end +end diff --git a/core/app/models/spree/tax/order_adjuster.rb b/core/app/models/spree/tax/order_adjuster.rb new file mode 100644 index 00000000000..422f7fe9455 --- /dev/null +++ b/core/app/models/spree/tax/order_adjuster.rb @@ -0,0 +1,33 @@ +module Spree + module Tax + class OrderAdjuster + attr_reader :order + + include TaxHelpers + + def initialize(order) + @order = order + end + + def adjust! + return unless order_tax_zone + + (order.line_items + order.shipments).each do |item| + ItemAdjuster.new(item, order_wide_options).adjust! + end + end + + private + + def order_wide_options + { + rates_for_order_zone: rates_for_order_zone, + rates_for_default_zone: rates_for_default_zone, + order_tax_zone: order_tax_zone, + default_tax_zone: default_tax_zone, + outside_default_vat_zone: outside_default_vat_zone? + } + end + end + end +end diff --git a/core/app/models/spree/tax/tax_helpers.rb b/core/app/models/spree/tax/tax_helpers.rb new file mode 100644 index 00000000000..1085601136f --- /dev/null +++ b/core/app/models/spree/tax/tax_helpers.rb @@ -0,0 +1,33 @@ +module Spree + module Tax + module TaxHelpers + private + + def rates_for_order_zone + @rates_for_order_zone ||= Spree::TaxRate.for_zone(order_tax_zone) + end + + def rates_for_default_zone + @rates_for_default_zone ||= Spree::TaxRate.for_zone(default_tax_zone) + end + + def order_tax_zone + @order_tax_zone ||= order.tax_zone + end + + def default_tax_zone + # Memoizing values that are potentially `false` can not use the `||=` shorthand + @default_tax_zone.nil? ? Spree::Zone.default_tax.presence : @default_tax_zone + end + + def outside_default_vat_zone? + # Memoizing booleans can not use the `||=` shorthand + if @outside_default_vat_zone.nil? + @outside_default_vat_zone = default_tax_zone && !default_tax_zone.contains?(order_tax_zone) + else + @outside_default_vat_zone + end + end + end + end +end diff --git a/core/app/models/spree/tax_rate.rb b/core/app/models/spree/tax_rate.rb index 116fbb3fefb..9c48a51697c 100644 --- a/core/app/models/spree/tax_rate.rb +++ b/core/app/models/spree/tax_rate.rb @@ -1,13 +1,3 @@ -module Spree - class DefaultTaxZoneValidator < ActiveModel::Validator - def validate(record) - if record.included_in_price - record.errors.add(:included_in_price, Spree.t(:included_price_validation)) unless Zone.default_tax - end - end - end -end - module Spree class TaxRate < Spree::Base acts_as_paranoid @@ -25,11 +15,8 @@ class TaxRate < Spree::Base validates :amount, presence: true, numericality: true validates :tax_category_id, presence: true - validates_with DefaultTaxZoneValidator - - scope :by_zone, ->(zone) { where(zone_id: zone) } - # Finds geographically matching tax rates for an order's tax zone. + # Finds geographically matching tax rates for a tax zone. # We do not know if they are/aren't applicable until we attempt to apply these rates to # the items contained within the Order itself. # For instance, if a rate passes the criteria outlined in this method, @@ -69,33 +56,11 @@ class TaxRate < Spree::Base # Under no circumstances should negative adjustments be applied for the Spanish tax rates. # # Those rates should never come into play at all and only the French rates should apply. - def self.match(order_tax_zone) - return [] unless order_tax_zone - all_rates = includes(zone: { zone_members: :zoneable }).load + scope :for_zone, ->(zone) { where(zone_id: Spree::Zone.with_shared_members(zone).pluck(:id)) } + scope :included_in_price, -> { where(included_in_price: true) } - rates_for_order_zone = all_rates.select { |rate| rate.zone.contains?(order_tax_zone) } - rates_for_default_zone = all_rates.select(&:default_vat?) - - # Imagine with me this scenario: - # You are living in Spain and you have a store which ships to France. - # Spain is therefore your default tax rate. - # When you ship to Spain, you want the Spanish rate to apply. - # When you ship to France, you want the French rate to apply. - # - # Normally, Spree would notice that you have two potentially applicable - # tax rates for one particular item. - # When you ship to Spain, only the Spanish one will apply. - # When you ship to France, you'll see a Spanish refund AND a French tax. - # This little bit of code at the end stops the Spanish refund from appearing. - # - # For further discussion, see https://github.com/spree/spree/issues/4397 and https://github.com/spree/spree/issues/4327. - - order_zone_tax_categories = rates_for_order_zone.map(&:tax_category) - rates_for_default_zone.delete_if do |default_rate| - order_zone_tax_categories.include?(default_rate.tax_category) - end - - (rates_for_order_zone + rates_for_default_zone).uniq + def applicable_for?(item) + tax_category == item.tax_category end # Pre-tax amounts must be stored so that we can calculate @@ -115,65 +80,23 @@ def self.store_pre_tax_amount(item, rates) item.update_column(:pre_tax_amount, pre_tax_amount.round(2)) end - # This method is best described by the documentation on .match - def self.adjust(order_tax_zone, items) - rates = match(order_tax_zone) - tax_categories = rates.map(&:tax_category) - relevant_items, non_relevant_items = items.partition { |item| tax_categories.include?(item.tax_category) } - unless relevant_items.empty? - Spree::Adjustment.where(adjustable: relevant_items).tax.destroy_all # using destroy_all to ensure adjustment destroy callback fires. - end - relevant_items.each do |item| - relevant_rates = rates.select { |rate| rate.tax_category == item.tax_category } - store_pre_tax_amount(item, relevant_rates) - relevant_rates.each do |rate| - rate.adjust(order_tax_zone, item) - end - end - non_relevant_items.each do |item| - if item.adjustments.tax.present? - item.adjustments.tax.destroy_all # using destroy_all to ensure adjustment destroy callback fires. - item.update_columns pre_tax_amount: 0 - end - end - end - # Creates necessary tax adjustments for the order. - def adjust(order_tax_zone, item) + def adjust(item) amount = compute_amount(item) return if amount == 0 - included = included_in_price && default_zone_or_zone_match?(order_tax_zone) - - if amount < 0 - label = Spree.t(:refund) + ' ' + create_label - end - - adjustments.create!({ + adjustments.create!( adjustable: item, amount: amount, order_id: item.order_id, - label: label || create_label, - included: included - }) + label: create_label, + included: included_in_price + ) end # 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 - end - - def default_zone_or_zone_match?(order_tax_zone) - Zone.default_tax.try!(:contains?, order_tax_zone) || zone.contains?(order_tax_zone) - end - - def default_vat? - included_in_price && zone.contains?(Spree::Zone.default_tax) + calculator.compute(item) end private diff --git a/core/app/models/spree/zone.rb b/core/app/models/spree/zone.rb index 4df41080e0c..96735ff702a 100644 --- a/core/app/models/spree/zone.rb +++ b/core/app/models/spree/zone.rb @@ -58,6 +58,8 @@ def self.match(address) # current zone, if it's a state zone. If the passed-in zone has members, it # will also be in the result set. def self.with_shared_members(zone) + return none unless zone + states_and_state_country_ids = zone.states.pluck(:id, :country_id).to_a state_ids = states_and_state_country_ids.map(&:first) state_country_ids = states_and_state_country_ids.map(&:second) diff --git a/core/db/migrate/20160126123300_add_initial_price_to_line_items.rb b/core/db/migrate/20160126123300_add_initial_price_to_line_items.rb new file mode 100644 index 00000000000..36d986ae931 --- /dev/null +++ b/core/db/migrate/20160126123300_add_initial_price_to_line_items.rb @@ -0,0 +1,5 @@ +class AddInitialPriceToLineItems < ActiveRecord::Migration + def change + add_column :spree_line_items, :initial_price, :decimal, precision: 8, scale: 2 + end +end diff --git a/core/spec/models/spree/line_item_spec.rb b/core/spec/models/spree/line_item_spec.rb index e85461c7304..c5148cec62d 100644 --- a/core/spec/models/spree/line_item_spec.rb +++ b/core/spec/models/spree/line_item_spec.rb @@ -4,6 +4,27 @@ let(:order) { create :order_with_line_items, line_items_count: 1 } let(:line_item) { order.line_items.first } + describe 'initial_price' do + it 'is available' do + expect(line_item).to respond_to(:initial_price) + end + + it 'returns a BigDecimal' do + expect(line_item.initial_price).to be_a(BigDecimal) + end + + it 'is equal to the initial price' do + expect(line_item.initial_price).to eq(line_item.price) + end + + it 'does not change even if the price is modified' do + new_item = create(:line_item, price: 15) + expect(new_item.initial_price).to eq(15) + new_item.update(price: 10) + expect(new_item.initial_price).to eq(15) + end + end + context '#destroy' do it "fetches deleted products" do line_item.product.destroy diff --git a/core/spec/models/spree/order/state_machine_spec.rb b/core/spec/models/spree/order/state_machine_spec.rb index aaf3fb76667..e7b7a9323fd 100644 --- a/core/spec/models/spree/order/state_machine_spec.rb +++ b/core/spec/models/spree/order/state_machine_spec.rb @@ -43,7 +43,7 @@ end it "adjusts tax rates when transitioning to delivery" do - expect(Spree::TaxRate).to receive(:adjust).once.with(order.tax_zone, order.line_items) + expect(Spree::Tax::OrderAdjuster).to receive(:new).once.with(order).and_call_original order.next! end end diff --git a/core/spec/models/spree/reimbursement_spec.rb b/core/spec/models/spree/reimbursement_spec.rb index b21612d209e..a7a582f56b9 100644 --- a/core/spec/models/spree/reimbursement_spec.rb +++ b/core/spec/models/spree/reimbursement_spec.rb @@ -49,7 +49,7 @@ let!(:adjustments) { [] } # placeholder to ensure it gets run prior the "before" at this level let!(:tax_rate) { nil } - let!(:tax_zone) { create :zone, default_tax: true } + let!(:tax_zone) { create :zone, :with_country, default_tax: true } let(:shipping_method) { create :shipping_method, zones: [tax_zone] } let(:variant) { create :variant } let(:order) { create(:order_with_line_items, state: 'payment', line_items_attributes: [{ variant: variant, price: line_items_price }], shipment_cost: 0, shipping_method: shipping_method) } diff --git a/core/spec/models/spree/reimbursement_tax_calculator_spec.rb b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb index 2ce2c78c5b5..c49e4ab99d4 100644 --- a/core/spec/models/spree/reimbursement_tax_calculator_spec.rb +++ b/core/spec/models/spree/reimbursement_tax_calculator_spec.rb @@ -24,7 +24,7 @@ context 'with additional tax' do let!(:tax_rate) { create(:tax_rate, name: "Sales Tax", amount: 0.10, included_in_price: false, zone: tax_zone) } - let(:tax_zone) { create(:zone, default_tax: true) } + let(:tax_zone) { create(:zone, :with_country, default_tax: true) } it 'sets additional_tax_total on the return items' do subject @@ -37,13 +37,13 @@ context 'with included tax' do let!(:tax_rate) { create(:tax_rate, name: "VAT Tax", amount: 0.1, included_in_price: true, zone: tax_zone) } - let(:tax_zone) { create(:zone, default_tax: true) } + let(:tax_zone) { create(:zone, :with_country, default_tax: true) } it 'sets included_tax_total on the return items' do subject return_item.reload - expect(return_item.included_tax_total).to be < 0 + expect(return_item.included_tax_total).to be > 0 expect(return_item.included_tax_total).to eq line_item.included_tax_total end end diff --git a/core/spec/models/spree/tax/item_adjuster_spec.rb b/core/spec/models/spree/tax/item_adjuster_spec.rb new file mode 100644 index 00000000000..bc613178e19 --- /dev/null +++ b/core/spec/models/spree/tax/item_adjuster_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +RSpec.describe Spree::Tax::ItemAdjuster do + subject(:adjuster) { described_class.new(item) } + let(:order) { Spree::Order.new } + let(:item) { Spree::LineItem.new(order: order) } + + before do + allow(order).to receive(:tax_zone) { build(:zone) } + end + + describe 'initialization' do + it 'sets order to item order' do + expect(adjuster.order).to eq(item.order) + end + + it 'sets adjustable' do + expect(adjuster.item).to eq(item) + end + end + + describe '#adjust!' do + before do + expect(order).to receive(:tax_zone).and_return(tax_zone) + end + + context 'when the order has no tax zone' do + let(:tax_zone) { nil } + + before do + allow(order).to receive(:tax_zone).and_return(nil) + adjuster.adjust! + end + + it 'returns nil early' do + expect(adjuster.adjust!).to be_nil + end + end + + context 'when the order has a tax zone' do + let(:item) { build_stubbed :line_item, order: order } + let(:tax_zone) { build_stubbed(:zone, :with_country) } + + before do + expect(item).to receive(:update_column) + + expect(Spree::TaxRate).to receive(:for_zone).with(tax_zone).and_return(rates_for_order_zone) + expect(Spree::TaxRate).to receive(:for_zone).with(Spree::Zone.default_tax).and_return([]) + end + + context 'when there are no matching rates' do + let(:rates_for_order_zone) { [] } + + it 'returns no adjustments' do + expect(adjuster.adjust!).to eq([]) + end + end + + context 'when there are matching rates for the zone' do + context 'and all rates have the same tax category as the item' do + let(:item_tax_category) { build_stubbed(:tax_category) } + let(:rate_1) { create :tax_rate, tax_category: item_tax_category } + let(:rate_2) { create :tax_rate } + let(:rates_for_order_zone) { [rate_1, rate_2] } + + before { allow(item).to receive(:tax_category).and_return(item_tax_category) } + + it 'creates an adjustment for every matching rate' do + expect(rate_1).to receive_message_chain(:adjustments, :create!) + expect(adjuster.adjust!.length).to eq(1) + end + end + end + end + end +end diff --git a/core/spec/models/spree/tax/order_adjuster_spec.rb b/core/spec/models/spree/tax/order_adjuster_spec.rb new file mode 100644 index 00000000000..378d0540bd2 --- /dev/null +++ b/core/spec/models/spree/tax/order_adjuster_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +RSpec.describe Spree::Tax::OrderAdjuster do + subject(:adjuster) { described_class.new(order) } + + describe 'initialization' do + let(:order) { Spree::Order.new } + + it 'sets order to adjustable' do + expect(adjuster.order).to eq(order) + end + end + + describe '#adjust!' do + let(:zone) { build_stubbed(:zone) } + let(:line_items) { build_stubbed_list(:line_item, 2) } + let(:order) { build_stubbed(:order, line_items: line_items) } + let(:rates_for_order_zone) { [] } + let(:item_adjuster) { Spree::Tax::ItemAdjuster.new(line_items.first) } + + before do + expect(order).to receive(:tax_zone).at_least(:once).and_return(zone) + expect(Spree::TaxRate).to receive(:for_zone).with(zone).and_return(rates_for_order_zone) + expect(Spree::TaxRate).to receive(:for_zone).with(Spree::Zone.default_tax).and_return([]) + end + + it 'calls the item adjuster with all line items' do + expect(Spree::Tax::ItemAdjuster).to receive(:new).with(line_items.first, rates_for_order_zone: rates_for_order_zone).and_return(item_adjuster) + expect(Spree::Tax::ItemAdjuster).to receive(:new).with(line_items.second, rates_for_order_zone: rates_for_order_zone).and_return(item_adjuster) + + expect(item_adjuster).to receive(:adjust!).twice + adjuster.adjust! + end + end +end diff --git a/core/spec/models/spree/tax_rate_spec.rb b/core/spec/models/spree/tax_rate_spec.rb index c65289dfe70..f5fd083251a 100644 --- a/core/spec/models/spree/tax_rate_spec.rb +++ b/core/spec/models/spree/tax_rate_spec.rb @@ -1,107 +1,68 @@ require 'spec_helper' describe Spree::TaxRate, type: :model do - context "match" do - let(:order) { create(:order) } - let(:country) { create(:country) } - let(:tax_category) { create(:tax_category) } - let(:calculator) { Spree::Calculator::FlatRate.new } - - it "should return an empty array when tax_zone is nil" do - allow(order).to receive_messages tax_zone: nil - expect(Spree::TaxRate.match(order.tax_zone)).to eq([]) + context ".for_zone" do + subject(:rates_for_zone) { Spree::TaxRate.for_zone(zone) } + + context "when zone is nil" do + let(:zone) { nil } + + it "should return an empty array" do + expect(subject).to eq([]) + end end context "when no rate zones match the tax zone" do - before do - Spree::TaxRate.create(amount: 1, zone: create(:zone)) - end + let(:rate_zone) { create(:zone, :with_country) } + let!(:rate) { create :tax_rate, zone: rate_zone } context "when there is no default tax zone" do - before do - @zone = create(:zone, name: "Country Zone", default_tax: false, zone_members: []) - @zone.zone_members.create(zoneable: country) - end + context "and the zone has no shared members with the rate zone" do + let(:zone) { create(:zone, :with_country) } - it "should return an empty array" do - allow(order).to receive_messages tax_zone: @zone - expect(Spree::TaxRate.match(order.tax_zone)).to eq([]) + it "should return an empty array" do + expect(subject).to eq([]) + end end - it "should return the rate that matches the rate zone" do - rate = Spree::TaxRate.create( - amount: 1, - zone: @zone, - tax_category: tax_category, - calculator: calculator - ) + context "and the zone has shared members with the rate zone" do + let(:zone) { create(:zone, countries: rate_zone.countries) } - allow(order).to receive_messages tax_zone: @zone - expect(Spree::TaxRate.match(order.tax_zone)).to eq([rate]) + it "should return the rate that matches the rate zone" do + expect(subject).to eq([rate]) + end end - it "should return all rates that match the rate zone" do - rate1 = Spree::TaxRate.create( - amount: 1, - zone: @zone, - tax_category: tax_category, - calculator: calculator - ) - - rate2 = Spree::TaxRate.create( - amount: 2, - zone: @zone, - tax_category: tax_category, - calculator: Spree::Calculator::FlatRate.new - ) - - allow(order).to receive_messages tax_zone: @zone - expect(Spree::TaxRate.match(order.tax_zone)).to match_array([rate1, rate2]) + context "there is many rates that match the zone" do + let!(:rate2) { create :tax_rate, zone: rate_zone } + let(:zone) { create(:zone, countries: rate_zone.countries) } + + it "should return all rates that match the rate zone" do + expect(subject).to match_array([rate, rate2]) + end end context "when the tax_zone is contained within a rate zone" do - before do - sub_zone = create(:zone, name: "State Zone", zone_members: []) - sub_zone.zone_members.create(zoneable: create(:state, country: country)) - allow(order).to receive_messages tax_zone: sub_zone - @rate = Spree::TaxRate.create( - amount: 1, - zone: @zone, - tax_category: tax_category, - calculator: calculator - ) - end + let(:country1) { create :country } + let(:country2) { create :country } + let(:rate_zone) { create(:zone, countries: [country1, country2]) } + let(:zone) { create(:zone, countries: [country1]) } it "should return the rate zone" do - expect(Spree::TaxRate.match(order.tax_zone)).to eq([@rate]) + expect(subject).to eq([rate]) end end end context "when there is a default tax zone" do - before do - @zone = create(:zone, name: "Country Zone", default_tax: true, zone_members: []) - @zone.zone_members.create(zoneable: country) - end - + let(:default_zone) { create(:zone, :with_country, default_tax: true) } let(:included_in_price) { false } let!(:rate) do - Spree::TaxRate.create(amount: 1, - zone: @zone, - tax_category: tax_category, - calculator: calculator, - included_in_price: included_in_price) + create(:tax_rate, zone: default_zone, included_in_price: included_in_price) end - subject { Spree::TaxRate.match(order.tax_zone) } - - context "when the order has the same tax zone" do - before do - allow(order).to receive_messages tax_zone: @zone - allow(order).to receive_messages tax_address: tax_address - end - - let(:tax_address) { stub_model(Spree::Address) } + context "when the zone is the default zone" do + let(:zone) { default_zone } context "when the tax is not a VAT" do it { is_expected.to eq([rate]) } @@ -109,57 +70,23 @@ context "when the tax is a VAT" do let(:included_in_price) { true } + it { is_expected.to eq([rate]) } end end - context "when the order has a different tax zone" do - before do - allow(order).to receive_messages tax_zone: create(:zone, name: "Other Zone") - allow(order).to receive_messages tax_address: tax_address - end - - context "when the order has a tax_address" do - let(:tax_address) { stub_model(Spree::Address) } - - context "when the tax is a VAT" do - let(:included_in_price) { true } - # The rate should match in this instance because: - # 1) It's the default rate (and as such, a negative adjustment should apply) - it { is_expected.to eq([rate]) } - end - - context "when the tax is not VAT" do - it "returns no tax rate" do - expect(subject).to be_empty - end - end - end - - context "when the order does not have a tax_address" do - let(:tax_address) { nil } + context "when the zone is outside the default zone" do + let(:zone) { create(:zone, :with_country) } - context "when the tax is a VAT" do - let(:included_in_price) { true } - # The rate should match in this instance because: - # 1) The order has no tax address by this stage - # 2) With no tax address, it has no tax zone - # 3) Therefore, we assume the default tax zone - # 4) This default zone has a default tax rate. - it { is_expected.to eq([rate]) } - end - - context "when the tax is not a VAT" do - it { is_expected.to be_empty } - end - end + it { is_expected.to be_empty } end end end end context ".adjust" do - let(:order) { stub_model(Spree::Order) } + let(:zone) { stub_model(Spree::Zone) } + let(:order) { stub_model(Spree::Order, tax_zone: zone) } let(:tax_category_1) { stub_model(Spree::TaxCategory) } let(:tax_category_2) { stub_model(Spree::TaxCategory) } let(:rate_1) { stub_model(Spree::TaxRate, tax_category: tax_category_1) } @@ -171,34 +98,46 @@ price: 10.0, quantity: 1, tax_category: tax_category_1, - variant: stub_model(Spree::Variant) + variant: stub_model(Spree::Variant), + order: order ) end let(:line_items) { [line_item] } before do - allow(Spree::TaxRate).to receive_messages match: [rate_1, rate_2] + allow(Spree::TaxRate).to receive(:for_zone).with(zone).and_return([rate_1, rate_2]) + allow(Spree::TaxRate).to receive(:for_zone).with(Spree::Zone.default_tax).and_return([]) + allow(order).to receive(:line_items).and_return([line_item]) end it "should only apply adjustments for matching rates" do expect(rate_1).to receive(:adjust) expect(rate_2).not_to receive(:adjust) - Spree::TaxRate.adjust(order.tax_zone, line_items) + Spree::Tax::OrderAdjuster.new(order).adjust! end end context "with shipments" do - let(:shipments) { [stub_model(Spree::Shipment, cost: 10.0, tax_category: tax_category_1)] } + let(:shipment) do + stub_model( + Spree::Shipment, + cost: 10.0, + tax_category: tax_category_1, + order: order + ) + end before do - allow(Spree::TaxRate).to receive_messages match: [rate_1, rate_2] + allow(Spree::TaxRate).to receive(:for_zone).with(zone).and_return([rate_1, rate_2]) + allow(Spree::TaxRate).to receive(:for_zone).with(Spree::Zone.default_tax).and_return([]) + allow(order).to receive(:shipments).and_return([shipment]) end it "should apply adjustments for matching rates" do expect(rate_1).to receive(:adjust) expect(rate_2).not_to receive(:adjust) - Spree::TaxRate.adjust(order.tax_zone, shipments) + Spree::Tax::OrderAdjuster.new(order).adjust! end end end @@ -265,7 +204,7 @@ before do allow(order).to receive(:tax_zone) { tax_zone } order.contents.add(variant) - Spree::TaxRate.adjust(order.tax_zone, order.line_items) + Spree::Tax::OrderAdjuster.new(order).adjust! end let(:line_item) { order.line_items.first } @@ -373,7 +312,6 @@ let(:variant) { download } it 'still has an adjusted price for romania' do - pending "waiting for the MOSS refactoring" expect(line_item.price).to eq(10.42) end @@ -382,12 +320,10 @@ end it 'has 2.02 of included tax' do - pending 'waiting for the MOSS refactoring' expect(line_item.included_tax_total).to eq(2.02) end it 'has a constant amount pre tax' do - pending 'but it changes to 8.06, because Spree thinks both VATs apply' expect(line_item.pre_tax_amount).to eq(8.40) end end @@ -401,7 +337,6 @@ let(:variant) { book } it 'should sell at the net price' do - pending "Prices have to be adjusted" expect(line_item.price).to eq(18.69) end @@ -410,7 +345,6 @@ end it 'has no tax adjustments' do - pending "Right now it gets a refund" expect(line_item.adjustments.tax.count).to eq(0) end @@ -419,7 +353,6 @@ end it 'has no additional tax' do - pending 'but there is a refund, still' expect(line_item.additional_tax_total).to eq(0) end @@ -432,12 +365,10 @@ let(:variant) { sweater } it 'should sell at the net price' do - pending 'but prices are not adjusted according to the zone yet' expect(line_item.price).to eq(25.21) end it 'has no tax adjustments' do - pending 'but it has a refund' expect(line_item.adjustments.tax.count).to eq(0) end @@ -446,7 +377,6 @@ end it 'has no additional tax' do - pending 'but it has a refund for included taxes wtf' expect(line_item.additional_tax_total).to eq(0) end @@ -459,12 +389,10 @@ let(:variant) { download } it 'should sell at the net price' do - pending 'but prices are not adjusted yet' expect(line_item.price).to eq(8.40) end it 'has no tax adjustments' do - pending 'but a refund is created' expect(line_item.adjustments.tax.count).to eq(0) end @@ -473,7 +401,6 @@ end it 'has no additional tax' do - pending 'but an tax refund that disguises as additional tax is created' expect(line_item.additional_tax_total).to eq(0) end @@ -525,7 +452,7 @@ before do allow(order).to receive(:tax_zone) { tax_zone } order.contents.add(variant) - Spree::TaxRate.adjust(order.tax_zone, order.line_items) + Spree::Tax::OrderAdjuster.new(order).adjust! end let(:line_item) { order.line_items.first } diff --git a/core/spec/models/spree/zone_spec.rb b/core/spec/models/spree/zone_spec.rb index 36b13c718f6..8cc4f0a5829 100644 --- a/core/spec/models/spree/zone_spec.rb +++ b/core/spec/models/spree/zone_spec.rb @@ -348,6 +348,14 @@ end end + context 'when passing nil' do + let!(:zone) { nil } + + it 'will return an empty set' do + expect(subject).to eq([]) + end + end + context "finding potential matches for a country zone" do let!(:zone) do create(:zone).tap do |z|