super_good-solidus_taxjar 0.15.1 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +19 -13
  3. data/.travis.yml +12 -5
  4. data/CHANGELOG.md +64 -0
  5. data/Gemfile +21 -5
  6. data/LICENSE +26 -0
  7. data/PULL_REQUEST_TEMPLATE.md +19 -0
  8. data/README.md +41 -11
  9. data/Rakefile +4 -17
  10. data/bin/rails +7 -0
  11. data/bin/rails-engine +13 -0
  12. data/bin/rails-sandbox +16 -0
  13. data/bin/rake +7 -0
  14. data/bin/sandbox +84 -0
  15. data/lib/super_good-solidus_taxjar.rb +4 -0
  16. data/lib/super_good/engine.rb +8 -0
  17. data/lib/super_good/solidus_taxjar.rb +18 -6
  18. data/lib/super_good/solidus_taxjar/addresses.rb +63 -0
  19. data/lib/super_good/solidus_taxjar/api.rb +23 -10
  20. data/lib/super_good/solidus_taxjar/api_params.rb +27 -9
  21. data/lib/super_good/solidus_taxjar/calculator_helper.rb +38 -0
  22. data/lib/super_good/solidus_taxjar/discount_calculator.rb +1 -1
  23. data/lib/super_good/solidus_taxjar/tax_calculator.rb +13 -46
  24. data/lib/super_good/solidus_taxjar/tax_rate_calculator.rb +35 -0
  25. data/lib/super_good/solidus_taxjar/version.rb +2 -2
  26. data/spec/spec_helper.rb +20 -0
  27. data/spec/super_good/solidus_taxjar/addresses_spec.rb +288 -0
  28. data/spec/super_good/solidus_taxjar/api_params_spec.rb +402 -0
  29. data/spec/super_good/solidus_taxjar/api_spec.rb +205 -0
  30. data/spec/super_good/solidus_taxjar/discount_calculator_spec.rb +13 -0
  31. data/spec/super_good/solidus_taxjar/tax_calculator_spec.rb +332 -0
  32. data/spec/super_good/solidus_taxjar/tax_rate_calculator_spec.rb +116 -0
  33. data/spec/super_good/solidus_taxjar_spec.rb +77 -0
  34. data/super_good-solidus_taxjar.gemspec +17 -14
  35. metadata +37 -10
  36. data/LICENSE.txt +0 -21
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "super_good/solidus_taxjar"
4
+ require "super_good/engine"
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SuperGoodSolidusTaxjar
4
+ class Engine < Rails::Engine
5
+ isolate_namespace Spree
6
+ engine_name 'super_good_solidus_taxjar'
7
+ end
8
+ end
@@ -1,18 +1,22 @@
1
- require 'solidus_core'
2
- require 'solidus_support'
3
- require 'taxjar'
1
+ require "solidus_core"
2
+ require "solidus_support"
3
+ require "taxjar"
4
4
 
5
5
  require "super_good/solidus_taxjar/version"
6
6
  require "super_good/solidus_taxjar/api_params"
7
7
  require "super_good/solidus_taxjar/api"
8
+ require "super_good/solidus_taxjar/calculator_helper"
8
9
  require "super_good/solidus_taxjar/tax_calculator"
10
+ require "super_good/solidus_taxjar/tax_rate_calculator"
9
11
  require "super_good/solidus_taxjar/discount_calculator"
12
+ require "super_good/solidus_taxjar/addresses"
10
13
 
11
14
  module SuperGood
12
- module SolidusTaxJar
15
+ module SolidusTaxjar
13
16
  class << self
14
17
  attr_accessor :cache_duration
15
18
  attr_accessor :cache_key
19
+ attr_accessor :custom_order_params
16
20
  attr_accessor :discount_calculator
17
21
  attr_accessor :exception_handler
18
22
  attr_accessor :line_item_tax_label_maker
@@ -22,11 +26,19 @@ module SuperGood
22
26
  attr_accessor :taxable_address_check
23
27
  attr_accessor :taxable_order_check
24
28
  attr_accessor :test_mode
29
+
30
+ def api
31
+ ::SuperGood::SolidusTaxjar::Api.new
32
+ end
25
33
  end
26
34
 
27
35
  self.cache_duration = 3.hours
28
- self.cache_key = ->(order) { APIParams.order_params(order).to_json }
29
- self.discount_calculator = ::SuperGood::SolidusTaxJar::DiscountCalculator
36
+ self.cache_key = ->(record) {
37
+ record_type = record.class.name.demodulize.underscore
38
+ ApiParams.send("#{record_type}_params", record).to_json
39
+ }
40
+ self.custom_order_params = ->(order) { {} }
41
+ self.discount_calculator = ::SuperGood::SolidusTaxjar::DiscountCalculator
30
42
  self.exception_handler = ->(e) {
31
43
  Rails.logger.error "An error occurred while fetching TaxJar tax rates - #{e}: #{e.message}"
32
44
  }
@@ -0,0 +1,63 @@
1
+ module SuperGood
2
+ module SolidusTaxjar
3
+ class Addresses
4
+ class << self
5
+ def normalize(spree_address)
6
+ new.normalize(spree_address)
7
+ end
8
+
9
+ def possibilities(spree_address)
10
+ new.possibilities(spree_address)
11
+ end
12
+ end
13
+
14
+ def initialize(api: ::SuperGood::SolidusTaxjar.api)
15
+ @api = api
16
+ end
17
+
18
+ def normalize(spree_address)
19
+ taxjar_address = taxjar_addresses(spree_address).first
20
+
21
+ return if taxjar_address.nil?
22
+
23
+ Spree::Address.immutable_merge(spree_address, {
24
+ country: us, # TaxJar only supports the US currently.
25
+ state: state(taxjar_address.state),
26
+ zipcode: taxjar_address.zip,
27
+ city: taxjar_address.city,
28
+ address1: taxjar_address.street
29
+ })
30
+ end
31
+
32
+ def possibilities(spree_address)
33
+ taxjar_addresses(spree_address).map { |taxjar_address|
34
+ Spree::Address.immutable_merge(spree_address, {
35
+ country: us, # TaxJar only supports the US currently.
36
+ state: state(taxjar_address.state),
37
+ zipcode: taxjar_address.zip,
38
+ city: taxjar_address.city,
39
+ address1: taxjar_address.street
40
+ })
41
+ }
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :api
47
+
48
+ def taxjar_addresses(spree_address)
49
+ api.validate_spree_address(spree_address)
50
+ rescue Taxjar::Error::NotFound
51
+ []
52
+ end
53
+
54
+ def us
55
+ Spree::Country.find_by iso: "US"
56
+ end
57
+
58
+ def state(abbr)
59
+ us.states.find_by_abbr abbr
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,11 +1,16 @@
1
1
  module SuperGood
2
- module SolidusTaxJar
3
- class API
2
+ module SolidusTaxjar
3
+ class Api
4
4
  def self.default_taxjar_client
5
- ::Taxjar::Client.new(
5
+ client = ::Taxjar::Client.new(
6
6
  api_key: ENV.fetch("TAXJAR_API_KEY"),
7
- api_url: ENV.fetch("TAXJAR_API_URL") { 'https://api.taxjar.com' } # Sandbox URL: https://api.sandbox.taxjar.com
7
+ api_url: ENV.fetch("TAXJAR_API_URL") { "https://api.taxjar.com" } # Sandbox URL: https://api.sandbox.taxjar.com
8
8
  )
9
+ client.set_api_config('headers', {
10
+ 'x-api-version' => '2020-08-07',
11
+ 'plugin' => 'supergoodsolidustaxjar'
12
+ })
13
+ client
9
14
  end
10
15
 
11
16
  def initialize(taxjar_client: self.class.default_taxjar_client)
@@ -13,8 +18,8 @@ module SuperGood
13
18
  end
14
19
 
15
20
  def tax_for(order)
16
- taxjar_client.tax_for_order(APIParams.order_params(order)).tap do |taxes|
17
- next unless SuperGood::SolidusTaxJar.logging_enabled
21
+ taxjar_client.tax_for_order(ApiParams.order_params(order)).tap do |taxes|
22
+ next unless SuperGood::SolidusTaxjar.logging_enabled
18
23
 
19
24
  Rails.logger.info(
20
25
  "TaxJar response for #{order.number}: #{taxes.to_h.inspect}"
@@ -22,16 +27,20 @@ module SuperGood
22
27
  end
23
28
  end
24
29
 
30
+ def tax_rate_for(address)
31
+ taxjar_client.tax_for_order(ApiParams.tax_rate_address_params(address)).rate
32
+ end
33
+
25
34
  def tax_rates_for(address)
26
- taxjar_client.rates_for_location(*APIParams.address_params(address))
35
+ taxjar_client.rates_for_location(*ApiParams.address_params(address))
27
36
  end
28
37
 
29
38
  def create_transaction_for(order)
30
- taxjar_client.create_order APIParams.transaction_params(order)
39
+ taxjar_client.create_order ApiParams.transaction_params(order)
31
40
  end
32
41
 
33
42
  def update_transaction_for(order)
34
- taxjar_client.update_order APIParams.transaction_params(order)
43
+ taxjar_client.update_order ApiParams.transaction_params(order)
35
44
  end
36
45
 
37
46
  def delete_transaction_for(order)
@@ -39,7 +48,11 @@ module SuperGood
39
48
  end
40
49
 
41
50
  def create_refund_for(reimbursement)
42
- taxjar_client.create_refund APIParams.refund_params(reimbursement)
51
+ taxjar_client.create_refund ApiParams.refund_params(reimbursement)
52
+ end
53
+
54
+ def validate_spree_address(spree_address)
55
+ taxjar_client.validate_address ApiParams.validate_address_params(spree_address)
43
56
  end
44
57
 
45
58
  private
@@ -1,18 +1,19 @@
1
1
  module SuperGood
2
- module SolidusTaxJar
3
- module APIParams
2
+ module SolidusTaxjar
3
+ module ApiParams
4
4
  class << self
5
5
  def order_params(order)
6
6
  {}
7
7
  .merge(customer_params(order))
8
8
  .merge(order_address_params(order.tax_address))
9
9
  .merge(line_items_params(order.line_items))
10
- .merge(shipping: order.shipment_total)
10
+ .merge(shipping: shipping(order))
11
+ .merge(SuperGood::SolidusTaxjar.custom_order_params.call(order))
11
12
  .tap do |params|
12
- next unless SuperGood::SolidusTaxJar.logging_enabled
13
+ next unless SuperGood::SolidusTaxjar.logging_enabled
13
14
 
14
15
  Rails.logger.info(
15
- "TaxJar params: #{params.inspect}"
16
+ "TaxJar params for #{order.number}: #{params.inspect}"
16
17
  )
17
18
  end
18
19
  end
@@ -29,6 +30,13 @@ module SuperGood
29
30
  ]
30
31
  end
31
32
 
33
+ def tax_rate_address_params(address)
34
+ {
35
+ amount: 100,
36
+ shipping: 0
37
+ }.merge(order_address_params(address))
38
+ end
39
+
32
40
  def transaction_params(order)
33
41
  {}
34
42
  .merge(customer_params(order))
@@ -58,12 +66,22 @@ module SuperGood
58
66
  )
59
67
  end
60
68
 
69
+ def validate_address_params(spree_address)
70
+ {
71
+ country: spree_address.country&.iso,
72
+ state: spree_address.state&.abbr || spree_address.state_name,
73
+ zip: spree_address.zipcode,
74
+ city: spree_address.city,
75
+ street: spree_address.address1
76
+ }
77
+ end
78
+
61
79
  private
62
80
 
63
81
  def customer_params(order)
64
82
  return {} unless order.user_id
65
83
 
66
- { customer_id: order.user_id.to_s }
84
+ {customer_id: order.user_id.to_s}
67
85
  end
68
86
 
69
87
  def order_address_params(address)
@@ -72,7 +90,7 @@ module SuperGood
72
90
  to_zip: address.zipcode,
73
91
  to_city: address.city,
74
92
  to_state: address&.state&.abbr || address.state_name,
75
- to_street: address.address1,
93
+ to_street: address.address1
76
94
  }
77
95
  end
78
96
 
@@ -115,11 +133,11 @@ module SuperGood
115
133
  end
116
134
 
117
135
  def discount(line_item)
118
- ::SuperGood::SolidusTaxJar.discount_calculator.new(line_item).discount
136
+ ::SuperGood::SolidusTaxjar.discount_calculator.new(line_item).discount
119
137
  end
120
138
 
121
139
  def shipping(order)
122
- SuperGood::SolidusTaxJar.shipping_calculator.(order)
140
+ SuperGood::SolidusTaxjar.shipping_calculator.call(order)
123
141
  end
124
142
 
125
143
  def sales_tax(order)
@@ -0,0 +1,38 @@
1
+ module SuperGood
2
+ module SolidusTaxjar
3
+ module CalculatorHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ def incomplete_address?(address)
7
+ return true if address.is_a?(Spree::Tax::TaxLocation)
8
+
9
+ [
10
+ address.address1,
11
+ address.city,
12
+ address.state&.abbr || address.state_name,
13
+ address.zipcode,
14
+ address.country&.iso
15
+ ].any?(&:blank?)
16
+ end
17
+
18
+ def taxable_address?(address)
19
+ SuperGood::SolidusTaxjar.taxable_address_check.call(address)
20
+ end
21
+
22
+ def cache
23
+ if !Rails.env.test?
24
+ Rails.cache.fetch(
25
+ cache_key,
26
+ expires_in: SuperGood::SolidusTaxjar.cache_duration
27
+ ) { yield }
28
+ else
29
+ yield
30
+ end
31
+ end
32
+
33
+ def exception_handler
34
+ SuperGood::SolidusTaxjar.exception_handler
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  module SuperGood
2
- module SolidusTaxJar
2
+ module SolidusTaxjar
3
3
  class DiscountCalculator
4
4
  def initialize(line_item)
5
5
  @line_item = line_item
@@ -1,17 +1,15 @@
1
1
  module SuperGood
2
- module SolidusTaxJar
2
+ module SolidusTaxjar
3
3
  class TaxCalculator
4
- def self.default_api
5
- ::SuperGood::SolidusTaxJar::API.new
6
- end
4
+ include CalculatorHelper
7
5
 
8
- def initialize(order, api: self.class.default_api)
6
+ def initialize(order, api: SuperGood::SolidusTaxjar.api)
9
7
  @order = order
10
8
  @api = api
11
9
  end
12
10
 
13
11
  def calculate
14
- return no_tax if SuperGood::SolidusTaxJar.test_mode
12
+ return no_tax if SuperGood::SolidusTaxjar.test_mode
15
13
  return no_tax if incomplete_address?(order.tax_address) || order.line_items.none?
16
14
  return no_tax unless taxable_order? order
17
15
  return no_tax unless taxable_address? order.tax_address
@@ -25,8 +23,8 @@ module SuperGood
25
23
  shipment_taxes: shipment_taxes
26
24
  )
27
25
  end
28
- rescue StandardError => e
29
- exception_handler.(e)
26
+ rescue => e
27
+ exception_handler.call(e)
30
28
  no_tax
31
29
  end
32
30
 
@@ -36,7 +34,7 @@ module SuperGood
36
34
 
37
35
  def line_item_taxes
38
36
  @line_item_taxes ||=
39
- taxjar_breakdown.line_items.map do |taxjar_line_item|
37
+ taxjar_breakdown.line_items.map { |taxjar_line_item|
40
38
  spree_line_item_id = taxjar_line_item.id.to_i
41
39
 
42
40
  # Searching in memory because this association is loaded and most
@@ -50,13 +48,13 @@ module SuperGood
50
48
  amount: taxjar_line_item.tax_collectable,
51
49
  included_in_price: false
52
50
  )
53
- end
51
+ }
54
52
  end
55
53
 
56
54
  def shipment_taxes
57
55
  @shipment_taxes ||=
58
56
  if taxjar_breakdown.shipping? &&
59
- (total_shipping_tax = taxjar_breakdown.shipping.tax_collectable) != 0
57
+ (total_shipping_tax = taxjar_breakdown.shipping.tax_collectable) != 0
60
58
 
61
59
  # Distribute shipping tax across shipments:
62
60
  # TaxJar does not provide a breakdown of shipping taxes, so we have
@@ -115,54 +113,23 @@ module SuperGood
115
113
  Spree::TaxRate.find_by(name: "Sales Tax")
116
114
  end
117
115
 
118
- def cache
119
- if !Rails.env.test?
120
- Rails.cache.fetch(
121
- cache_key,
122
- expires_in: SuperGood::SolidusTaxJar.cache_duration
123
- ) { yield }
124
- else
125
- yield
126
- end
127
- end
128
-
129
116
  def cache_key
130
- SuperGood::SolidusTaxJar.cache_key.(order)
131
- end
132
-
133
- def exception_handler
134
- SuperGood::SolidusTaxJar.exception_handler
117
+ SuperGood::SolidusTaxjar.cache_key.call(order)
135
118
  end
136
119
 
137
120
  def taxable_order?(order)
138
- SuperGood::SolidusTaxJar.taxable_order_check.(order)
139
- end
140
-
141
- def taxable_address?(address)
142
- SuperGood::SolidusTaxJar.taxable_address_check.(address)
121
+ SuperGood::SolidusTaxjar.taxable_order_check.call(order)
143
122
  end
144
123
 
145
124
  def shipping_tax_label(shipment, shipping_tax)
146
- SuperGood::SolidusTaxJar.shipping_tax_label_maker.(
125
+ SuperGood::SolidusTaxjar.shipping_tax_label_maker.call(
147
126
  shipment,
148
127
  shipping_tax
149
128
  )
150
129
  end
151
130
 
152
131
  def line_item_tax_label(taxjar_line_item, spree_line_item)
153
- SuperGood::SolidusTaxJar.line_item_tax_label_maker.(taxjar_line_item, spree_line_item)
154
- end
155
-
156
- def incomplete_address?(tax_address)
157
- return true if tax_address.is_a?(Spree::Tax::TaxLocation)
158
-
159
- [
160
- tax_address.address1,
161
- tax_address.city,
162
- tax_address&.state&.abbr || tax_address.state_name,
163
- tax_address.zipcode,
164
- tax_address.country.iso
165
- ].any?(&:blank?)
132
+ SuperGood::SolidusTaxjar.line_item_tax_label_maker.call(taxjar_line_item, spree_line_item)
166
133
  end
167
134
  end
168
135
  end
@@ -0,0 +1,35 @@
1
+ module SuperGood
2
+ module SolidusTaxjar
3
+ class TaxRateCalculator
4
+ include CalculatorHelper
5
+ def initialize(address, api: SuperGood::SolidusTaxjar.api)
6
+ @address = address
7
+ @api = api
8
+ end
9
+
10
+ def calculate
11
+ return no_rate if SuperGood::SolidusTaxjar.test_mode
12
+ return no_rate if incomplete_address?(address)
13
+ return no_rate unless taxable_address?(address)
14
+ cache do
15
+ api.tax_rate_for(address).to_d
16
+ end
17
+ rescue => e
18
+ exception_handler.call(e)
19
+ no_rate
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :address, :api
25
+
26
+ def no_rate
27
+ BigDecimal(0)
28
+ end
29
+
30
+ def cache_key
31
+ SuperGood::SolidusTaxjar.cache_key.call(address)
32
+ end
33
+ end
34
+ end
35
+ end