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

[FM-1195] Support for Colorado fee by introducing order level tax and flat fee #15

Merged
merged 14 commits into from
Oct 20, 2022
Merged
14 changes: 14 additions & 0 deletions backend/app/views/spree/admin/tax_rates/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@
<%= t('spree.included_in_price') %>
</label>
</div>
<div data-hook="level" class="field">
<%= f.label :level, t('spree.tax_rate_level') %>
<%= admin_hint t('spree.tax_rate_level'), t(:tax_rate_level, scope: [:spree, :hints, "spree/tax_rate"]) %>
<ul>
<% Spree::TaxRate.levels.keys.each do |level| %>
<li>
<label>
<%= f.radio_button :level, level %>
<%= t("spree.#{level}_level") %>
</label>
</li>
<% end %>
</ul>
</div>
</div>

<div class="col-5">
Expand Down
21 changes: 21 additions & 0 deletions core/app/models/spree/calculator/flat_fee.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require_dependency 'spree/calculator'

module Spree
# Very simple tax rate calculator. Can be used to apply a flat fee to any
# type of item, including an order.
class Calculator::FlatFee < Calculator
alias_method :rate, :calculable

# Amount is fixed regardles of what it's being applied to.
def compute(_object)
rate.active? ? rate.amount : 0
end

alias_method :compute_order, :compute
alias_method :compute_shipment, :compute
alias_method :compute_line_item, :compute
alias_method :compute_shipping_rate, :compute
end
end
10 changes: 6 additions & 4 deletions core/app/models/spree/order_taxation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ def initialize(order)

# 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.
# This method will create or update adjustments on the order and 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)
update_adjustments(@order, taxes.order_taxes) if taxes.order_taxes

@order.line_items.each do |item|
taxed_items = taxes.line_item_taxes.select { |element| element.item_id == item.id }
update_adjustments(item, taxed_items)
Expand Down Expand Up @@ -70,7 +72,7 @@ def update_adjustment(item, tax_item)

tax_adjustment ||= item.adjustments.new(
source: tax_item.tax_rate,
order_id: item.order_id,
order_id: item.is_a?(Spree::Order) ? item.id : item.order_id,
label: tax_item.label,
included: tax_item.included_in_price
)
Expand Down
5 changes: 3 additions & 2 deletions core/app/models/spree/order_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ def update_adjustment_total
recalculate_adjustments

all_items = line_items + shipments
order_tax_adjustments = adjustments.select(&:eligible?).select(&:tax?)

order.adjustment_total = all_items.sum(&:adjustment_total) + adjustments.select(&:eligible?).sum(&:amount)
order.included_tax_total = all_items.sum(&:included_tax_total)
order.additional_tax_total = all_items.sum(&:additional_tax_total)
order.included_tax_total = all_items.sum(&:included_tax_total) + order_tax_adjustments.select(&:included?).sum(&:amount)
order.additional_tax_total = all_items.sum(&:additional_tax_total) + order_tax_adjustments.reject(&:included?).sum(&:amount)

order.promo_total = all_items.sum(&:promo_total) + adjustments.select(&:eligible?).select(&:promotion?).sum(&:amount)

Expand Down
5 changes: 3 additions & 2 deletions core/app/models/spree/tax/item_tax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ 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}.)
# an item. (Either a {Spree::Order}, a {Spree::LineItem} or a {Spree::Shipment}.)
#
# @attr_reader [Integer] item_id the {Spree::LineItem} or {Spree::Shipment} ID
# @attr_reader [Integer] item_id the {Spree::LineItem} or {Spree::Shipment} ID.
# Or blank if an order-level tax.
# @attr_reader [String] label information about the taxes
# @attr_reader [Spree::TaxRate] tax_rate will be used as the source for tax
# adjustments
Expand Down
4 changes: 3 additions & 1 deletion core/app/models/spree/tax/order_tax.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ module Tax
# adjustments on an order.
#
# @attr_reader [Integer] order_id the {Spree::Order} these taxes apply to
# @attr_reader [Array<Spree::Tax::ItemTax>] order_taxes an array of tax
# data for the order
# @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
attr_accessor :order_id, :order_taxes, :line_item_taxes, :shipment_taxes
end
end
end
4 changes: 2 additions & 2 deletions core/app/models/spree/tax/tax_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ module TaxHelpers
private

def rates_for_item(item)
@rates_for_order ||= Spree::TaxRate.for_address(item.order.tax_address)
@rates_for_item ||= Spree::TaxRate.item_level.for_address(item.order.tax_address)

@rates_for_order.select do |rate|
@rates_for_item.select do |rate|
rate.active? && rate.tax_categories.map(&:id).include?(item.tax_category_id)
end
end
Expand Down
31 changes: 31 additions & 0 deletions core/app/models/spree/tax_calculator/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def initialize(order)
def calculate
Spree::Tax::OrderTax.new(
order_id: order.id,
order_taxes: order_rates,
line_item_taxes: line_item_rates,
shipment_taxes: shipment_rates
)
Expand All @@ -34,6 +35,23 @@ def calculate

attr_reader :order

# Calculate the order-level taxes.
#
# @private
# @return [Array<Spree::Tax::ItemTax>] calculated taxes for the order
def order_rates
rates_for_order.map do |rate|
amount = rate.compute_amount(order)

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

# Calculate the taxes for line items.
#
# @private
Expand Down Expand Up @@ -76,6 +94,19 @@ def calculate_rates(item)
)
end
end

# @private
# @return [Array<Spree::TaxRate>] rates that apply to an order
def rates_for_order
tax_category_ids = Set[
*@order.line_items.map(&:tax_category_id),
*@order.shipments.map(&:tax_category_id)
]
rates = Spree::TaxRate.active.order_level.for_address(@order.tax_address)
rates.select do |rate|
tax_category_ids.intersect?(rate.tax_category_ids.to_set)
end
end
end
end
end
7 changes: 7 additions & 0 deletions core/app/models/spree/tax_rate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class TaxRate < Spree::Base
include Spree::CalculatedAdjustments
include Spree::AdjustmentSource

enum level: {
item: 0,
order: 1
}, _suffix: true

belongs_to :zone, class_name: "Spree::Zone", inverse_of: :tax_rates, optional: true

has_many :tax_rate_tax_categories,
Expand Down Expand Up @@ -143,6 +148,8 @@ def amount_for_adjustment_label
end

def translation_key(_amount)
return "flat_fee" if calculator.is_a?(Spree::Calculator::FlatFee)

key = included_in_price? ? "vat" : "sales_tax"
key += "_with_rate" if show_rate_in_label?
key.to_sym
Expand Down
26 changes: 13 additions & 13 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,9 @@ en:
spree/calculator/distributed_amount:
one: Distributed Amount
other: Distributed Amount
spree/calculator/flat_fee:
one: Flat Fee
other: Flat Fee
spree/calculator/flat_percent_item_total:
one: Flat Percent
other: Flat Percent
Expand Down Expand Up @@ -840,6 +843,7 @@ en:
line_item: "%{promotion} (%{promotion_name})"
order: "%{promotion} (%{promotion_name})"
tax_rates:
flat_fee: "%{name}"
sales_tax: "%{name}"
sales_tax_with_rate: "%{name} %{amount}"
vat: "%{name} (Included in Price)"
Expand Down Expand Up @@ -1402,13 +1406,9 @@ en:
hide_out_of_stock: Hide out of stock
hints:
spree/calculator:
tax_rates: This is used to calculate both sales tax (United States-style taxes)
and value-added tax (VAT). Typically this calculator should be the only tax
calculator required by your store.
shipping_methods: This is used to calculate the shipping rates on a per order or
per package rate.
promotions: This is used to determine the promotional discount to be applied to an
order, an item, or shipping charges.
promotions: This is used to determine the promotional discount to be applied to an order, an item, or shipping charges.
shipping_methods: This is used to calculate the shipping rates on a per order or per package rate.
tax_rates: The "Default Tax" calculator is used for both sales tax (United States-style taxes) and value-added tax (VAT). Typically this calculator should be the only tax calculator required by your store. "Flat Fee" can be used for any taxes that require a flat fee be charged to the customer.
spree/price:
country: 'This determines in what country the price is valid.<br/>Default:
Any Country'
Expand Down Expand Up @@ -1463,10 +1463,8 @@ en:
spree/tax_category:
is_default: 'When checked, this tax category will be selected by default when creating new products or variants.'
spree/tax_rate:
validity_period: This determines the validity period within which the tax
rate is valid and will be applied to eligible items. <br /> If no start
date value is specified, the tax rate will be immediately available. <br
/> If no expiration date value is specified, the tax rate will never expire
tax_rate_level: Item-level taxes will be applied as adjustments on line items and shipments. Order-level taxes will create an adjustment on the order. Care should be taken when chosing order-level adjustments as they aren't considered in refunds or in discounts. The default is item-level.
validity_period: This determines the validity period within which the tax rate is valid and will be applied to eligible items. <br /> If no start date value is specified, the tax rate will be immediately available. <br /> If no expiration date value is specified, the tax rate will never expire
spree/variant:
deleted: Deleted Variant
deleted_explanation: This variant was deleted on %{date}.
Expand Down Expand Up @@ -1540,6 +1538,7 @@ en:
none: No Item Selected
one: One Item Selected
custom: Items Selected
item_level: Item-level
item_total: Item Total
item_total_rule:
operators:
Expand Down Expand Up @@ -1720,6 +1719,7 @@ en:
order_details: Order Details
order_email_resent: Order Email Resent
order_information: Order Information
order_level: Order-level
order_mailer:
cancel_email:
dear_customer: Dear Customer,
Expand Down Expand Up @@ -2157,8 +2157,8 @@ en:
tax_category: Tax Category
tax_code: Tax Code
tax_included: Tax (incl.)
tax_rate_amount_explanation: Tax rates are a decimal amount to aid in calculations,
(i.e. if the tax rate is 5% then enter 0.05)
tax_rate_amount_explanation: When using the "Default Tax" calculator, the amount is treated as a decimal amount to aid in calculations. (i.e. If the tax rate is 5% then enter 0.05) If using the "Flat Fee" calculator, the amount is used for the static fee.
tax_rate_level: Tax Rate Level
tax_rates: Tax Rates
taxon: Taxon
taxon_attachment_removal_error: There was an error removing the attachment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddLevelToSpreeTaxRates < ActiveRecord::Migration[5.2]
def change
add_column :spree_tax_rates, :level, :integer, default: 0, null: false
end
end
1 change: 1 addition & 0 deletions core/lib/spree/app_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,7 @@ def environment

env.calculators.tax_rates = %w[
Spree::Calculator::DefaultTax
Spree::Calculator::FlatFee
]

env.payment_methods = %w[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
factory :default_tax_calculator, class: 'Spree::Calculator::DefaultTax' do
end

factory :flat_fee_calculator, class: 'Spree::Calculator::FlatFee' do
end

factory :shipping_calculator, class: 'Spree::Calculator::Shipping::FlatRate' do
preferred_amount { 10.0 }
end
Expand Down
28 changes: 28 additions & 0 deletions core/lib/tasks/colorado_delivery_fee.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

namespace :taxes do
desc "Creates all of the records necessary to start collecting the Colorado Delivery Fee"
task colorado_delivery_fee: :environment do
usa = Spree::Country.find_by!(iso: "US")
colorado = usa.states.find_by!(abbr: "CO")

ActiveRecord::Base.transaction do
zone = Spree::Zone.create!(
name: "Colorado",
description: "State-based zone containing only Colorado.",
states: [colorado]
)

calculator = Spree::Calculator::FlatFee.new
rate = Spree::TaxRate.create!(
name: "Colorado Delivery Fee",
calculator: calculator,
zone: zone,
amount: 0.27,
show_rate_in_label: false,
level: "order"
)
rate.tax_categories << Spree::TaxCategory.default if Spree::TaxCategory.default
end
end
end
26 changes: 26 additions & 0 deletions core/spec/models/spree/calculator/flat_fee_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require "rails_helper"
require "shared_examples/calculator_shared_examples"

RSpec.describe Spree::Calculator::FlatFee, type: :model do
let(:tax_rate) { build(:tax_rate, amount: 42) }
let(:calculator) { described_class.new(calculable: tax_rate) }

it_behaves_like "a calculator with a description"

let(:order) { build(:order) }

describe "#compute" do
subject { calculator.compute(order) }

context "when the calculator is active" do
it { is_expected.to eq 42 }
end

context "when the calculator is inactive" do
let(:tax_rate) { build(:tax_rate, expires_at: 2.days.ago) }
it { is_expected.to eq 0 }
end
end
end
Loading