solidus_avatax 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|