diff options
author | Jared Norman <jared@super.gd> | 2020-01-15 15:24:22 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-01-15 15:24:22 -0800 |
commit | 48a68fcb692d83e211794ba064fd4448158a0c92 (patch) | |
tree | 01f32803ea75c3c5f9c1920ce523b0eca2bded0d | |
parent | 98677696e6f43432c05eb581dcaec292beeafaaa (diff) | |
parent | dc109637def501b965c4c59112eb625188484b6a (diff) |
Merge pull request #17 from spaghetticode/spaghetticode/tax-rate-calculator
Add tax rate calculator
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | README.md | 15 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar.rb | 7 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar/api.rb | 4 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar/api_params.rb | 7 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar/calculator_helper.rb | 44 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar/tax_calculator.rb | 35 | ||||
-rw-r--r-- | lib/super_good/solidus_taxjar/tax_rate_calculator.rb | 36 | ||||
-rw-r--r-- | spec/super_good/solidus_taxjar/api_params_spec.rb | 18 | ||||
-rw-r--r-- | spec/super_good/solidus_taxjar/api_spec.rb | 24 | ||||
-rw-r--r-- | spec/super_good/solidus_taxjar/tax_rate_calculator_spec.rb | 97 |
12 files changed, 255 insertions, 35 deletions
diff --git a/.travis.yml b/.travis.yml index c421d58..4598c2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ --- +dist: trusty sudo: false language: ruby cache: bundler diff --git a/CHANGELOG.md b/CHANGELOG.md index a83a480..581f0d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## master +- Added `SuperGood::SolidusTaxJar::TaxRateCalculator` for retrieving the tax rate for a given `Spree::Address`. The calculator follows `TaxCalculator` conventions by relying on address validators and custom exception handling. + ## v0.15.2 - Add order number to param logging. @@ -48,6 +48,21 @@ Requirements for TaxJar integrations vary as some stores also need reporting, wh If you're having trouble integrating this extension with your store and would like some assistance, please reach out to Jared via e-mail at [jared@super.gd](mailto:jared@super.gd) or on the official Solidus as `@jared`. +## Features + +The extension provides currently two high level `calculator` classes that wrap the low-level Ruby taxjar gem API calls: + +* tax calculator +* tax rate calculator + +### TaxCalculator + +`SuperGood::SolidusTaxJar::TaxCalculator` allows calculating the full tax breakdown for a given `Spree::Order`. The breakdown includes separate line items taxes and shipment taxes. + +### TaxRateCalculator + +`SuperGood::SolidusTaxJar::TaxRateCalculator` allows calculating the tax rate for a given `Spree::Address`. It relies on the same low-level Ruby TaxJar API endpoint of the tax calculator in order to provide the most coherent and reliable results. TaxJar support recommends using this endpoint for live calculations. + ## Development After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. diff --git a/lib/super_good/solidus_taxjar.rb b/lib/super_good/solidus_taxjar.rb index 9318181..1b1a4ac 100644 --- a/lib/super_good/solidus_taxjar.rb +++ b/lib/super_good/solidus_taxjar.rb @@ -5,7 +5,9 @@ require 'taxjar' require "super_good/solidus_taxjar/version" require "super_good/solidus_taxjar/api_params" require "super_good/solidus_taxjar/api" +require "super_good/solidus_taxjar/calculator_helper" require "super_good/solidus_taxjar/tax_calculator" +require "super_good/solidus_taxjar/tax_rate_calculator" require "super_good/solidus_taxjar/discount_calculator" module SuperGood @@ -25,7 +27,10 @@ module SuperGood end self.cache_duration = 3.hours - self.cache_key = ->(order) { APIParams.order_params(order).to_json } + self.cache_key = ->(record) { + record_type = record.class.name.demodulize.underscore + APIParams.send("#{record_type}_params", record).to_json + } self.discount_calculator = ::SuperGood::SolidusTaxJar::DiscountCalculator self.exception_handler = ->(e) { Rails.logger.error "An error occurred while fetching TaxJar tax rates - #{e}: #{e.message}" diff --git a/lib/super_good/solidus_taxjar/api.rb b/lib/super_good/solidus_taxjar/api.rb index 0d182ce..601a2ae 100644 --- a/lib/super_good/solidus_taxjar/api.rb +++ b/lib/super_good/solidus_taxjar/api.rb @@ -22,6 +22,10 @@ module SuperGood end end + def tax_rate_for(address) + taxjar_client.tax_for_order(APIParams.tax_rate_address_params(address)).rate + end + def tax_rates_for(address) taxjar_client.rates_for_location(*APIParams.address_params(address)) end diff --git a/lib/super_good/solidus_taxjar/api_params.rb b/lib/super_good/solidus_taxjar/api_params.rb index b8ef896..3e5bcb4 100644 --- a/lib/super_good/solidus_taxjar/api_params.rb +++ b/lib/super_good/solidus_taxjar/api_params.rb @@ -29,6 +29,13 @@ module SuperGood ] end + def tax_rate_address_params(address) + { + amount: 100, + shipping: 0, + }.merge(order_address_params(address)) + end + def transaction_params(order) {} .merge(customer_params(order)) diff --git a/lib/super_good/solidus_taxjar/calculator_helper.rb b/lib/super_good/solidus_taxjar/calculator_helper.rb new file mode 100644 index 0000000..89dedc0 --- /dev/null +++ b/lib/super_good/solidus_taxjar/calculator_helper.rb @@ -0,0 +1,44 @@ +module SuperGood + module SolidusTaxJar + module CalculatorHelper + extend ActiveSupport::Concern + + class_methods do + def default_api + ::SuperGood::SolidusTaxJar::API.new + end + end + + def incomplete_address?(address) + return true if address.is_a?(Spree::Tax::TaxLocation) + + [ + address.address1, + address.city, + address&.state&.abbr || address.state_name, + address.zipcode, + address.country.iso + ].any?(&:blank?) + end + + def taxable_address?(address) + SuperGood::SolidusTaxJar.taxable_address_check.(address) + end + + def cache + if !Rails.env.test? + Rails.cache.fetch( + cache_key, + expires_in: SuperGood::SolidusTaxJar.cache_duration + ) { yield } + else + yield + end + end + + def exception_handler + SuperGood::SolidusTaxJar.exception_handler + end + end + end +end diff --git a/lib/super_good/solidus_taxjar/tax_calculator.rb b/lib/super_good/solidus_taxjar/tax_calculator.rb index c7d44a9..7fac1bd 100644 --- a/lib/super_good/solidus_taxjar/tax_calculator.rb +++ b/lib/super_good/solidus_taxjar/tax_calculator.rb @@ -1,9 +1,7 @@ module SuperGood module SolidusTaxJar class TaxCalculator - def self.default_api - ::SuperGood::SolidusTaxJar::API.new - end + include CalculatorHelper def initialize(order, api: self.class.default_api) @order = order @@ -115,33 +113,14 @@ module SuperGood Spree::TaxRate.find_by(name: "Sales Tax") end - def cache - if !Rails.env.test? - Rails.cache.fetch( - cache_key, - expires_in: SuperGood::SolidusTaxJar.cache_duration - ) { yield } - else - yield - end - end - def cache_key SuperGood::SolidusTaxJar.cache_key.(order) end - def exception_handler - SuperGood::SolidusTaxJar.exception_handler - end - def taxable_order?(order) SuperGood::SolidusTaxJar.taxable_order_check.(order) end - def taxable_address?(address) - SuperGood::SolidusTaxJar.taxable_address_check.(address) - end - def shipping_tax_label(shipment, shipping_tax) SuperGood::SolidusTaxJar.shipping_tax_label_maker.( shipment, @@ -152,18 +131,6 @@ module SuperGood def line_item_tax_label(taxjar_line_item, spree_line_item) SuperGood::SolidusTaxJar.line_item_tax_label_maker.(taxjar_line_item, spree_line_item) end - - def incomplete_address?(tax_address) - return true if tax_address.is_a?(Spree::Tax::TaxLocation) - - [ - tax_address.address1, - tax_address.city, - tax_address&.state&.abbr || tax_address.state_name, - tax_address.zipcode, - tax_address.country.iso - ].any?(&:blank?) - end end end end diff --git a/lib/super_good/solidus_taxjar/tax_rate_calculator.rb b/lib/super_good/solidus_taxjar/tax_rate_calculator.rb new file mode 100644 index 0000000..dc3d771 --- /dev/null +++ b/lib/super_good/solidus_taxjar/tax_rate_calculator.rb @@ -0,0 +1,36 @@ +module SuperGood + module SolidusTaxJar + class TaxRateCalculator + include CalculatorHelper + def initialize(address, api: self.class.default_api) + @address = address + @api = api + end + + def calculate + return no_rate if SuperGood::SolidusTaxJar.test_mode + return no_rate if incomplete_address?(address) + return no_rate unless taxable_address?(address) + cache do + api.tax_rate_for(address).to_d + end + + rescue StandardError => e + exception_handler.(e) + no_rate + end + + private + + attr_reader :address, :api + + def no_rate + BigDecimal(0) + end + + def cache_key + SuperGood::SolidusTaxJar.cache_key.(address) + end + end + end +end diff --git a/spec/super_good/solidus_taxjar/api_params_spec.rb b/spec/super_good/solidus_taxjar/api_params_spec.rb index 3a55655..e0a8766 100644 --- a/spec/super_good/solidus_taxjar/api_params_spec.rb +++ b/spec/super_good/solidus_taxjar/api_params_spec.rb @@ -183,6 +183,24 @@ RSpec.describe SuperGood::SolidusTaxJar::APIParams do end end + describe "#tax_rate_address_params" do + subject { described_class.tax_rate_address_params(ship_address) } + + it "returns params for fetching the tax rate for that address" do + expect(subject).to eq( + { + amount: 100, + shipping: 0, + to_city: "Los Angeles", + to_country: "US", + to_state: "CA", + to_street: "475 N Beverly Dr", + to_zip: "90210" + } + ) + end + end + describe "#transaction_params" do subject { described_class.transaction_params(order) } diff --git a/spec/super_good/solidus_taxjar/api_spec.rb b/spec/super_good/solidus_taxjar/api_spec.rb index 0f890ca..8b0a5db 100644 --- a/spec/super_good/solidus_taxjar/api_spec.rb +++ b/spec/super_good/solidus_taxjar/api_spec.rb @@ -23,6 +23,30 @@ RSpec.describe SuperGood::SolidusTaxJar::API do it { is_expected.to eq({ some_kind_of: "response" }) } end + describe "tax_rate_for" do + subject { api.tax_rate_for address } + + let(:api) { described_class.new(taxjar_client: dummy_client) } + let(:dummy_client) { instance_double ::Taxjar::Client } + let(:address) { Spree::Address.new } + let(:tax_rate) { 0.04 } + let(:response) { double(rate: tax_rate) } + + before do + allow(SuperGood::SolidusTaxJar::APIParams) + .to receive(:tax_rate_address_params) + .with(address) + .and_return({ address: "params" }) + + allow(dummy_client) + .to receive(:tax_for_order) + .with({ address: "params" }) + .and_return(response) + end + + it { is_expected.to eq(tax_rate) } + end + describe "#tax_rates_for" do subject { api.tax_rates_for address } diff --git a/spec/super_good/solidus_taxjar/tax_rate_calculator_spec.rb b/spec/super_good/solidus_taxjar/tax_rate_calculator_spec.rb new file mode 100644 index 0000000..9a4e2b8 --- /dev/null +++ b/spec/super_good/solidus_taxjar/tax_rate_calculator_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +RSpec.describe ::SuperGood::SolidusTaxJar::TaxRateCalculator do + describe "#calculate" do + subject { calculator.calculate } + + let(:calculator) { described_class.new(address, api: dummy_api) } + + let(:dummy_api) do + instance_double ::SuperGood::SolidusTaxJar::API + end + + let(:dummy_tax_rate) { BigDecimal(0) } + + let(:incomplete_address) do + ::Spree::Address.new( + first_name: "Ronnie James", + zipcode: nil, + address1: nil, + city: "Beverly Hills", + state_name: "California", + country: ::Spree::Country.new(iso: "US") + ) + end + + let(:complete_address) do + incomplete_address.tap do |address| + address.zipcode = "90210" + address.address1 = "9900 Wilshire Blvd" + end + end + + shared_examples "returns the dummy tax rate" do + it { expect(subject).to eq dummy_tax_rate } + end + + context "when the address is not complete" do + let(:address) { incomplete_address } + + it_behaves_like "returns the dummy tax rate" + end + + context "when the address is complete" do + let(:address) { complete_address } + + context "when the address is not taxable" do + before do + allow(SuperGood::SolidusTaxJar.taxable_address_check) + .to receive(:call).with(address) + .and_return(false) + end + + it_behaves_like "returns the dummy tax rate" + end + + context "when the address is taxable" do + let(:tax_rate) { 0.03 } + + before do + allow(dummy_api).to receive(:tax_rate_for) { tax_rate } + end + + it "returns the expected tax rate" do + expect(subject).to eq tax_rate + end + end + end + + context "when the API encounters an error" do + let(:address) { complete_address } + + before do + allow(dummy_api).to receive(:tax_rate_for).and_raise("A bad thing happened.") + end + + it "calls the configured error handler" do + expect(SuperGood::SolidusTaxJar.exception_handler).to receive(:call) do |e| + expect(e).to be_a StandardError + expect(e.message).to eq "A bad thing happened." + end + + subject + end + + it_behaves_like "returns the dummy tax rate" + end + + context "when test_mode is set" do + let(:address) { complete_address } + + before { SuperGood::SolidusTaxJar.test_mode = true } + after { SuperGood::SolidusTaxJar.test_mode = false } + + it_behaves_like "returns the dummy tax rate" + end + end +end |