Skip to content

Commit

Permalink
Merge pull request #1892 from adammathys/tax-calculator-configuration
Browse files Browse the repository at this point in the history
New configurable tax calculator interface
  • Loading branch information
jhawthorn committed May 23, 2017
2 parents d3eb81c + 38004cb commit 6e8c953
Show file tree
Hide file tree
Showing 16 changed files with 476 additions and 217 deletions.
22 changes: 22 additions & 0 deletions core/app/models/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,17 @@ def shipping_rate_taxer_class
@shipping_rate_taxer_class ||= Spree::Tax::ShippingRateTaxer
end

# Allows providing your own class for calculating taxes on a shipping rate.
#
# @!attribute [rw] shipping_rate_tax_calculator_class
# @return [Class] a class with the same public interfaces as
# Spree::TaxCalculator::ShippingRate
# @api experimental
attr_writer :shipping_rate_tax_calculator_class
def shipping_rate_tax_calculator_class
@shipping_rate_tax_calculator_class ||= Spree::TaxCalculator::ShippingRate
end

# Allows providing your own Mailer for shipped cartons.
#
# @!attribute [rw] carton_shipped_email_class
Expand Down Expand Up @@ -365,6 +376,17 @@ def tax_adjuster_class
@tax_adjuster_class ||= Spree::Tax::OrderAdjuster
end

# Allows providing your own class for calculating taxes on an order.
#
# @!attribute [rw] tax_calculator_class
# @return [Class] a class with the same public interfaces as
# Spree::TaxCalculator::Default
# @api experimental
attr_writer :tax_calculator_class
def tax_calculator_class
@tax_calculator_class ||= Spree::TaxCalculator::Default
end

def static_model_preferences
@static_model_preferences ||= Spree::Preferences::StaticModelPreferences.new
end
Expand Down
79 changes: 79 additions & 0 deletions core/app/models/spree/order_taxation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module Spree
# Relatively simple class used to apply a {Spree::Tax::OrderTax} to a
# {Spree::Order}.
#
# This class will create or update adjustments on the taxed items and remove
# any now inapplicable tax adjustments from the order.
class OrderTaxation
# Create a new order taxation.
#
# @param [Spree::Order] order the order to apply taxes to
# @return [Spree::OrderTaxation] a {Spree::OrderTaxation} object
def initialize(order)
@order = order
end

# Apply taxes to the order.
#
# This method will create or update adjustments on all line items and
# shipments in the order to reflect the appropriate taxes passed in. It
# will also remove any now inapplicable tax adjustments.
#
# @param [Spree::Tax::OrderTax] taxes the taxes to apply to the order
# @return [void]
def apply(taxes)
@order.line_items.each do |item|
taxed_items = taxes.line_item_taxes.select { |i| i.item_id == item.id }
update_adjustments(item, taxed_items)
end

@order.shipments.each do |item|
taxed_items = taxes.shipment_taxes.select { |i| i.item_id == item.id }
update_adjustments(item, taxed_items)
end
end

private

# Walk through the taxes for an item and update adjustments for it. Once
# all of the taxes have been added as adjustments, remove any old tax
# adjustments that weren't touched.
#
# @private
# @param [#adjustments] item a {Spree::LineItem} or {Spree::Shipment}
# @param [Array<Spree::Tax::ItemTax>] taxed_items a list of calculated taxes for an item
# @return [void]
def update_adjustments(item, taxed_items)
tax_adjustments = item.adjustments.select(&:tax?)

active_adjustments = taxed_items.map do |tax_item|
update_adjustment(item, tax_item)
end

# Remove any tax adjustments tied to rates which no longer match.
unmatched_adjustments = tax_adjustments - active_adjustments
item.adjustments.destroy(unmatched_adjustments)
end

# Update or create a new tax adjustment on an item.
#
# @private
# @param [#adjustments] item a {Spree::LineItem} or {Spree::Shipment}
# @param [Spree::Tax::ItemTax] tax_item calculated taxes for an item
# @return [Spree::Adjustment] the created or updated tax adjustment
def update_adjustment(item, tax_item)
tax_adjustment = item.adjustments.detect do |adjustment|
adjustment.source == tax_item.tax_rate
end

tax_adjustment ||= item.adjustments.new(
source: tax_item.tax_rate,
order_id: item.order_id,
label: tax_item.label,
included: tax_item.included_in_price
)
tax_adjustment.update_attributes!(amount: tax_item.amount)
tax_adjustment
end
end
end
51 changes: 0 additions & 51 deletions core/app/models/spree/tax/item_adjuster.rb

This file was deleted.

20 changes: 20 additions & 0 deletions core/app/models/spree/tax/item_tax.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Spree
module Tax
# Simple object used to hold tax data for an item.
#
# This generic object will hold the amount of tax that should be applied to
# an item. (Either a {Spree::LineItem} or a {Spree::Shipment}.)
#
# @attr_reader [Integer] item_id the {Spree::LineItem} or {Spree::Shipment} ID
# @attr_reader [String] label information about the taxes
# @attr_reader [Spree::TaxRate] tax_rate will be used as the source for tax
# adjustments
# @attr_reader [BigDecimal] amount the amount of tax applied to the item
# @attr_reader [Boolean] included_in_price whether the amount is included
# in the items price, or additional tax.
class ItemTax
include ActiveModel::Model
attr_accessor :item_id, :label, :tax_rate, :amount, :included_in_price
end
end
end
16 changes: 2 additions & 14 deletions core/app/models/spree/tax/order_adjuster.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ module Tax
class OrderAdjuster
attr_reader :order

include TaxHelpers

# @param [Spree::Order] order to be adjusted
def initialize(order)
@order = order
Expand All @@ -14,18 +12,8 @@ def initialize(order)
# Creates tax adjustments for all taxable items (shipments and line items)
# in the given order.
def adjust!
(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: rates_for_order(order),
rates_for_default_zone: rates_for_default_zone
}
taxes = Spree::Config.tax_calculator_class.new(order).calculate
Spree::OrderTaxation.new(order).apply(taxes)
end
end
end
Expand Down
18 changes: 18 additions & 0 deletions core/app/models/spree/tax/order_tax.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module Spree
module Tax
# Simple object to pass back tax data from a calculator.
#
# Will be used by {Spree::OrderTaxation} to create or update tax
# adjustments on an order.
#
# @attr_reader [Integer] order_id the {Spree::Order} these taxes apply to
# @attr_reader [Array<Spree::Tax::ItemTax>] line_item_taxes an array of
# tax data for order's line items
# @attr_reader [Array<Spree::Tax::ItemTax>] shipment_taxes an array of
# tax data for the order's shipments
class OrderTax
include ActiveModel::Model
attr_accessor :order_id, :line_item_taxes, :shipment_taxes
end
end
end
17 changes: 4 additions & 13 deletions core/app/models/spree/tax/shipping_rate_taxer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,21 @@ module Spree
module Tax
# Used to build shipping rate taxes
class ShippingRateTaxer
include TaxHelpers

# Build shipping rate taxes for a shipping rate
# Modifies the passed-in shipping rate with associated shipping rate taxes.
# @param [Spree::ShippingRate] shipping_rate The shipping rate to add taxes to.
# This parameter will be modified.
# @return [Spree::ShippingRate] The shipping rate with associated tax objects
def tax(shipping_rate)
tax_rates_for_shipping_rate(shipping_rate).each do |tax_rate|
taxes = Spree::Config.shipping_rate_tax_calculator_class.new(shipping_rate).calculate
taxes.each do |tax|
shipping_rate.taxes.build(
amount: tax_rate.compute_amount(shipping_rate),
tax_rate: tax_rate
amount: tax.amount,
tax_rate: tax.tax_rate
)
end
shipping_rate
end

private

def tax_rates_for_shipping_rate(shipping_rate)
applicable_rates(shipping_rate.order).select do |tax_rate|
tax_rate.tax_categories.include?(shipping_rate.tax_category)
end
end
end
end
end
83 changes: 83 additions & 0 deletions core/app/models/spree/tax_calculator/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
module Spree
module TaxCalculator
# Default implementation for tax calculations. Will go through all line
# items and shipments and calculate their tax based on tax rates in the DB.
#
# The class used for tax calculation is configurable, so that the
# calculation can easily be pushed to third-party services. Users looking
# to provide their own calculator should adhere to the API of this class.
#
# @api experimental
# @note This API is currently in development and likely to change.
# Specifically, the input format is not yet finalized.
class Default
include Spree::Tax::TaxHelpers

# Create a new tax calculator.
#
# @param [Spree::Order] order the order to calculator taxes on
# @return [Spree::TaxCalculator::Default] a Spree::TaxCalculator::Default object
def initialize(order)
@order = order
end

# Calculate taxes for an order.
#
# @return [Spree::Tax::OrderTax] the calculated taxes for the order
def calculate
Spree::Tax::OrderTax.new(
order_id: order.id,
line_item_taxes: line_item_rates,
shipment_taxes: shipment_rates
)
end

private

attr_reader :order

# Calculate the taxes for line items.
#
# @private
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the line items
def line_item_rates
order.line_items.flat_map do |line_item|
calculate_rates(line_item)
end
end

# Calculate the taxes for shipments.
#
# @private
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the shipments
def shipment_rates
order.shipments.flat_map do |shipment|
calculate_rates(shipment)
end
end

# Calculate the taxes for a single item.
#
# The item could be either a {Spree::LineItem} or a {Spree::Shipment}.
#
# Will go through all applicable rates for an item and create a new
# {Spree::Tax::ItemTax} containing the calculated taxes for the item.
#
# @private
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the item
def calculate_rates(item)
rates_for_item(item).map do |rate|
amount = rate.compute_amount(item)

Spree::Tax::ItemTax.new(
item_id: item.id,
label: rate.adjustment_label(amount),
tax_rate: rate,
amount: amount,
included_in_price: rate.included_in_price
)
end
end
end
end
end
Loading

0 comments on commit 6e8c953

Please sign in to comment.