workarea-avatax 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +20 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.github/workflows/ci.yml +66 -0
  7. data/.gitignore +18 -0
  8. data/.rubocop.yml +8 -0
  9. data/CHANGELOG.md +123 -0
  10. data/CODE_OF_CONDUCT.md +3 -0
  11. data/CONTRIBUTING.md +3 -0
  12. data/Gemfile +10 -0
  13. data/LICENSE.md +3 -0
  14. data/README.md +57 -0
  15. data/Rakefile +50 -0
  16. data/app/models/workarea/avatax/usage_type.rb +17 -0
  17. data/app/models/workarea/pricing/calculators/avalara_tax_calculator.rb +105 -0
  18. data/app/models/workarea/user.decorator +7 -0
  19. data/app/seeds/workarea/avatax_seeds.rb +44 -0
  20. data/app/view_models/workarea/admin/user_view_model.decorator +21 -0
  21. data/app/view_models/workarea/storefront/cart_view_model.decorator +7 -0
  22. data/app/views/workarea/admin/users/_avatax_properties_fields.html.haml +5 -0
  23. data/app/workers/workarea/avatax/tax_invoice_worker.rb +29 -0
  24. data/bin/rails +17 -0
  25. data/config/initializers/appends.rb +4 -0
  26. data/config/initializers/avatax.rb +14 -0
  27. data/config/initializers/seeds.rb +1 -0
  28. data/config/locales/en.yml +6 -0
  29. data/lib/avatax/api.decorator +52 -0
  30. data/lib/workarea/avatax/bogus_gateway/bogus_create_transaction.rb +187 -0
  31. data/lib/workarea/avatax/bogus_gateway.rb +34 -0
  32. data/lib/workarea/avatax/engine.rb +36 -0
  33. data/lib/workarea/avatax/tax_request/addresses.rb +35 -0
  34. data/lib/workarea/avatax/tax_request/line_item.rb +47 -0
  35. data/lib/workarea/avatax/tax_request/order_line_item.rb +64 -0
  36. data/lib/workarea/avatax/tax_request/order_lines.rb +70 -0
  37. data/lib/workarea/avatax/tax_request/response.rb +49 -0
  38. data/lib/workarea/avatax/tax_request/shipping_line_item.rb +34 -0
  39. data/lib/workarea/avatax/tax_request.rb +77 -0
  40. data/lib/workarea/avatax/version.rb +5 -0
  41. data/lib/workarea/avatax.rb +71 -0
  42. data/test/dummy/Rakefile +6 -0
  43. data/test/dummy/bin/bundle +3 -0
  44. data/test/dummy/bin/rails +4 -0
  45. data/test/dummy/bin/rake +4 -0
  46. data/test/dummy/bin/setup +34 -0
  47. data/test/dummy/bin/update +29 -0
  48. data/test/dummy/config/application.rb +23 -0
  49. data/test/dummy/config/boot.rb +5 -0
  50. data/test/dummy/config/cable.yml +9 -0
  51. data/test/dummy/config/environment.rb +5 -0
  52. data/test/dummy/config/environments/development.rb +54 -0
  53. data/test/dummy/config/environments/production.rb +86 -0
  54. data/test/dummy/config/environments/test.rb +44 -0
  55. data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
  56. data/test/dummy/config/initializers/assets.rb +11 -0
  57. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  58. data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
  59. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  60. data/test/dummy/config/initializers/inflections.rb +16 -0
  61. data/test/dummy/config/initializers/mime_types.rb +4 -0
  62. data/test/dummy/config/initializers/new_framework_defaults.rb +23 -0
  63. data/test/dummy/config/initializers/workarea.rb +5 -0
  64. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  65. data/test/dummy/config/locales/en.yml +23 -0
  66. data/test/dummy/config/puma.rb +47 -0
  67. data/test/dummy/config/routes.rb +5 -0
  68. data/test/dummy/config/secrets.yml +22 -0
  69. data/test/dummy/config/spring.rb +6 -0
  70. data/test/dummy/config.ru +5 -0
  71. data/test/dummy/db/seeds.rb +2 -0
  72. data/test/dummy/lib/assets/.keep +0 -0
  73. data/test/dummy/log/.keep +0 -0
  74. data/test/dummy/public/404.html +67 -0
  75. data/test/dummy/public/422.html +67 -0
  76. data/test/dummy/public/500.html +66 -0
  77. data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
  78. data/test/dummy/public/apple-touch-icon.png +0 -0
  79. data/test/dummy/public/favicon.ico +0 -0
  80. data/test/factories/workarea/avatax_factories.rb +142 -0
  81. data/test/lib/workarea/avatax/tax_request/order_line_items_test.rb +114 -0
  82. data/test/lib/workarea/avatax/tax_request/response_test.rb +42 -0
  83. data/test/lib/workarea/avatax/tax_request_test.rb +51 -0
  84. data/test/lib/workarea/avatax_test.rb +28 -0
  85. data/test/models/workarea/avatax/usage_type_test.rb +13 -0
  86. data/test/models/workarea/pricing/calculators/avalara_tax_calculator_test.rb +113 -0
  87. data/test/support/avatax_test_settings.rb +9 -0
  88. data/test/system/workarea/storefront/avatax_system_test.rb +22 -0
  89. data/test/test_helper.rb +20 -0
  90. data/test/vcr_cassettes/succesful_avatax_create_transaction.yml +60 -0
  91. data/test/vcr_cassettes/succesful_avatax_create_transaction_multiple_shippings.yml +76 -0
  92. data/workarea-avatax.gemspec +20 -0
  93. metadata +176 -0
@@ -0,0 +1,29 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxInvoiceWorker
4
+ include Sidekiq::Worker
5
+ include Sidekiq::CallbacksWorker
6
+
7
+ sidekiq_options(
8
+ enqueue_on: {
9
+ Workarea::Order => :place,
10
+ ignore_if: -> { Avatax.config.order_handling == :none }
11
+ }
12
+ )
13
+
14
+ def perform(order_id)
15
+ order = Workarea::Order.find(order_id)
16
+ shippings = Workarea::Shipping.where(order_id: order.id).to_a
17
+
18
+ response = Avatax::TaxRequest.new(
19
+ order: order,
20
+ shippings: shippings,
21
+ type: "SalesInvoice",
22
+ commit: Avatax.commit?
23
+ ).response
24
+
25
+ raise "Failed to invoice tax for order: #{order.id}" unless response.success?
26
+ end
27
+ end
28
+ end
29
+ end
data/bin/rails ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
3
+
4
+ ENGINE_ROOT = File.expand_path("../..", __FILE__)
5
+ ENGINE_PATH = File.expand_path("../../lib/workarea/avatax/engine", __FILE__)
6
+
7
+ # Set up gems listed in the Gemfile.
8
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
9
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
10
+
11
+ require "action_controller/railtie"
12
+ require "action_view/railtie"
13
+ require "action_mailer/railtie"
14
+ require "rails/test_unit/railtie"
15
+ require "sprockets/railtie"
16
+
17
+ require "rails/engine/commands"
@@ -0,0 +1,4 @@
1
+ Workarea.append_partials(
2
+ "admin.user_properties",
3
+ "workarea/admin/users/avatax_properties_fields"
4
+ )
@@ -0,0 +1,14 @@
1
+ Workarea.config.pricing_calculators.swap(
2
+ "Workarea::Pricing::Calculators::TaxCalculator",
3
+ "Workarea::Pricing::Calculators::AvalaraTaxCalculator"
4
+ )
5
+
6
+ if Rails.application.secrets.avatax.present?
7
+ avatax_sercrets = Rails.application.secrets.avatax.deep_symbolize_keys
8
+ AvaTax.configure do |config|
9
+ config.username = avatax_sercrets[:username]
10
+ config.password = avatax_sercrets[:password]
11
+ end
12
+ end
13
+
14
+ Workarea::Avatax.auto_configure_gateway
@@ -0,0 +1 @@
1
+ Workarea.config.seeds.append("Workarea::AvataxSeeds")
@@ -0,0 +1,6 @@
1
+ en:
2
+ workarea:
3
+ admin:
4
+ users:
5
+ edit:
6
+ avatax_entity_use_code: AvaTax Entity/Use Code
@@ -0,0 +1,52 @@
1
+ module AvaTax
2
+ decorate API, with: :workarea_avatax do
3
+ def request(method, path, model, options = {})
4
+ timeout = options.delete(:timeout)
5
+
6
+ connection.send(method) do |request|
7
+ request.options.timeout = timeout if timeout
8
+
9
+ case method
10
+ when :get, :delete
11
+ request.url("#{URI.encode(path)}?#{URI.encode_www_form(options)}")
12
+ when :post, :put
13
+ request.path = ("#{URI.encode(path)}?#{URI.encode_www_form(options)}")
14
+ request.headers["Content-Type"] = "application/json"
15
+ request.body = model.to_json unless model.empty?
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ # NOTE: Remove this decoration when
23
+ # https://github.com/avadev/AvaTax-REST-V2-Ruby-SDK/issues/16 is
24
+ # resolved and merged in.
25
+ def connection
26
+ client_id = "#{app_name};#{app_version};RubySdk;#{AvaTax::VERSION.dup};#{machine_name}"
27
+ options = {
28
+ :headers =>
29
+ {
30
+ 'Accept' => "application/json; charset=utf-8",
31
+ 'User-Agent' => user_agent,
32
+ 'X-Avalara-Client' => client_id
33
+ },
34
+ :url => endpoint,
35
+ :proxy => proxy,
36
+ }.merge(connection_options)
37
+
38
+ Faraday.new(options) do |faraday|
39
+ faraday.response :oj, content_type: /\bjson$/
40
+ faraday.basic_auth(username, password)
41
+
42
+ if logger
43
+ faraday.response :logger do |logger|
44
+ logger.filter(/(Authorization\:\ \"Basic\ )(\w+)\=/, '\1[REMOVED]')
45
+ end
46
+ end
47
+
48
+ faraday.adapter Faraday.default_adapter
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,187 @@
1
+ module Workarea
2
+ module Avatax
3
+ class BogusGateway::BogusCreateTransaction < BogusGateway::BogusResponse
4
+ attr_reader :request_body
5
+
6
+ def initialize(request_body)
7
+ @request_body = request_body
8
+ end
9
+
10
+ def response
11
+ env = Faraday::Env.new(
12
+ :post,
13
+ response_body,
14
+ URI("https://sandbox-rest.avatax.com/api/v2/transactions/create"),
15
+ Faraday::RequestOptions.new,
16
+ request_headers,
17
+ Faraday::SSLOptions.new(true),
18
+ nil,
19
+ nil,
20
+ nil,
21
+ response_headers,
22
+ 201,
23
+ "Created"
24
+ )
25
+ Faraday::Response.new(env)
26
+ end
27
+
28
+ private
29
+
30
+ def response_body
31
+ {
32
+ "id" => 0,
33
+ "code" => "0B41518F49",
34
+ "companyId" => 0,
35
+ "status" => "Temporary",
36
+ "type" => "SalesOrder",
37
+ "totalTax" => lines.sum { |line| line["tax"] },
38
+ "lines" => lines
39
+ }
40
+ end
41
+
42
+ def lines
43
+ @lines ||= request_body[:lines].map.with_index(1) do |line, number|
44
+ if Rails.env.test?
45
+ test_like_line(line, number)
46
+ else
47
+ api_like_line(line, number)
48
+ end
49
+ end.compact
50
+ end
51
+
52
+ def api_like_line(line, number)
53
+ {
54
+ "id" => 0,
55
+ "transactionId" => 0,
56
+ "lineNumber" => number.to_s,
57
+ "description" => "",
58
+ "discountAmount" => 0.0,
59
+ "exemptAmount" => 0.0,
60
+ "exemptCertId" => 0,
61
+ "isItemTaxable" => true,
62
+ "itemCode" => "",
63
+ "lineAmount" => line[:amount],
64
+ "quantity" => 0.0,
65
+ "reportingDate" => "2017-06-08",
66
+ "tax" => (line[:amount].to_m * 0.08).to_s,
67
+ "taxableAmount" => line[:amount],
68
+ "taxCalculated" => (line[:amount].to_m * 0.08).to_s,
69
+ "taxCode" => "P0000000",
70
+ "taxDate" => "2017-06-08",
71
+ "taxIncluded" => false,
72
+ "details" => [
73
+ {
74
+ "id" => 0,
75
+ "transactionLineId" => 0,
76
+ "transactionId" => 0,
77
+ "country" => "US",
78
+ "region" => "PA",
79
+ "exemptAmount" => 0.0,
80
+ "jurisCode" => "42",
81
+ "jurisName" => "PENNSYLVANIA",
82
+ "stateAssignedNo" => "",
83
+ "jurisType" => "STA",
84
+ "nonTaxableAmount" => 0.0,
85
+ "rate" => 0.06,
86
+ "tax" => (line[:amount].to_m * 0.06).to_s,
87
+ "taxableAmount" => line[:amount],
88
+ "taxType" => "Sales",
89
+ "taxName" => "PA STATE TAX",
90
+ "taxAuthorityTypeId" => 45,
91
+ "taxCalculated" => (line[:amount].to_m * 0.06).to_s,
92
+ "rateType" => "General",
93
+ "rateTypeCode" => "G"
94
+ },
95
+ {
96
+ "id" => 0,
97
+ "transactionLineId" => 0,
98
+ "transactionId" => 0,
99
+ "country" => "US",
100
+ "region" => "PA",
101
+ "exemptAmount" => 0.0,
102
+ "jurisCode" => "101",
103
+ "jurisName" => "PHILADELPHIA",
104
+ "stateAssignedNo" => "51",
105
+ "jurisType" => "CTY",
106
+ "nonTaxableAmount" => 0.0,
107
+ "rate" => 0.02,
108
+ "tax" => (line[:amount].to_m * 0.02).to_s,
109
+ "taxableAmount" => line[:amount],
110
+ "taxType" => "Sales",
111
+ "taxName" => "PA COUNTY TAX",
112
+ "taxAuthorityTypeId" => 45,
113
+ "taxCalculated" => (line[:amount].to_m * 0.02).to_s,
114
+ "rateType" => "General",
115
+ "rateTypeCode" => "G"
116
+ }
117
+ ]
118
+ }
119
+ end
120
+
121
+ def test_like_line(line, number)
122
+ request_address = request_body[:addresses][:shipTo] ||
123
+ request_body[:addresses][:singleLocation]
124
+
125
+ address =
126
+ if request_address.blank?
127
+ Workarea::Address.new
128
+ else
129
+ # need to underscore keys and duck a workarea::address
130
+ Hashie::Mash.new(Hash[request_address.map { |k, v| [k.to_s.underscore, v] }])
131
+ end
132
+
133
+ tax_rate = Workarea::Tax.find_rate(
134
+ line[:taxCode],
135
+ line[:amount].to_m,
136
+ address
137
+ )
138
+
139
+ return unless tax_rate.present?
140
+ {
141
+ "id" => 0,
142
+ "transactionId" => 0,
143
+ "lineNumber" => number.to_s,
144
+ "description" => "",
145
+ "discountAmount" => 0.0,
146
+ "exemptAmount" => 0.0,
147
+ "exemptCertId" => 0,
148
+ "isItemTaxable" => true,
149
+ "itemCode" => "",
150
+ "lineAmount" => line[:amount],
151
+ "quantity" => 0.0,
152
+ "reportingDate" => "2017-06-08",
153
+ "tax" => (line[:amount].to_m * tax_rate.percentage).to_s,
154
+ "taxableAmount" => line[:amount],
155
+ "taxCalculated" => (line[:amount].to_m * tax_rate.percentage).to_s,
156
+ "taxCode" => "P0000000",
157
+ "taxDate" => "2017-06-08",
158
+ "taxIncluded" => false,
159
+ "details" => [
160
+ {
161
+ "id" => 0,
162
+ "transactionLineId" => 0,
163
+ "transactionId" => 0,
164
+ "country" => "US",
165
+ "region" => "PA",
166
+ "exemptAmount" => 0.0,
167
+ "jurisCode" => "42",
168
+ "jurisName" => "PENNSYLVANIA",
169
+ "stateAssignedNo" => "",
170
+ "jurisType" => "STA",
171
+ "nonTaxableAmount" => 0.0,
172
+ "rate" => tax_rate.percentage,
173
+ "tax" => (line[:amount].to_m * tax_rate.percentage).to_s,
174
+ "taxableAmount" => line[:amount],
175
+ "taxType" => "Sales",
176
+ "taxName" => "PA STATE TAX",
177
+ "taxAuthorityTypeId" => 45,
178
+ "taxCalculated" => (line[:amount].to_m * tax_rate.percentage).to_s,
179
+ "rateType" => "General",
180
+ "rateTypeCode" => "G"
181
+ }
182
+ ]
183
+ }
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,34 @@
1
+ module Workarea
2
+ module Avatax
3
+ class BogusGateway
4
+ class BogusResponse
5
+ private
6
+
7
+ def request_headers
8
+ {
9
+ "Accept" => "application/json; charset=utf-8",
10
+ "User-Agent" => "AvaTax Ruby Gem 17.5.0",
11
+ "Content-Type" => "application/json",
12
+ "Authorization" => "Basic ZXBpZ2VvbkB3ZWJsaW5jLmNvbTo2NDhCMEE5ODUx"
13
+ }
14
+ end
15
+
16
+ def response_headers
17
+ {
18
+ "transfer-encoding" => "chunked",
19
+ "content-type" => "application/json; charset=utf-8",
20
+ "server" => "Kestrel",
21
+ "serverduration" => "00:00:00.0937524",
22
+ "databasecalls" => "4",
23
+ "databaseduration" => "00:00:00.0156261",
24
+ "date" => "Thu, 08 Jun 2017 19:04:38 GMT"
25
+ }
26
+ end
27
+ end
28
+
29
+ def create_transaction(body, options = {})
30
+ BogusCreateTransaction.new(body).response
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,36 @@
1
+ module Workarea
2
+ module Avatax
3
+ class Engine < ::Rails::Engine
4
+ include Workarea::Plugin
5
+
6
+ isolate_namespace Workarea::Avatax
7
+
8
+ #Workarea::Avatax.configure do |config|
9
+ # config.valid_service_urls = [
10
+ # "https://development.avalara.net", # development
11
+ # "https://avatax.avalara.net" # production
12
+ # ]
13
+ #end
14
+
15
+ initializer "workarea.avatax.templates" do
16
+ #Plugin.append_partials(
17
+ # "admin.store_menu",
18
+ # "workarea/admin/menus/avatax_settings"
19
+ #)
20
+
21
+ #Plugin.append_partials(
22
+ # "admin.user_permissions",
23
+ # "workarea/admin/users/avatax_settings"
24
+ #)
25
+ #Plugin.append_partials(
26
+ # "admin.user_properties",
27
+ # "workarea/admin/users/user_properties_fields"
28
+ #)
29
+ end
30
+
31
+ #initializer "workarea.avatax.listeners" do
32
+ # Workarea::Publisher.add_listener(Workarea::Avatax::InvoiceListener)
33
+ #end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxRequest::Addresses
4
+ attr_reader :tax_request
5
+
6
+ def initialize(tax_request:)
7
+ @tax_request = tax_request
8
+ end
9
+
10
+ def hash
11
+ return { singleLocation: ship_from } unless ship_to.present?
12
+
13
+ { shipFrom: ship_from, shipTo: ship_to }
14
+ end
15
+
16
+ private
17
+
18
+ def ship_from
19
+ Avatax.config.distribution_center
20
+ end
21
+
22
+ def ship_to
23
+ return unless address = tax_request.shippings.first.try(:address)
24
+ {
25
+ line1: address.street,
26
+ line2: address.street_2,
27
+ city: address.city,
28
+ region: address.region,
29
+ country: address.country.alpha2,
30
+ postalCode: address.postal_code
31
+ }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,47 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxRequest::LineItem
4
+ attr_accessor :line_number
5
+
6
+ def initialize(**)
7
+ @line_number = 0
8
+ end
9
+
10
+ def hash
11
+ {
12
+ quantity: quantity,
13
+ amount: amount.to_s,
14
+ itemCode: item_code,
15
+ taxCode: tax_code,
16
+ description: description,
17
+ number: line_number,
18
+ addresses: addresses
19
+ }.compact
20
+ end
21
+
22
+ private
23
+
24
+ def quantity
25
+ raise NotImplementedError, "#{self.class.name} must implement #quantity"
26
+ end
27
+
28
+ def amount
29
+ raise NotImplementedError, "#{self.class.name} must implement #amount"
30
+ end
31
+
32
+ def item_code
33
+ raise NotImplementedError, "#{self.class.name} must implement #item_code"
34
+ end
35
+
36
+ def tax_code
37
+ raise NotImplementedError, "#{self.class.name} must implement #tax_code"
38
+ end
39
+
40
+ def description
41
+ raise NotImplementedError, "#{self.class.name} must implement #description_code"
42
+ end
43
+
44
+ def addresses; end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxRequest::OrderLineItem < TaxRequest::LineItem
4
+ attr_reader :order_item, :adjustment, :shipping
5
+
6
+ def initialize(order_item:, adjustment:, adjustment_set:, quantity: nil, shipping: nil)
7
+ @order_item = order_item
8
+ @adjustment = adjustment
9
+ @adjustment_set = adjustment_set
10
+ @quantity = quantity
11
+ @shipping = shipping
12
+ end
13
+
14
+ private
15
+
16
+ def addresses
17
+ return unless address = shipping&.address
18
+
19
+ {
20
+ shipTo: {
21
+ line1: address.street,
22
+ line2: address.street_2,
23
+ city: address.city,
24
+ region: address.region,
25
+ country: address.country.alpha2,
26
+ postalCode: address.postal_code
27
+ }
28
+ }
29
+ end
30
+
31
+ def quantity
32
+ @quantity || adjustment.quantity
33
+ end
34
+
35
+ def amount
36
+ total = @adjustment_set.taxable_share_for adjustment
37
+ if shipping.present? && shipping.partial?
38
+ total *= quantity / order_item.quantity.to_f
39
+ end
40
+ total
41
+ end
42
+
43
+ def item_code
44
+ order_item.sku
45
+ end
46
+
47
+ def tax_code
48
+ adjustment.data["tax_code"]
49
+ end
50
+
51
+ def product
52
+ @product ||= Mongoid::Factory.from_db(
53
+ Catalog::Product,
54
+ order_item.product_attributes
55
+ )
56
+ end
57
+
58
+ def description
59
+ return if order_item.product_attributes.empty?
60
+ product.name
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,70 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxRequest::OrderLines
4
+ attr_reader :order, :shippings
5
+
6
+ def initialize(order, shippings)
7
+ @order = order
8
+ @shippings = shippings
9
+ end
10
+
11
+ def order_line_items
12
+ @order_line_items ||=
13
+ if shippings.any?(&:partial?)
14
+ split_shipping_lines
15
+ else
16
+ single_shipping_lines
17
+ end.map.with_index(1) { |line, index| line.tap { |li| li.line_number = index } }
18
+ end
19
+
20
+ def shipping_line_items
21
+ @shipping_line_item ||= shippings.map { |shipping| TaxRequest::ShippingLineItem.new(shipping: shipping) }
22
+ end
23
+
24
+ def lines
25
+ (order_line_items + shipping_line_items).map.with_index(1) do |line_item, index|
26
+ line_item.tap { |li| li.line_number = index }
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def order_price_adjustments
33
+ order.price_adjustments
34
+ end
35
+
36
+ def grouped_order_price_adjustments
37
+ order_price_adjustments.grouped_by_parent
38
+ end
39
+
40
+ def single_shipping_lines
41
+ grouped_order_price_adjustments.flat_map do |item, set|
42
+ set.map do |adjustment|
43
+ next unless adjustment.data["tax_code"].present?
44
+
45
+ TaxRequest::OrderLineItem.new(order_item: item, adjustment: adjustment, adjustment_set: set)
46
+ end
47
+ end.compact
48
+ end
49
+
50
+ def split_shipping_lines
51
+ shippings.flat_map do |shipping|
52
+ shipping.quantities.flat_map do |order_item_id, quantity|
53
+ item = order.items.detect { |oi| oi.id.to_s == order_item_id }
54
+ adjustment_set = grouped_order_price_adjustments[item]
55
+
56
+ adjustment_set.map do |adjustment|
57
+ TaxRequest::OrderLineItem.new(
58
+ order_item: item,
59
+ adjustment: adjustment,
60
+ adjustment_set: adjustment_set,
61
+ quantity: quantity,
62
+ shipping: shipping
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,49 @@
1
+ module Workarea
2
+ module Avatax
3
+ class TaxRequest::Response
4
+ attr_reader :response, :request_order_line_items, :request_shipping_line_items
5
+ delegate :success?, to: :response
6
+
7
+ def initialize(response:, request_order_line_items:, request_shipping_line_items:)
8
+ @response = response
9
+ @request_order_line_items = request_order_line_items
10
+ @request_shipping_line_items = request_shipping_line_items
11
+ end
12
+
13
+ def body
14
+ @body ||= Hashie::Mash.new response.body
15
+ end
16
+
17
+ def tax_line_for_adjustment(price_adjustment, shipping: nil)
18
+ return unless success?
19
+
20
+ line_number =
21
+ if shipping&.partial?
22
+ request_order_line_items
23
+ .detect { |line_item| line_item.adjustment == price_adjustment && line_item.shipping == shipping }
24
+ .try(:line_number)
25
+ else
26
+ request_order_line_items
27
+ .detect { |line_item| line_item.adjustment == price_adjustment }
28
+ .try(:line_number)
29
+ end
30
+
31
+ return unless line_number
32
+
33
+ body.lines.detect { |line| line.lineNumber.to_i == line_number }
34
+ end
35
+
36
+ def tax_line_for_shipping(shipping)
37
+ return unless success?
38
+
39
+ line_number = request_shipping_line_items
40
+ .detect { |line_item| line_item.shipping.id == shipping.id }
41
+ .try(:line_number)
42
+
43
+ return unless line_number
44
+
45
+ body.lines.detect { |line| line.lineNumber.to_i == line_number }
46
+ end
47
+ end
48
+ end
49
+ end