Skip to content
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
115 changes: 53 additions & 62 deletions core/app/models/spree/tax_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,52 @@ class TaxRate < Spree::Base

scope :by_zone, ->(zone) { where(zone_id: zone) }

# Gets the array of TaxRates appropriate for the specified order
# Finds geographically matching tax rates for an order's 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,
# but then has a tax category that doesn't match against any of the line items
# inside of the order, then that tax rate will not be applicable to anything.
# For instance:
#
# Zones:
# - Spain (default tax zone)
# - France
#
# Tax rates: (note: amounts below do not actually reflect real VAT rates)
# 21% inclusive - "Clothing" - Spain
# 18% inclusive - "Clothing" - France
# 10% inclusive - "Food" - Spain
# 8% inclusive - "Food" - France
# 5% inclusive - "Hotels" - Spain
# 2% inclusive - "Hotels" - France
#
# Order has:
# Line Item #1 - Tax Category: Clothing
# Line Item #2 - Tax Category: Food
#
# Tax rates that should be selected:
#
# 21% inclusive - "Clothing" - Spain
# 10% inclusive - "Food" - Spain
#
# If the order's address changes to one in France, then the tax will be recalculated:
#
# 18% inclusive - "Clothing" - France
# 8% inclusive - "Food" - France
#
# Note here that the "Hotels" tax rates will not be used at all.
# This is because there are no items which have the tax category of "Hotels".
#
# 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
rates = includes(zone: { zone_members: :zoneable }).load.select do |rate|
# Why "potentially"?
# Go see the documentation for that method.
rate.potentially_applicable?(order_tax_zone)
end
all_rates = includes(zone: { zone_members: :zoneable }).load

rates_for_order_zone = all_rates.select { |rate| rate.zone.contains?(order_tax_zone) }
rates_for_default_zone = all_rates.select { |rate| rate.default_vat? }

# Imagine with me this scenario:
# You are living in Spain and you have a store which ships to France.
Expand All @@ -52,16 +90,12 @@ def self.match(order_tax_zone)
#
# For further discussion, see https://github.com/spree/spree/issues/4397 and https://github.com/spree/spree/issues/4327.

remaining_rates = rates.dup
rates.each do |rate|
if rate.included_in_price?
if remaining_rates.any?{|r| r != rate && r.tax_category == rate.tax_category }
remaining_rates.delete(rate)
end
end
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

remaining_rates
(rates_for_order_zone + rates_for_default_zone).uniq
end

# Pre-tax amounts must be stored so that we can calculate
Expand All @@ -81,7 +115,7 @@ 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 #potentially_applicable?
# This method is best described by the documentation on .match
def self.adjust(order_tax_zone, items)
rates = self.match(order_tax_zone)
tax_categories = rates.map(&:tax_category)
Expand All @@ -104,53 +138,6 @@ def self.adjust(order_tax_zone, items)
end
end

# Tax rates can *potentially* be applicable to an order.
# We do not know if they are/aren't 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,
# but then has a tax category that doesn't match against any of the line items
# inside of the order, then that tax rate will not be applicable to anything.
# For instance:
#
# Zones:
# - Spain (default tax zone)
# - France
#
# Tax rates: (note: amounts below do not actually reflect real VAT rates)
# 21% inclusive - "Clothing" - Spain
# 18% inclusive - "Clothing" - France
# 10% inclusive - "Food" - Spain
# 8% inclusive - "Food" - France
# 5% inclusive - "Hotels" - Spain
# 2% inclusive - "Hotels" - France
#
# Order has:
# Line Item #1 - Tax Category: Clothing
# Line Item #2 - Tax Category: Food
#
# Tax rates that should be selected:
#
# 21% inclusive - "Clothing" - Spain
# 10% inclusive - "Food" - Spain
#
# If the order's address changes to one in France, then the tax will be recalculated:
#
# 18% inclusive - "Clothing" - France
# 8% inclusive - "Food" - France
#
# Note here that the "Hotels" tax rates will not be used at all.
# This is because there are no items which have the tax category of "Hotels".
#
# 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 potentially_applicable?(order_tax_zone)
# If the rate's zone *contains* the order's tax zone, then it's applicable.
self.zone.contains?(order_tax_zone) ||
# The rate is a VAT and its zone contains the default zone, then it's applicable.
(self.included_in_price? && self.zone.contains?(Spree::Zone.default_tax))
end

# Creates necessary tax adjustments for the order.
def adjust(order_tax_zone, item)
amount = compute_amount(item)
Expand Down Expand Up @@ -185,6 +172,10 @@ def default_zone_or_zone_match?(order_tax_zone)
Zone.default_tax.try!(:contains?, order_tax_zone) || self.zone.contains?(order_tax_zone)
end

def default_vat?
included_in_price && zone.contains?(Spree::Zone.default_tax)
end

private

def create_label
Expand Down
7 changes: 3 additions & 4 deletions core/spec/models/spree/tax_rate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,7 @@
expect(line_item.adjustments.tax.count).to eq(1)
end

# This test fails intermittently - it's a matter of luck
xit 'has 4.79 of included tax' do
it 'has 4.79 of included tax' do
expect(line_item.included_tax_total).to eq(4.79)
end

Expand All @@ -382,8 +381,8 @@
expect(line_item.adjustments.tax.count).to eq(1)
end

# Fails intermittently - xit'ed for the time being
xit 'has 2.02 of included tax' do
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

Expand Down