Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New configurable tax calculator interface #1892

Merged
merged 9 commits into from
May 23, 2017
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
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