solidus_avatax 0.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/.gitignore +15 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +15 -0
- data/LICENSE +26 -0
- data/README.md +71 -0
- data/Rakefile +21 -0
- data/app/assets/javascripts/spree/frontend/solidus_avatax.js +1 -0
- data/app/assets/stylesheets/spree/frontend/solidus_avatax.css +1 -0
- data/app/models/spree/adjustment_decorator.rb +21 -0
- data/app/models/spree/order_contents_decorator.rb +26 -0
- data/app/models/spree/order_decorator.rb +43 -0
- data/app/models/spree/promotion_handler/coupon_decorator.rb +11 -0
- data/app/models/spree/reimbursement_decorator.rb +1 -0
- data/app/models/spree/tax_rate_decorator.rb +45 -0
- data/app/models/spree_avatax/calculator.rb +26 -0
- data/app/models/spree_avatax/return_invoice.rb +197 -0
- data/app/models/spree_avatax/sales_invoice.rb +157 -0
- data/app/models/spree_avatax/sales_shared.rb +211 -0
- data/app/models/spree_avatax/shared.rb +53 -0
- data/app/models/spree_avatax/short_ship_return_invoice.rb +133 -0
- data/app/models/spree_avatax/short_ship_return_invoice_inventory_unit.rb +11 -0
- data/bin/rails +7 -0
- data/circle.yml +6 -0
- data/config/locales/en.yml +5 -0
- data/db/migrate/20140122165618_add_avatax_response_at_to_orders.rb +5 -0
- data/db/migrate/20140214153139_add_avatax_invoice_at_to_orders.rb +5 -0
- data/db/migrate/20140617222244_close_all_tax_adjustments.rb +5 -0
- data/db/migrate/20140701144237_update_avatax_calculator_type.rb +17 -0
- data/db/migrate/20140801132302_create_spree_avatax_return_invoices.rb +19 -0
- data/db/migrate/20140903135132_create_spree_avatax_sales_orders.rb +15 -0
- data/db/migrate/20140903135357_create_spree_avatax_sales_invoices.rb +19 -0
- data/db/migrate/20140904171341_generate_uncommitted_sales_invoices.rb +31 -0
- data/db/migrate/20140911214414_add_transaction_id_to_sales_orders_and_sales_invoices.rb +6 -0
- data/db/migrate/20140911215422_add_canceled_at_and_cancel_transaction_id_to_sales_invoices.rb +6 -0
- data/db/migrate/20150427154942_create_spree_avatax_short_ship_return_invoices.rb +44 -0
- data/db/migrate/20150518172627_fix_avatax_short_ship_index.rb +30 -0
- data/lib/generators/solidus_avatax/install/install_generator.rb +31 -0
- data/lib/generators/solidus_avatax/install/templates/config/initializers/avatax.rb +18 -0
- data/lib/solidus_avatax.rb +4 -0
- data/lib/spree_avatax/config.rb +16 -0
- data/lib/spree_avatax/engine.rb +29 -0
- data/lib/spree_avatax/factories.rb +25 -0
- data/lib/tasks/commit_backfill.rake +119 -0
- data/lib/tasks/sales_invoice_backfill.rake +43 -0
- data/solidus_avatax.gemspec +34 -0
- data/spec/features/store_credits_spec.rb +92 -0
- data/spec/features/tax_calculation_spec.rb +75 -0
- data/spec/fixtures/vcr_cassettes/sales_invoice_gettax_with_discounts.yml +136 -0
- data/spec/fixtures/vcr_cassettes/sales_invoice_gettax_without_discounts.yml +136 -0
- data/spec/fixtures/vcr_cassettes/taxes_with_store_credits.yml +113 -0
- data/spec/models/spree/adjustment_spec.rb +23 -0
- data/spec/models/spree/order_contents_spec.rb +35 -0
- data/spec/models/spree/order_spec.rb +111 -0
- data/spec/models/spree/shipping_rate_spec.rb +23 -0
- data/spec/models/spree/tax_rate_spec.rb +40 -0
- data/spec/models/spree_avatax/calculator.rb +20 -0
- data/spec/models/spree_avatax/return_invoice_spec.rb +193 -0
- data/spec/models/spree_avatax/sales_invoice_spec.rb +491 -0
- data/spec/models/spree_avatax/sales_shared_spec.rb +47 -0
- data/spec/models/spree_avatax/shared_spec.rb +89 -0
- data/spec/models/spree_avatax/short_ship_return_invoice_spec.rb +181 -0
- data/spec/spec_helper.rb +85 -0
- data/spec/support/return_invoice_soap_responses.rb +117 -0
- data/spec/support/sales_invoice_soap_responses.rb +259 -0
- data/spec/support/short_ship_return_invoice_soap_responses.rb +178 -0
- data/spec/support/zone_support.rb +6 -0
- metadata +320 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
# A SalesInvoice is persisted by Avatax but it's not recognized as complete until it's "committed".
|
2
|
+
class SpreeAvatax::SalesInvoice < ActiveRecord::Base
|
3
|
+
DOC_TYPE = 'SalesInvoice'
|
4
|
+
CANCEL_CODE = 'DocVoided'
|
5
|
+
|
6
|
+
class CommitInvoiceNotFound < StandardError; end
|
7
|
+
class AlreadyCommittedError < StandardError; end
|
8
|
+
|
9
|
+
belongs_to :order, class_name: "Spree::Order", inverse_of: :avatax_sales_invoice
|
10
|
+
|
11
|
+
validates :order, presence: true
|
12
|
+
validates :doc_id, presence: true
|
13
|
+
validates :doc_code, presence: true
|
14
|
+
validates :doc_date, presence: true
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Calls the Avatax API to generate a sales invoice and calculate taxes on the line items.
|
18
|
+
# On failure it will raise.
|
19
|
+
# On success it updates taxes on the order and its line items and create a SalesInvoice record.
|
20
|
+
# At this point the record is saved but uncommitted on Avatax's end.
|
21
|
+
# After the order completes the ".commit" method will get called and we'll commit the
|
22
|
+
# sales invoice, which marks it as complete on Avatax's end.
|
23
|
+
def generate(order)
|
24
|
+
bench_start = Time.now
|
25
|
+
|
26
|
+
return if order.completed? || !SpreeAvatax::Shared.taxable_order?(order)
|
27
|
+
|
28
|
+
taxable_records = order.line_items + order.shipments
|
29
|
+
taxable_records.each do |taxable_record|
|
30
|
+
taxable_record.update_column(:pre_tax_amount, taxable_record.discounted_amount.round(2))
|
31
|
+
end
|
32
|
+
|
33
|
+
result = SpreeAvatax::SalesShared.get_tax(order, DOC_TYPE)
|
34
|
+
# run this immediately to ensure that everything matches up before modifying the database
|
35
|
+
tax_line_data = SpreeAvatax::SalesShared.build_tax_line_data(order, result)
|
36
|
+
|
37
|
+
if sales_invoice = order.avatax_sales_invoice
|
38
|
+
if sales_invoice.committed_at.nil?
|
39
|
+
sales_invoice.destroy
|
40
|
+
else
|
41
|
+
raise AlreadyCommittedError.new("Sales invoice #{sales_invoice.id} is already committed.")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
sales_invoice = order.create_avatax_sales_invoice!({
|
46
|
+
transaction_id: result[:transaction_id],
|
47
|
+
doc_id: result[:doc_id],
|
48
|
+
doc_code: result[:doc_code],
|
49
|
+
doc_date: result[:doc_date],
|
50
|
+
pre_tax_total: result[:total_amount],
|
51
|
+
additional_tax_total: result[:total_tax],
|
52
|
+
})
|
53
|
+
|
54
|
+
SpreeAvatax::SalesShared.update_taxes(order, tax_line_data)
|
55
|
+
|
56
|
+
sales_invoice
|
57
|
+
rescue Exception => e
|
58
|
+
if SpreeAvatax::Config.sales_invoice_generate_error_handler
|
59
|
+
SpreeAvatax::Config.sales_invoice_generate_error_handler.call(order, e)
|
60
|
+
else
|
61
|
+
raise
|
62
|
+
end
|
63
|
+
ensure
|
64
|
+
duration = (Time.now - bench_start).round
|
65
|
+
Rails.logger.info "avatax_sales_invoice_generate_duration=#{(duration*1000).round}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def commit(order)
|
69
|
+
return if !SpreeAvatax::Shared.taxable_order?(order)
|
70
|
+
|
71
|
+
raise CommitInvoiceNotFound.new("No invoice for order #{order.number}") if order.avatax_sales_invoice.nil?
|
72
|
+
|
73
|
+
post_tax(order.avatax_sales_invoice)
|
74
|
+
|
75
|
+
order.avatax_sales_invoice.update!(committed_at: Time.now)
|
76
|
+
|
77
|
+
order.avatax_sales_invoice
|
78
|
+
rescue Exception => e
|
79
|
+
if SpreeAvatax::Config.sales_invoice_commit_error_handler
|
80
|
+
SpreeAvatax::Config.sales_invoice_commit_error_handler.call(order, e)
|
81
|
+
else
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def cancel(order)
|
87
|
+
return if order.avatax_sales_invoice.nil?
|
88
|
+
|
89
|
+
result = cancel_tax(order.avatax_sales_invoice)
|
90
|
+
|
91
|
+
order.avatax_sales_invoice.update!({
|
92
|
+
canceled_at: Time.now,
|
93
|
+
cancel_transaction_id: result[:transaction_id],
|
94
|
+
})
|
95
|
+
rescue Exception => e
|
96
|
+
if SpreeAvatax::Config.sales_invoice_cancel_error_handler
|
97
|
+
SpreeAvatax::Config.sales_invoice_cancel_error_handler.call(order, e)
|
98
|
+
else
|
99
|
+
raise
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def post_tax(sales_invoice)
|
106
|
+
params = posttax_params(sales_invoice)
|
107
|
+
|
108
|
+
logger.info "[avatax] posttax sales_invoice=#{sales_invoice.id} order=#{sales_invoice.order_id}"
|
109
|
+
logger.debug { "[avatax] params: #{params.to_json}" }
|
110
|
+
|
111
|
+
response = SpreeAvatax::Shared.tax_svc.posttax(params)
|
112
|
+
SpreeAvatax::Shared.require_success!(response)
|
113
|
+
|
114
|
+
response
|
115
|
+
end
|
116
|
+
|
117
|
+
def cancel_tax(sales_invoice)
|
118
|
+
params = canceltax_params(sales_invoice)
|
119
|
+
|
120
|
+
logger.info "[avatax] canceltax sales_invoice=#{sales_invoice.id}"
|
121
|
+
logger.debug { "[avatax] params: #{params.to_json}" }
|
122
|
+
|
123
|
+
response = SpreeAvatax::Shared.tax_svc.canceltax(params)
|
124
|
+
|
125
|
+
SpreeAvatax::Shared.require_success!(response)
|
126
|
+
|
127
|
+
response
|
128
|
+
end
|
129
|
+
|
130
|
+
# see https://github.com/avadev/AvaTax-Calc-SOAP-Ruby/blob/master/PostTaxTest.rb
|
131
|
+
def posttax_params(sales_invoice)
|
132
|
+
{
|
133
|
+
doccode: sales_invoice.doc_code,
|
134
|
+
companycode: SpreeAvatax::Config.company_code,
|
135
|
+
|
136
|
+
doctype: DOC_TYPE,
|
137
|
+
docdate: sales_invoice.doc_date,
|
138
|
+
|
139
|
+
commit: true,
|
140
|
+
|
141
|
+
totalamount: sales_invoice.pre_tax_total,
|
142
|
+
totaltax: sales_invoice.additional_tax_total,
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
# see https://github.com/avadev/AvaTax-Calc-SOAP-Ruby/blob/master/CancelTaxTest.rb
|
147
|
+
def canceltax_params(sales_invoice)
|
148
|
+
{
|
149
|
+
# Required Parameters
|
150
|
+
doccode: sales_invoice.doc_code,
|
151
|
+
doctype: DOC_TYPE,
|
152
|
+
cancelcode: CANCEL_CODE,
|
153
|
+
companycode: SpreeAvatax::Config.company_code,
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
module SpreeAvatax::SalesShared
|
2
|
+
|
3
|
+
DESTINATION_CODE = "1"
|
4
|
+
|
5
|
+
SHIPPING_TAX_CODE = 'FR020100' # docs: http://goo.gl/KuIuxc
|
6
|
+
|
7
|
+
SHIPPING_DESCRIPTION = 'Shipping Charge'
|
8
|
+
|
9
|
+
class InvalidApiResponse < StandardError; end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Queries Avatax for taxes on a specific order using the specified doc_type.
|
13
|
+
# SalesOrder doc types are not persisted on Avatax.
|
14
|
+
# SalesInvoice doc types do persist an uncommitted record on Avatax.
|
15
|
+
def get_tax(order, doc_type)
|
16
|
+
params = gettax_params(order, doc_type)
|
17
|
+
|
18
|
+
logger.info "[avatax] gettax order=#{order.id} doc_type=#{doc_type}"
|
19
|
+
logger.debug { "[avatax] params: #{params.to_json}" }
|
20
|
+
|
21
|
+
response = SpreeAvatax::Shared.tax_svc.gettax(params)
|
22
|
+
SpreeAvatax::Shared.require_success!(response)
|
23
|
+
|
24
|
+
response
|
25
|
+
end
|
26
|
+
|
27
|
+
def update_taxes(order, tax_line_data)
|
28
|
+
reset_tax_attributes(order)
|
29
|
+
|
30
|
+
tax_line_data.each do |data|
|
31
|
+
record, tax_line = data[:record], data[:tax_line]
|
32
|
+
|
33
|
+
record.update_column(:pre_tax_amount, record.discounted_amount)
|
34
|
+
|
35
|
+
tax = BigDecimal.new(tax_line[:tax]).abs
|
36
|
+
|
37
|
+
record.adjustments.tax.create!({
|
38
|
+
adjustable: record,
|
39
|
+
amount: tax,
|
40
|
+
order: order,
|
41
|
+
label: Spree.t(:avatax_label),
|
42
|
+
included: false, # would be true for VAT
|
43
|
+
source: Spree::TaxRate.avatax_the_one_rate,
|
44
|
+
finalized: true, # this tells spree not to automatically recalculate avatax tax adjustments
|
45
|
+
})
|
46
|
+
|
47
|
+
Spree::ItemAdjustments.new(record).update
|
48
|
+
record.save!
|
49
|
+
end
|
50
|
+
|
51
|
+
Spree::OrderUpdater.new(order).update
|
52
|
+
order.save!
|
53
|
+
end
|
54
|
+
|
55
|
+
# returns an array like:
|
56
|
+
# [
|
57
|
+
# {tax_line: {...}, record: #<Spree::LineItem id=111>},
|
58
|
+
# {tax_line: {...}, record: #<Spree::LineItem id=222>},
|
59
|
+
# {tax_line: {...}, record: #<Spree::Shipment id=111>},
|
60
|
+
# ]
|
61
|
+
def build_tax_line_data(order, avatax_result)
|
62
|
+
# Array.wrap is required because the XML engine the Avatax gem uses turns child nodes into
|
63
|
+
# {...} instead of [{...}] when there is only one child.
|
64
|
+
tax_lines = Array.wrap(avatax_result[:tax_lines][:tax_line])
|
65
|
+
|
66
|
+
# builds a hash like: {"L-111": {record: #<Spree::LineItem ...>}, ...}
|
67
|
+
data = (order.line_items + order.shipments).map { |r| [avatax_id(r), {record: r}] }.to_h
|
68
|
+
|
69
|
+
# adds :tax_line to each entry in the data
|
70
|
+
tax_lines.each do |tax_line|
|
71
|
+
avatax_id = tax_line[:no]
|
72
|
+
if data[avatax_id]
|
73
|
+
data[avatax_id][:tax_line] = tax_line
|
74
|
+
else
|
75
|
+
raise InvalidApiResponse.new("Couldn't find #{avatax_id.inspect}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
missing = data.select { |avatax_id, data| data[:tax_line].nil? }
|
80
|
+
if missing.any?
|
81
|
+
raise InvalidApiResponse.new("missing tax data for #{missing.keys}")
|
82
|
+
end
|
83
|
+
|
84
|
+
data.values
|
85
|
+
end
|
86
|
+
|
87
|
+
# sometimes we have to store different types of things in a single array (like line items and
|
88
|
+
# shipments). this allows us to provide a unique identifier to each record.
|
89
|
+
def avatax_id(record)
|
90
|
+
"#{record.class.name}-#{record.id}"
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Clears previously-set tax attributes from an order, if any, unless the
|
95
|
+
# order has already been completed.
|
96
|
+
#
|
97
|
+
# @param order [Spree::Order] the order
|
98
|
+
def reset_tax_attributes(order)
|
99
|
+
return if order.completed?
|
100
|
+
|
101
|
+
destroyed_adjustments = order.all_adjustments.tax.destroy_all
|
102
|
+
return if destroyed_adjustments.empty?
|
103
|
+
|
104
|
+
order.line_items.each do |line_item|
|
105
|
+
line_item.update_attributes!({
|
106
|
+
additional_tax_total: 0,
|
107
|
+
adjustment_total: 0,
|
108
|
+
pre_tax_amount: 0,
|
109
|
+
included_tax_total: 0,
|
110
|
+
})
|
111
|
+
|
112
|
+
Spree::ItemAdjustments.new(line_item).update
|
113
|
+
line_item.save!
|
114
|
+
end
|
115
|
+
|
116
|
+
order.update_attributes!({
|
117
|
+
additional_tax_total: 0,
|
118
|
+
adjustment_total: 0,
|
119
|
+
included_tax_total: 0,
|
120
|
+
})
|
121
|
+
|
122
|
+
order.update!
|
123
|
+
order.save!
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def logger
|
129
|
+
SpreeAvatax::Shared.logger
|
130
|
+
end
|
131
|
+
|
132
|
+
# see https://github.com/avadev/AvaTax-Calc-SOAP-Ruby/blob/master/GetTaxTest.rb
|
133
|
+
def gettax_params(order, doc_type)
|
134
|
+
{
|
135
|
+
doccode: order.number,
|
136
|
+
customercode: REXML::Text.normalize(order.email),
|
137
|
+
companycode: SpreeAvatax::Config.company_code,
|
138
|
+
|
139
|
+
doctype: doc_type,
|
140
|
+
docdate: Date.today,
|
141
|
+
|
142
|
+
commit: false, # we commit separately after the order completes
|
143
|
+
|
144
|
+
# NOTE: we only want order-level adjustments here. not line item or shipping adjustments.
|
145
|
+
# avatax distributes order-level discounts across all "lineitem" entries that have
|
146
|
+
# "discounted:true"
|
147
|
+
# Also, the "discount" can be negative and Avatax handles that OK. A negative number
|
148
|
+
# would mean that *charges* were added to the order via an order-level adjustment.
|
149
|
+
discount: order.avatax_order_adjustment_total.round(2).to_f,
|
150
|
+
|
151
|
+
addresses: [
|
152
|
+
{
|
153
|
+
addresscode: DESTINATION_CODE,
|
154
|
+
line1: REXML::Text.normalize(order.ship_address.address1),
|
155
|
+
line2: REXML::Text.normalize(order.ship_address.address2),
|
156
|
+
city: REXML::Text.normalize(order.ship_address.city),
|
157
|
+
postalcode: REXML::Text.normalize(order.ship_address.zipcode),
|
158
|
+
},
|
159
|
+
],
|
160
|
+
|
161
|
+
lines: gettax_lines_params(order),
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
def gettax_lines_params(order)
|
166
|
+
line_items = order.line_items.includes(variant: :product)
|
167
|
+
|
168
|
+
line_item_lines = line_items.map do |line_item|
|
169
|
+
{
|
170
|
+
# Required Parameters
|
171
|
+
no: avatax_id(line_item),
|
172
|
+
qty: line_item.quantity,
|
173
|
+
amount: line_item.discounted_amount.round(2).to_f,
|
174
|
+
origincodeline: DESTINATION_CODE, # We don't really send the correct value here
|
175
|
+
destinationcodeline: DESTINATION_CODE,
|
176
|
+
|
177
|
+
# Best Practice Parameters
|
178
|
+
description: REXML::Text.normalize(line_item.variant.product.description.to_s.truncate(100)),
|
179
|
+
|
180
|
+
# Optional Parameters
|
181
|
+
itemcode: line_item.variant.sku,
|
182
|
+
taxcode: line_item.tax_category.tax_code,
|
183
|
+
# "discounted" tells avatax to include this item when it distributes order-level discounts
|
184
|
+
# across avatax "lines"
|
185
|
+
discounted: true,
|
186
|
+
}
|
187
|
+
end
|
188
|
+
|
189
|
+
shipment_lines = order.shipments.map do |shipment|
|
190
|
+
{
|
191
|
+
# Required Parameters
|
192
|
+
no: avatax_id(shipment),
|
193
|
+
qty: 1,
|
194
|
+
amount: shipment.discounted_amount.round(2).to_f,
|
195
|
+
origincodeline: DESTINATION_CODE, # We don't really send the correct value here
|
196
|
+
destinationcodeline: DESTINATION_CODE,
|
197
|
+
|
198
|
+
# Best Practice Parameters
|
199
|
+
description: SHIPPING_DESCRIPTION,
|
200
|
+
|
201
|
+
# Optional Parameters
|
202
|
+
taxcode: SHIPPING_TAX_CODE,
|
203
|
+
# order-level discounts do not apply to shipments
|
204
|
+
discounted: false,
|
205
|
+
}
|
206
|
+
end
|
207
|
+
|
208
|
+
line_item_lines + shipment_lines
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module SpreeAvatax::Shared
|
2
|
+
class FailedApiResponse < StandardError
|
3
|
+
attr_reader :response, :messages
|
4
|
+
|
5
|
+
def initialize(response)
|
6
|
+
@response = response
|
7
|
+
# avatax seems to have two different error message formats:
|
8
|
+
# https://gist.github.com/jordan-brough/a22163e4551c692365b8
|
9
|
+
# https://gist.github.com/jordan-brough/c778a3417850dfa2307c
|
10
|
+
# We should pester Avatax about this sometime.
|
11
|
+
if @response[:messages].is_a?(Array)
|
12
|
+
@messages = response[:messages]
|
13
|
+
else
|
14
|
+
@messages = Array.wrap(response[:messages][:message])
|
15
|
+
end
|
16
|
+
|
17
|
+
super(messages.map { |msg| msg[:summary] })
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def logger
|
24
|
+
Rails.logger
|
25
|
+
end
|
26
|
+
|
27
|
+
def taxable_order?(order)
|
28
|
+
order.line_items.present? && order.ship_address.present?
|
29
|
+
end
|
30
|
+
|
31
|
+
def tax_svc
|
32
|
+
@tax_svc ||= AvaTax::TaxService.new({
|
33
|
+
username: SpreeAvatax::Config.username,
|
34
|
+
password: SpreeAvatax::Config.password,
|
35
|
+
service_url: SpreeAvatax::Config.service_url,
|
36
|
+
clientname: 'Spree::Avatax',
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
def require_success!(response)
|
41
|
+
if response[:result_code] == 'Success'
|
42
|
+
logger.info "[avatax] response - result=success doc_id=#{response[:doc_id]} doc_code=#{response[:doc_code]} transaction_id=#{response[:transaction_id]}"
|
43
|
+
logger.debug { "[avatax] response: #{response.to_json}" }
|
44
|
+
else
|
45
|
+
logger.error "[avatax] response - result=error doc_id=#{response[:doc_id]} doc_code=#{response[:doc_code]} transaction_id=#{response[:transaction_id]}"
|
46
|
+
logger.error "[avatax] response: #{response.to_json}"
|
47
|
+
raise FailedApiResponse.new(response)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
class SpreeAvatax::ShortShipReturnInvoice < ActiveRecord::Base
|
2
|
+
DOC_TYPE = 'ReturnInvoice'
|
3
|
+
|
4
|
+
DESTINATION_CODE = "1"
|
5
|
+
|
6
|
+
TAX_OVERRIDE_TYPE = 'TaxAmount'
|
7
|
+
TAX_OVERRIDE_REASON = 'Short ship'
|
8
|
+
|
9
|
+
has_many :short_ship_return_invoice_inventory_units, class_name: 'SpreeAvatax::ShortShipReturnInvoiceInventoryUnit', inverse_of: :short_ship_return_invoice
|
10
|
+
has_many :inventory_units, through: :short_ship_return_invoice_inventory_units, class_name: 'Spree::InventoryUnit'
|
11
|
+
|
12
|
+
validates :doc_id, presence: true
|
13
|
+
validates :doc_code, presence: true
|
14
|
+
validates :doc_date, presence: true
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Calls the Avatax API to generate a return invoice for an item that has
|
18
|
+
# been short shipped. It tells Avatax how much tax was refunded rather than
|
19
|
+
# asking it how much should be refunded.
|
20
|
+
# It is generated in the "committed" state since there is no need for a two-
|
21
|
+
# step commit here.
|
22
|
+
#
|
23
|
+
# On failure it will raise.
|
24
|
+
def generate(unit_cancels:)
|
25
|
+
inventory_units = unit_cancels.map(&:inventory_unit)
|
26
|
+
|
27
|
+
order_ids = inventory_units.map(&:order_id).uniq
|
28
|
+
if order_ids.size > 1
|
29
|
+
raise "unit cancels #{unit_cancels.map(&:id)} had more than one order: #{order_ids}"
|
30
|
+
end
|
31
|
+
|
32
|
+
success_result = get_tax(unit_cancels: unit_cancels)
|
33
|
+
|
34
|
+
create!(
|
35
|
+
inventory_units: inventory_units,
|
36
|
+
committed: true,
|
37
|
+
doc_id: success_result[:doc_id],
|
38
|
+
doc_code: success_result[:doc_code],
|
39
|
+
doc_date: success_result[:doc_date],
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_tax(unit_cancels:)
|
46
|
+
params = gettax_params(unit_cancels: unit_cancels)
|
47
|
+
|
48
|
+
logger.info("[avatax] gettax unit_cancel_ids=#{unit_cancels.map(&:id)} doc_type=#{DOC_TYPE}")
|
49
|
+
logger.debug("[avatax] params: " + params.to_json)
|
50
|
+
|
51
|
+
response = SpreeAvatax::Shared.tax_svc.gettax(params)
|
52
|
+
SpreeAvatax::Shared.require_success!(response)
|
53
|
+
|
54
|
+
response
|
55
|
+
end
|
56
|
+
|
57
|
+
# see https://github.com/avadev/AvaTax-Calc-SOAP-Ruby/blob/master/GetTaxTest.rb
|
58
|
+
def gettax_params(unit_cancels:)
|
59
|
+
# we verified previously that there is only one order
|
60
|
+
order = unit_cancels.first.inventory_unit.order
|
61
|
+
|
62
|
+
lines = gettax_line_params(
|
63
|
+
unit_cancels: unit_cancels,
|
64
|
+
taxed_at: order.avatax_sales_invoice.try!(:doc_date) || order.completed_at
|
65
|
+
)
|
66
|
+
|
67
|
+
{
|
68
|
+
doccode: "#{order.number}-short-#{Time.now.to_f}",
|
69
|
+
referencecode: order.number,
|
70
|
+
customercode: order.user_id,
|
71
|
+
companycode: SpreeAvatax::Config.company_code,
|
72
|
+
|
73
|
+
doctype: DOC_TYPE,
|
74
|
+
docdate: Date.today,
|
75
|
+
|
76
|
+
commit: true,
|
77
|
+
|
78
|
+
addresses: [
|
79
|
+
{
|
80
|
+
addresscode: DESTINATION_CODE,
|
81
|
+
line1: REXML::Text.normalize(order.ship_address.address1),
|
82
|
+
line2: REXML::Text.normalize(order.ship_address.address2),
|
83
|
+
city: REXML::Text.normalize(order.ship_address.city),
|
84
|
+
postalcode: REXML::Text.normalize(order.ship_address.zipcode),
|
85
|
+
},
|
86
|
+
],
|
87
|
+
|
88
|
+
lines: lines,
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def gettax_line_params(unit_cancels:, taxed_at:)
|
93
|
+
unit_cancels.sort_by(&:id).map do |unit_cancel|
|
94
|
+
inventory_unit = unit_cancel.inventory_unit
|
95
|
+
|
96
|
+
adjustment = unit_cancel.adjustment
|
97
|
+
|
98
|
+
if adjustment
|
99
|
+
total = -adjustment.amount # adjustment is stored as a negative amount
|
100
|
+
tax = inventory_unit.additional_tax_total
|
101
|
+
before_tax = total - tax
|
102
|
+
else
|
103
|
+
# TODO: Consider removing this case. Are there expected scenarios
|
104
|
+
# where there will be no adjustment present?
|
105
|
+
total = 0
|
106
|
+
tax = 0
|
107
|
+
before_tax = 0
|
108
|
+
end
|
109
|
+
|
110
|
+
{
|
111
|
+
# Required Parameters
|
112
|
+
no: inventory_unit.id,
|
113
|
+
itemcode: inventory_unit.line_item.variant.sku,
|
114
|
+
taxcode: inventory_unit.line_item.tax_category.tax_code,
|
115
|
+
qty: 1,
|
116
|
+
amount: -before_tax,
|
117
|
+
origincodeline: DESTINATION_CODE, # We don't really send the correct value here
|
118
|
+
destinationcodeline: DESTINATION_CODE,
|
119
|
+
|
120
|
+
# We tell Avatax what the amounts were rather than asking Avatax what
|
121
|
+
# the amounts should have been.
|
122
|
+
taxoverridetypeline: TAX_OVERRIDE_TYPE,
|
123
|
+
reasonline: TAX_OVERRIDE_REASON,
|
124
|
+
taxamountline: -tax,
|
125
|
+
taxdateline: taxed_at.to_date,
|
126
|
+
|
127
|
+
# Best Practice Parameters
|
128
|
+
description: REXML::Text.normalize(inventory_unit.line_item.variant.product.description.to_s[0...100]),
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|