workarea-avatax 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.editorconfig +20 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.github/workflows/ci.yml +66 -0
- data/.gitignore +18 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +123 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +10 -0
- data/LICENSE.md +3 -0
- data/README.md +57 -0
- data/Rakefile +50 -0
- data/app/models/workarea/avatax/usage_type.rb +17 -0
- data/app/models/workarea/pricing/calculators/avalara_tax_calculator.rb +105 -0
- data/app/models/workarea/user.decorator +7 -0
- data/app/seeds/workarea/avatax_seeds.rb +44 -0
- data/app/view_models/workarea/admin/user_view_model.decorator +21 -0
- data/app/view_models/workarea/storefront/cart_view_model.decorator +7 -0
- data/app/views/workarea/admin/users/_avatax_properties_fields.html.haml +5 -0
- data/app/workers/workarea/avatax/tax_invoice_worker.rb +29 -0
- data/bin/rails +17 -0
- data/config/initializers/appends.rb +4 -0
- data/config/initializers/avatax.rb +14 -0
- data/config/initializers/seeds.rb +1 -0
- data/config/locales/en.yml +6 -0
- data/lib/avatax/api.decorator +52 -0
- data/lib/workarea/avatax/bogus_gateway/bogus_create_transaction.rb +187 -0
- data/lib/workarea/avatax/bogus_gateway.rb +34 -0
- data/lib/workarea/avatax/engine.rb +36 -0
- data/lib/workarea/avatax/tax_request/addresses.rb +35 -0
- data/lib/workarea/avatax/tax_request/line_item.rb +47 -0
- data/lib/workarea/avatax/tax_request/order_line_item.rb +64 -0
- data/lib/workarea/avatax/tax_request/order_lines.rb +70 -0
- data/lib/workarea/avatax/tax_request/response.rb +49 -0
- data/lib/workarea/avatax/tax_request/shipping_line_item.rb +34 -0
- data/lib/workarea/avatax/tax_request.rb +77 -0
- data/lib/workarea/avatax/version.rb +5 -0
- data/lib/workarea/avatax.rb +71 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +34 -0
- data/test/dummy/bin/update +29 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +9 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +54 -0
- data/test/dummy/config/environments/production.rb +86 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/new_framework_defaults.rb +23 -0
- data/test/dummy/config/initializers/workarea.rb +5 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/puma.rb +47 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/db/seeds.rb +2 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/test/dummy/public/apple-touch-icon.png +0 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/factories/workarea/avatax_factories.rb +142 -0
- data/test/lib/workarea/avatax/tax_request/order_line_items_test.rb +114 -0
- data/test/lib/workarea/avatax/tax_request/response_test.rb +42 -0
- data/test/lib/workarea/avatax/tax_request_test.rb +51 -0
- data/test/lib/workarea/avatax_test.rb +28 -0
- data/test/models/workarea/avatax/usage_type_test.rb +13 -0
- data/test/models/workarea/pricing/calculators/avalara_tax_calculator_test.rb +113 -0
- data/test/support/avatax_test_settings.rb +9 -0
- data/test/system/workarea/storefront/avatax_system_test.rb +22 -0
- data/test/test_helper.rb +20 -0
- data/test/vcr_cassettes/succesful_avatax_create_transaction.yml +60 -0
- data/test/vcr_cassettes/succesful_avatax_create_transaction_multiple_shippings.yml +76 -0
- data/workarea-avatax.gemspec +20 -0
- 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,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,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
|