Skip to content

Commit 6e8c953

Browse files
authored
Merge pull request #1892 from adammathys/tax-calculator-configuration
New configurable tax calculator interface
2 parents d3eb81c + 38004cb commit 6e8c953

16 files changed

+476
-217
lines changed

core/app/models/spree/app_configuration.rb

+22
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ def shipping_rate_taxer_class
309309
@shipping_rate_taxer_class ||= Spree::Tax::ShippingRateTaxer
310310
end
311311

312+
# Allows providing your own class for calculating taxes on a shipping rate.
313+
#
314+
# @!attribute [rw] shipping_rate_tax_calculator_class
315+
# @return [Class] a class with the same public interfaces as
316+
# Spree::TaxCalculator::ShippingRate
317+
# @api experimental
318+
attr_writer :shipping_rate_tax_calculator_class
319+
def shipping_rate_tax_calculator_class
320+
@shipping_rate_tax_calculator_class ||= Spree::TaxCalculator::ShippingRate
321+
end
322+
312323
# Allows providing your own Mailer for shipped cartons.
313324
#
314325
# @!attribute [rw] carton_shipped_email_class
@@ -365,6 +376,17 @@ def tax_adjuster_class
365376
@tax_adjuster_class ||= Spree::Tax::OrderAdjuster
366377
end
367378

379+
# Allows providing your own class for calculating taxes on an order.
380+
#
381+
# @!attribute [rw] tax_calculator_class
382+
# @return [Class] a class with the same public interfaces as
383+
# Spree::TaxCalculator::Default
384+
# @api experimental
385+
attr_writer :tax_calculator_class
386+
def tax_calculator_class
387+
@tax_calculator_class ||= Spree::TaxCalculator::Default
388+
end
389+
368390
def static_model_preferences
369391
@static_model_preferences ||= Spree::Preferences::StaticModelPreferences.new
370392
end
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
module Spree
2+
# Relatively simple class used to apply a {Spree::Tax::OrderTax} to a
3+
# {Spree::Order}.
4+
#
5+
# This class will create or update adjustments on the taxed items and remove
6+
# any now inapplicable tax adjustments from the order.
7+
class OrderTaxation
8+
# Create a new order taxation.
9+
#
10+
# @param [Spree::Order] order the order to apply taxes to
11+
# @return [Spree::OrderTaxation] a {Spree::OrderTaxation} object
12+
def initialize(order)
13+
@order = order
14+
end
15+
16+
# Apply taxes to the order.
17+
#
18+
# This method will create or update adjustments on all line items and
19+
# shipments in the order to reflect the appropriate taxes passed in. It
20+
# will also remove any now inapplicable tax adjustments.
21+
#
22+
# @param [Spree::Tax::OrderTax] taxes the taxes to apply to the order
23+
# @return [void]
24+
def apply(taxes)
25+
@order.line_items.each do |item|
26+
taxed_items = taxes.line_item_taxes.select { |i| i.item_id == item.id }
27+
update_adjustments(item, taxed_items)
28+
end
29+
30+
@order.shipments.each do |item|
31+
taxed_items = taxes.shipment_taxes.select { |i| i.item_id == item.id }
32+
update_adjustments(item, taxed_items)
33+
end
34+
end
35+
36+
private
37+
38+
# Walk through the taxes for an item and update adjustments for it. Once
39+
# all of the taxes have been added as adjustments, remove any old tax
40+
# adjustments that weren't touched.
41+
#
42+
# @private
43+
# @param [#adjustments] item a {Spree::LineItem} or {Spree::Shipment}
44+
# @param [Array<Spree::Tax::ItemTax>] taxed_items a list of calculated taxes for an item
45+
# @return [void]
46+
def update_adjustments(item, taxed_items)
47+
tax_adjustments = item.adjustments.select(&:tax?)
48+
49+
active_adjustments = taxed_items.map do |tax_item|
50+
update_adjustment(item, tax_item)
51+
end
52+
53+
# Remove any tax adjustments tied to rates which no longer match.
54+
unmatched_adjustments = tax_adjustments - active_adjustments
55+
item.adjustments.destroy(unmatched_adjustments)
56+
end
57+
58+
# Update or create a new tax adjustment on an item.
59+
#
60+
# @private
61+
# @param [#adjustments] item a {Spree::LineItem} or {Spree::Shipment}
62+
# @param [Spree::Tax::ItemTax] tax_item calculated taxes for an item
63+
# @return [Spree::Adjustment] the created or updated tax adjustment
64+
def update_adjustment(item, tax_item)
65+
tax_adjustment = item.adjustments.detect do |adjustment|
66+
adjustment.source == tax_item.tax_rate
67+
end
68+
69+
tax_adjustment ||= item.adjustments.new(
70+
source: tax_item.tax_rate,
71+
order_id: item.order_id,
72+
label: tax_item.label,
73+
included: tax_item.included_in_price
74+
)
75+
tax_adjustment.update_attributes!(amount: tax_item.amount)
76+
tax_adjustment
77+
end
78+
end
79+
end

core/app/models/spree/tax/item_adjuster.rb

-51
This file was deleted.

core/app/models/spree/tax/item_tax.rb

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
module Spree
2+
module Tax
3+
# Simple object used to hold tax data for an item.
4+
#
5+
# This generic object will hold the amount of tax that should be applied to
6+
# an item. (Either a {Spree::LineItem} or a {Spree::Shipment}.)
7+
#
8+
# @attr_reader [Integer] item_id the {Spree::LineItem} or {Spree::Shipment} ID
9+
# @attr_reader [String] label information about the taxes
10+
# @attr_reader [Spree::TaxRate] tax_rate will be used as the source for tax
11+
# adjustments
12+
# @attr_reader [BigDecimal] amount the amount of tax applied to the item
13+
# @attr_reader [Boolean] included_in_price whether the amount is included
14+
# in the items price, or additional tax.
15+
class ItemTax
16+
include ActiveModel::Model
17+
attr_accessor :item_id, :label, :tax_rate, :amount, :included_in_price
18+
end
19+
end
20+
end

core/app/models/spree/tax/order_adjuster.rb

+2-14
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ module Tax
44
class OrderAdjuster
55
attr_reader :order
66

7-
include TaxHelpers
8-
97
# @param [Spree::Order] order to be adjusted
108
def initialize(order)
119
@order = order
@@ -14,18 +12,8 @@ def initialize(order)
1412
# Creates tax adjustments for all taxable items (shipments and line items)
1513
# in the given order.
1614
def adjust!
17-
(order.line_items + order.shipments).each do |item|
18-
ItemAdjuster.new(item, order_wide_options).adjust!
19-
end
20-
end
21-
22-
private
23-
24-
def order_wide_options
25-
{
26-
rates_for_order: rates_for_order(order),
27-
rates_for_default_zone: rates_for_default_zone
28-
}
15+
taxes = Spree::Config.tax_calculator_class.new(order).calculate
16+
Spree::OrderTaxation.new(order).apply(taxes)
2917
end
3018
end
3119
end
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Spree
2+
module Tax
3+
# Simple object to pass back tax data from a calculator.
4+
#
5+
# Will be used by {Spree::OrderTaxation} to create or update tax
6+
# adjustments on an order.
7+
#
8+
# @attr_reader [Integer] order_id the {Spree::Order} these taxes apply to
9+
# @attr_reader [Array<Spree::Tax::ItemTax>] line_item_taxes an array of
10+
# tax data for order's line items
11+
# @attr_reader [Array<Spree::Tax::ItemTax>] shipment_taxes an array of
12+
# tax data for the order's shipments
13+
class OrderTax
14+
include ActiveModel::Model
15+
attr_accessor :order_id, :line_item_taxes, :shipment_taxes
16+
end
17+
end
18+
end

core/app/models/spree/tax/shipping_rate_taxer.rb

+4-13
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,21 @@ module Spree
22
module Tax
33
# Used to build shipping rate taxes
44
class ShippingRateTaxer
5-
include TaxHelpers
6-
75
# Build shipping rate taxes for a shipping rate
86
# Modifies the passed-in shipping rate with associated shipping rate taxes.
97
# @param [Spree::ShippingRate] shipping_rate The shipping rate to add taxes to.
108
# This parameter will be modified.
119
# @return [Spree::ShippingRate] The shipping rate with associated tax objects
1210
def tax(shipping_rate)
13-
tax_rates_for_shipping_rate(shipping_rate).each do |tax_rate|
11+
taxes = Spree::Config.shipping_rate_tax_calculator_class.new(shipping_rate).calculate
12+
taxes.each do |tax|
1413
shipping_rate.taxes.build(
15-
amount: tax_rate.compute_amount(shipping_rate),
16-
tax_rate: tax_rate
14+
amount: tax.amount,
15+
tax_rate: tax.tax_rate
1716
)
1817
end
1918
shipping_rate
2019
end
21-
22-
private
23-
24-
def tax_rates_for_shipping_rate(shipping_rate)
25-
applicable_rates(shipping_rate.order).select do |tax_rate|
26-
tax_rate.tax_categories.include?(shipping_rate.tax_category)
27-
end
28-
end
2920
end
3021
end
3122
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
module Spree
2+
module TaxCalculator
3+
# Default implementation for tax calculations. Will go through all line
4+
# items and shipments and calculate their tax based on tax rates in the DB.
5+
#
6+
# The class used for tax calculation is configurable, so that the
7+
# calculation can easily be pushed to third-party services. Users looking
8+
# to provide their own calculator should adhere to the API of this class.
9+
#
10+
# @api experimental
11+
# @note This API is currently in development and likely to change.
12+
# Specifically, the input format is not yet finalized.
13+
class Default
14+
include Spree::Tax::TaxHelpers
15+
16+
# Create a new tax calculator.
17+
#
18+
# @param [Spree::Order] order the order to calculator taxes on
19+
# @return [Spree::TaxCalculator::Default] a Spree::TaxCalculator::Default object
20+
def initialize(order)
21+
@order = order
22+
end
23+
24+
# Calculate taxes for an order.
25+
#
26+
# @return [Spree::Tax::OrderTax] the calculated taxes for the order
27+
def calculate
28+
Spree::Tax::OrderTax.new(
29+
order_id: order.id,
30+
line_item_taxes: line_item_rates,
31+
shipment_taxes: shipment_rates
32+
)
33+
end
34+
35+
private
36+
37+
attr_reader :order
38+
39+
# Calculate the taxes for line items.
40+
#
41+
# @private
42+
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the line items
43+
def line_item_rates
44+
order.line_items.flat_map do |line_item|
45+
calculate_rates(line_item)
46+
end
47+
end
48+
49+
# Calculate the taxes for shipments.
50+
#
51+
# @private
52+
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the shipments
53+
def shipment_rates
54+
order.shipments.flat_map do |shipment|
55+
calculate_rates(shipment)
56+
end
57+
end
58+
59+
# Calculate the taxes for a single item.
60+
#
61+
# The item could be either a {Spree::LineItem} or a {Spree::Shipment}.
62+
#
63+
# Will go through all applicable rates for an item and create a new
64+
# {Spree::Tax::ItemTax} containing the calculated taxes for the item.
65+
#
66+
# @private
67+
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the item
68+
def calculate_rates(item)
69+
rates_for_item(item).map do |rate|
70+
amount = rate.compute_amount(item)
71+
72+
Spree::Tax::ItemTax.new(
73+
item_id: item.id,
74+
label: rate.adjustment_label(amount),
75+
tax_rate: rate,
76+
amount: amount,
77+
included_in_price: rate.included_in_price
78+
)
79+
end
80+
end
81+
end
82+
end
83+
end

0 commit comments

Comments
 (0)