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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +1 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +15 -0
  6. data/LICENSE +26 -0
  7. data/README.md +71 -0
  8. data/Rakefile +21 -0
  9. data/app/assets/javascripts/spree/frontend/solidus_avatax.js +1 -0
  10. data/app/assets/stylesheets/spree/frontend/solidus_avatax.css +1 -0
  11. data/app/models/spree/adjustment_decorator.rb +21 -0
  12. data/app/models/spree/order_contents_decorator.rb +26 -0
  13. data/app/models/spree/order_decorator.rb +43 -0
  14. data/app/models/spree/promotion_handler/coupon_decorator.rb +11 -0
  15. data/app/models/spree/reimbursement_decorator.rb +1 -0
  16. data/app/models/spree/tax_rate_decorator.rb +45 -0
  17. data/app/models/spree_avatax/calculator.rb +26 -0
  18. data/app/models/spree_avatax/return_invoice.rb +197 -0
  19. data/app/models/spree_avatax/sales_invoice.rb +157 -0
  20. data/app/models/spree_avatax/sales_shared.rb +211 -0
  21. data/app/models/spree_avatax/shared.rb +53 -0
  22. data/app/models/spree_avatax/short_ship_return_invoice.rb +133 -0
  23. data/app/models/spree_avatax/short_ship_return_invoice_inventory_unit.rb +11 -0
  24. data/bin/rails +7 -0
  25. data/circle.yml +6 -0
  26. data/config/locales/en.yml +5 -0
  27. data/db/migrate/20140122165618_add_avatax_response_at_to_orders.rb +5 -0
  28. data/db/migrate/20140214153139_add_avatax_invoice_at_to_orders.rb +5 -0
  29. data/db/migrate/20140617222244_close_all_tax_adjustments.rb +5 -0
  30. data/db/migrate/20140701144237_update_avatax_calculator_type.rb +17 -0
  31. data/db/migrate/20140801132302_create_spree_avatax_return_invoices.rb +19 -0
  32. data/db/migrate/20140903135132_create_spree_avatax_sales_orders.rb +15 -0
  33. data/db/migrate/20140903135357_create_spree_avatax_sales_invoices.rb +19 -0
  34. data/db/migrate/20140904171341_generate_uncommitted_sales_invoices.rb +31 -0
  35. data/db/migrate/20140911214414_add_transaction_id_to_sales_orders_and_sales_invoices.rb +6 -0
  36. data/db/migrate/20140911215422_add_canceled_at_and_cancel_transaction_id_to_sales_invoices.rb +6 -0
  37. data/db/migrate/20150427154942_create_spree_avatax_short_ship_return_invoices.rb +44 -0
  38. data/db/migrate/20150518172627_fix_avatax_short_ship_index.rb +30 -0
  39. data/lib/generators/solidus_avatax/install/install_generator.rb +31 -0
  40. data/lib/generators/solidus_avatax/install/templates/config/initializers/avatax.rb +18 -0
  41. data/lib/solidus_avatax.rb +4 -0
  42. data/lib/spree_avatax/config.rb +16 -0
  43. data/lib/spree_avatax/engine.rb +29 -0
  44. data/lib/spree_avatax/factories.rb +25 -0
  45. data/lib/tasks/commit_backfill.rake +119 -0
  46. data/lib/tasks/sales_invoice_backfill.rake +43 -0
  47. data/solidus_avatax.gemspec +34 -0
  48. data/spec/features/store_credits_spec.rb +92 -0
  49. data/spec/features/tax_calculation_spec.rb +75 -0
  50. data/spec/fixtures/vcr_cassettes/sales_invoice_gettax_with_discounts.yml +136 -0
  51. data/spec/fixtures/vcr_cassettes/sales_invoice_gettax_without_discounts.yml +136 -0
  52. data/spec/fixtures/vcr_cassettes/taxes_with_store_credits.yml +113 -0
  53. data/spec/models/spree/adjustment_spec.rb +23 -0
  54. data/spec/models/spree/order_contents_spec.rb +35 -0
  55. data/spec/models/spree/order_spec.rb +111 -0
  56. data/spec/models/spree/shipping_rate_spec.rb +23 -0
  57. data/spec/models/spree/tax_rate_spec.rb +40 -0
  58. data/spec/models/spree_avatax/calculator.rb +20 -0
  59. data/spec/models/spree_avatax/return_invoice_spec.rb +193 -0
  60. data/spec/models/spree_avatax/sales_invoice_spec.rb +491 -0
  61. data/spec/models/spree_avatax/sales_shared_spec.rb +47 -0
  62. data/spec/models/spree_avatax/shared_spec.rb +89 -0
  63. data/spec/models/spree_avatax/short_ship_return_invoice_spec.rb +181 -0
  64. data/spec/spec_helper.rb +85 -0
  65. data/spec/support/return_invoice_soap_responses.rb +117 -0
  66. data/spec/support/sales_invoice_soap_responses.rb +259 -0
  67. data/spec/support/short_ship_return_invoice_soap_responses.rb +178 -0
  68. data/spec/support/zone_support.rb +6 -0
  69. metadata +320 -0
@@ -0,0 +1,491 @@
1
+ require 'spec_helper'
2
+
3
+ describe SpreeAvatax::SalesInvoice do
4
+ describe '.generate' do
5
+ subject do
6
+ SpreeAvatax::SalesInvoice.generate(order)
7
+ end
8
+
9
+ let(:order) do
10
+ create(:order_with_line_items,
11
+ line_items_count: 1,
12
+ ship_address: create(:address, {
13
+ address1: "1234 Way",
14
+ address2: "",
15
+ city: "New York",
16
+ state: state,
17
+ country: country,
18
+ zipcode: "10010",
19
+ phone: "1111111111",
20
+ }))
21
+ end
22
+
23
+ let(:line_item) { order.line_items.first }
24
+ let(:shipment) { order.shipments.first }
25
+
26
+ let(:country) { create(:country) }
27
+ let(:state) { create(:state, country: country, name: 'New York', abbr: 'NY') }
28
+
29
+ let(:expected_gettax_params) do
30
+ {
31
+ doccode: order.number,
32
+ customercode: order.email,
33
+ companycode: SpreeAvatax::Config.company_code,
34
+
35
+ doctype: SpreeAvatax::SalesInvoice::DOC_TYPE,
36
+ docdate: Date.today,
37
+
38
+ commit: false,
39
+
40
+ discount: order.avatax_order_adjustment_total.round(2).to_f,
41
+
42
+ addresses: [
43
+ {
44
+ addresscode: SpreeAvatax::SalesShared::DESTINATION_CODE,
45
+ line1: REXML::Text.normalize(order.ship_address.address1),
46
+ line2: REXML::Text.normalize(order.ship_address.address2),
47
+ city: REXML::Text.normalize(order.ship_address.city),
48
+ postalcode: REXML::Text.normalize(order.ship_address.zipcode),
49
+ },
50
+ ],
51
+
52
+ lines: [
53
+ { # line item
54
+ no: "Spree::LineItem-#{line_item.id}",
55
+ qty: line_item.quantity,
56
+ amount: line_item.discounted_amount.round(2).to_f,
57
+ origincodeline: SpreeAvatax::SalesShared::DESTINATION_CODE,
58
+ destinationcodeline: SpreeAvatax::SalesShared::DESTINATION_CODE,
59
+
60
+ description: expected_truncated_description,
61
+
62
+ itemcode: line_item.variant.sku,
63
+ taxcode: line_item.tax_category.tax_code,
64
+ discounted: true,
65
+ },
66
+ { # shipping charge
67
+ no: "Spree::Shipment-#{shipment.id}",
68
+ qty: 1,
69
+ amount: shipment.discounted_amount.round(2).to_f,
70
+ origincodeline: SpreeAvatax::SalesShared::DESTINATION_CODE,
71
+ destinationcodeline: SpreeAvatax::SalesShared::DESTINATION_CODE,
72
+
73
+ description: SpreeAvatax::SalesShared::SHIPPING_DESCRIPTION,
74
+
75
+ taxcode: SpreeAvatax::SalesShared::SHIPPING_TAX_CODE,
76
+ discounted: false,
77
+ },
78
+ ],
79
+ }
80
+ end
81
+
82
+ let(:expected_truncated_description) { line_item.variant.product.description.truncate(100) }
83
+ let(:gettax_response) { sales_invoice_gettax_response(order.number, line_item, shipment) }
84
+ let(:gettax_response_line_item_tax_line) { Array.wrap(gettax_response[:tax_lines][:tax_line]).first }
85
+ let(:gettax_response_shipment_tax_line) { Array.wrap(gettax_response[:tax_lines][:tax_line]).last }
86
+ let(:order_calculated_tax) do
87
+ BigDecimal.new(gettax_response[:total_tax])
88
+ end
89
+ let(:line_item_calculated_tax) do
90
+ BigDecimal.new(gettax_response_line_item_tax_line[:tax]).abs
91
+ end
92
+ let(:shipment_calculated_tax) do
93
+ BigDecimal.new(gettax_response_shipment_tax_line[:tax]).abs
94
+ end
95
+
96
+ let!(:gettax_stub) do
97
+ expect(SpreeAvatax::Shared.tax_svc)
98
+ .to receive(:gettax)
99
+ .with(expected_gettax_params)
100
+ .and_return(gettax_response)
101
+ end
102
+
103
+ it 'creates a sales invoice' do
104
+ expect {
105
+ subject
106
+ }.to change { SpreeAvatax::SalesInvoice.count }.by(1)
107
+ expect(order.avatax_sales_invoice).to eq SpreeAvatax::SalesInvoice.last
108
+ expect(order.avatax_sales_invoice.attributes).to include({
109
+ "transaction_id" => gettax_response[:transaction_id],
110
+ "doc_id" => gettax_response[:doc_id],
111
+ "doc_code" => gettax_response[:doc_code],
112
+ "doc_date" => gettax_response[:doc_date],
113
+ "pre_tax_total" => BigDecimal.new(gettax_response[:total_amount]),
114
+ "additional_tax_total" => BigDecimal.new(gettax_response[:total_tax]),
115
+ })
116
+ end
117
+
118
+ it 'persists the results to the order' do
119
+ expect {
120
+ subject
121
+ }.to change { order.reload.additional_tax_total }.from(0).to(order_calculated_tax)
122
+ end
123
+
124
+ it 'persists the results to the line items' do
125
+ expect {
126
+ subject
127
+ }.to change { line_item.reload.additional_tax_total }.from(0).to(line_item_calculated_tax)
128
+ end
129
+
130
+ it 'persists the results to the shipments' do
131
+ expect {
132
+ subject
133
+ }.to change { shipment.reload.additional_tax_total }.from(0).to(shipment_calculated_tax)
134
+ end
135
+
136
+ it "creates a line item adjustment" do
137
+ subject
138
+ expect(line_item.adjustments.tax.count).to eq 1
139
+ adjustment = line_item.adjustments.first
140
+ expect(adjustment.amount).to eq line_item_calculated_tax
141
+ expect(adjustment.source).to eq Spree::TaxRate.first
142
+ expect(adjustment.finalized).to eq true
143
+ end
144
+
145
+ it "creates a shipment adjustment" do
146
+ subject
147
+ expect(shipment.adjustments.tax.count).to eq 1
148
+ adjustment = shipment.adjustments.first
149
+ expect(adjustment.amount).to eq shipment_calculated_tax
150
+ expect(adjustment.source).to eq Spree::TaxRate.first
151
+ expect(adjustment.finalized).to eq true
152
+ end
153
+
154
+ context 'user input contains XML characters' do
155
+ let(:line1) { "<&line1>" }
156
+ let(:line2) { "<&line2>" }
157
+ let(:city) { "<&city>" }
158
+ let(:email) { "test&@test.com" }
159
+ let(:description) { "A description <wi>&/th xml characters" }
160
+
161
+ before(:each) do
162
+ ship_address = order.ship_address
163
+ ship_address.update_columns(address1: line1, address2: line2, city: city)
164
+ order.update_columns(email: email)
165
+ line_item.variant.product.update_columns(description: description)
166
+ end
167
+
168
+ let(:expected_gettax_params) do
169
+ super().tap do |params|
170
+ params[:addresses].first.merge!(line1: REXML::Text.normalize(line1), line2: REXML::Text.normalize(line2), city: REXML::Text.normalize(city))
171
+ params[:customercode] = REXML::Text.normalize(email)
172
+ params[:lines][0][:description] = REXML::Text.normalize(description)
173
+ end
174
+ end
175
+
176
+ it 'succeeds' do
177
+ subject
178
+ end
179
+ end
180
+
181
+ context 'when an error occurs' do
182
+ let(:error) { StandardError.new('just testing') }
183
+ let!(:gettax_stub) { }
184
+ let(:order) do
185
+ create(:order_with_line_items,
186
+ line_items_count: 2,
187
+ ship_address: create(:address, {
188
+ address1: "1234 Way",
189
+ address2: "",
190
+ city: "New York",
191
+ state: state,
192
+ country: country,
193
+ zipcode: "10010",
194
+ phone: "1111111111",
195
+ }))
196
+ end
197
+
198
+ before do
199
+ expect(SpreeAvatax::SalesShared)
200
+ .to receive(:get_tax)
201
+ .and_raise(error)
202
+
203
+ order.line_items.update_all(pre_tax_amount: nil)
204
+ order.reload
205
+ end
206
+
207
+ it 'sets the pre_tax_amount on each line item in the order' do
208
+ expect{ subject }.to raise_error(error)
209
+ expect(order.line_items.first.pre_tax_amount.to_f).to equal(order.line_items.first.discounted_amount.to_f)
210
+ expect(order.line_items.last.pre_tax_amount.to_f).to equal(order.line_items.last.discounted_amount.to_f)
211
+ end
212
+
213
+ it 'sets the pre_tax_amount on each shipment in the order' do
214
+ expect{ subject }.to raise_error(error)
215
+ expect(order.shipments.first.pre_tax_amount.to_f).to equal(order.shipments.first.discounted_amount.to_f)
216
+ end
217
+
218
+ context 'when an error_handler is not defined' do
219
+ it 'calls the handler instead of raising the original error' do
220
+ expect {
221
+ subject
222
+ }.to raise_error(error)
223
+ end
224
+ end
225
+
226
+ context 'when an error_handler is defined' do
227
+ let(:handler) { -> (o, e) { raise new_error } }
228
+ let(:new_error) { StandardError.new('just testing 2') }
229
+
230
+ before do
231
+ allow(SpreeAvatax::Config).to receive_messages(sales_invoice_generate_error_handler: handler)
232
+ end
233
+
234
+ it 'calls the handler instead of raising the original error' do
235
+ expect {
236
+ subject
237
+ }.to raise_error(new_error)
238
+ end
239
+ end
240
+ end
241
+
242
+ context 'when the response for a line item is missing' do
243
+ before do
244
+ gettax_response_line_item_tax_line[:no] = (line_item.id + 1).to_s
245
+ end
246
+
247
+ it 'raises InvalidApiResponse' do
248
+ expect {
249
+ subject
250
+ }.to raise_error(SpreeAvatax::SalesShared::InvalidApiResponse)
251
+ end
252
+ end
253
+
254
+ context 'when an invoice already exists' do
255
+ context 'when the existing invoice is not committed' do
256
+ let!(:previous_sales_invoice) { create(:avatax_sales_invoice, order: order) }
257
+
258
+ it 'deletes the previous invoice' do
259
+ subject
260
+ expect(SpreeAvatax::SalesInvoice.find_by(id: previous_sales_invoice.id)).to be_nil
261
+ end
262
+ end
263
+
264
+ context 'when the existing invoice is committed' do
265
+ let!(:previous_sales_invoice) { create(:avatax_sales_invoice, order: order, committed_at: 1.day.ago) }
266
+
267
+ it 'raises an AlreadyCommittedError' do
268
+ expect {
269
+ subject
270
+ }.to raise_error(SpreeAvatax::SalesInvoice::AlreadyCommittedError)
271
+ end
272
+ end
273
+ end
274
+
275
+ describe 'when the description is too long' do
276
+ let(:description) { 'a'*1000 }
277
+ let(:expected_truncated_description) { description.truncate(100) }
278
+
279
+ before do
280
+ line_item.variant.product.update!(description: description)
281
+ end
282
+
283
+ it 'succeeds' do
284
+ subject # method expectation will fail if date isn't right
285
+ end
286
+ end
287
+
288
+ context 'when the order is not taxable' do
289
+ let(:order) { create(:order_with_line_items, ship_address: nil, line_items_count: 1) }
290
+
291
+ let!(:gettax_stub) { }
292
+
293
+ it 'does not create a sales invoice' do
294
+ expect {
295
+ subject
296
+ }.not_to change { SpreeAvatax::SalesInvoice.count }
297
+ expect(order.avatax_sales_invoice).to eq nil
298
+ end
299
+
300
+ it 'does not call avatax' do
301
+ expect(SpreeAvatax::Shared.tax_svc).to receive(:gettax).never
302
+ subject
303
+ end
304
+ end
305
+
306
+ context 'when the order is already completed' do
307
+ let(:order) { create(:completed_order_with_totals) }
308
+
309
+ let!(:gettax_stub) { }
310
+
311
+ it 'does not create a sales invoice' do
312
+ expect {
313
+ subject
314
+ }.not_to change { SpreeAvatax::SalesInvoice.count }
315
+ expect(order.avatax_sales_invoice).to eq nil
316
+ end
317
+
318
+ it 'does not call avatax' do
319
+ expect(SpreeAvatax::Shared.tax_svc).to receive(:gettax).never
320
+ subject
321
+ end
322
+ end
323
+ end
324
+
325
+ describe '.commit' do
326
+ subject do
327
+ SpreeAvatax::SalesInvoice.commit(order)
328
+ end
329
+
330
+ let!(:order) { sales_invoice.order }
331
+ let(:sales_invoice) { create(:avatax_sales_invoice) }
332
+
333
+ let(:expected_posttax_params) do
334
+ {
335
+ doccode: sales_invoice.doc_code,
336
+ companycode: SpreeAvatax::Config.company_code,
337
+
338
+ doctype: SpreeAvatax::SalesInvoice::DOC_TYPE,
339
+ docdate: sales_invoice.doc_date,
340
+
341
+ commit: true,
342
+
343
+ totalamount: sales_invoice.pre_tax_total,
344
+ totaltax: sales_invoice.additional_tax_total,
345
+ }
346
+ end
347
+
348
+ context 'when the order is taxable' do
349
+ let!(:posttax_stub) do
350
+ expect(SpreeAvatax::Shared.tax_svc)
351
+ .to receive(:posttax)
352
+ .with(expected_posttax_params)
353
+ .and_return(
354
+ sales_invoice_posttax_response
355
+ )
356
+ end
357
+
358
+ it 'marks the sales invoice as committed' do
359
+ expect {
360
+ subject
361
+ }.to change { sales_invoice.reload.committed_at? }.from(false).to(true)
362
+ end
363
+ end
364
+
365
+ context 'when the order is not taxable' do
366
+ before do
367
+ expect(SpreeAvatax::Shared).to receive(:taxable_order?).with(sales_invoice.order).and_return(false)
368
+ end
369
+
370
+ it 'does not call avatax' do
371
+ expect(SpreeAvatax::Shared.tax_svc).to receive(:posttax).never
372
+ subject
373
+ end
374
+ end
375
+
376
+ context 'when the sales_invoice does not exist' do
377
+ let(:sales_invoice) { nil }
378
+ let(:order) { create(:shipped_order, line_items_count: 1) }
379
+
380
+ it 'raises a SalesInvoiceNotFound error' do
381
+ expect {
382
+ subject
383
+ }.to raise_error(SpreeAvatax::SalesInvoice::CommitInvoiceNotFound)
384
+ end
385
+ end
386
+
387
+ context 'when an error occurs' do
388
+ let(:error) { StandardError.new('just testing') }
389
+ let!(:posttax_stub) { }
390
+
391
+ before do
392
+ expect(SpreeAvatax::SalesInvoice)
393
+ .to receive(:post_tax)
394
+ .and_raise(error)
395
+ end
396
+
397
+ context 'when an error_handler is not defined' do
398
+ it 'raises the original error' do
399
+ expect {
400
+ subject
401
+ }.to raise_error(error)
402
+ end
403
+ end
404
+
405
+ context 'when an error_handler is defined' do
406
+ let(:handler) { -> (o, e) { raise new_error } }
407
+ let(:new_error) { StandardError.new('just testing 2') }
408
+
409
+ before do
410
+ allow(SpreeAvatax::Config).to receive_messages(sales_invoice_commit_error_handler: handler)
411
+ end
412
+
413
+ it 'calls the handler instead of raising the original error' do
414
+ expect {
415
+ subject
416
+ }.to raise_error(new_error)
417
+ end
418
+ end
419
+ end
420
+ end
421
+
422
+ describe '.cancel' do
423
+ subject do
424
+ SpreeAvatax::SalesInvoice.cancel(order)
425
+ end
426
+
427
+ context 'when the sales invoice exists' do
428
+ let(:order) { sales_invoice.order }
429
+ let(:sales_invoice) { create(:avatax_sales_invoice, committed_at: Time.now) }
430
+
431
+ let(:expected_canceltax_params) do
432
+ {
433
+ doccode: sales_invoice.doc_code,
434
+ doctype: SpreeAvatax::SalesInvoice::DOC_TYPE,
435
+ cancelcode: SpreeAvatax::SalesInvoice::CANCEL_CODE,
436
+ companycode: SpreeAvatax::Config.company_code,
437
+ }
438
+ end
439
+
440
+ let(:canceltax_response) { sales_invoice_canceltax_response }
441
+
442
+ let!(:canceltax_stub) do
443
+ expect(SpreeAvatax::Shared.tax_svc)
444
+ .to receive(:canceltax)
445
+ .with(expected_canceltax_params)
446
+ .and_return(canceltax_response)
447
+ end
448
+
449
+ it 'should update the sales invoice' do
450
+ expect {
451
+ subject
452
+ }.to change { sales_invoice.canceled_at }.from(nil)
453
+ expect(sales_invoice.cancel_transaction_id).to eq canceltax_response[:transaction_id]
454
+ end
455
+
456
+ context 'when an error occurs' do
457
+ let(:error) { StandardError.new('just testing') }
458
+ let!(:canceltax_stub) { }
459
+
460
+ before do
461
+ expect(SpreeAvatax::SalesInvoice)
462
+ .to receive(:cancel_tax)
463
+ .and_raise(error)
464
+ end
465
+
466
+ context 'when an error_handler is not defined' do
467
+ it 'raises the original error' do
468
+ expect {
469
+ subject
470
+ }.to raise_error(error)
471
+ end
472
+ end
473
+
474
+ context 'when an error_handler is defined' do
475
+ let(:handler) { -> (o, e) { raise new_error } }
476
+ let(:new_error) { StandardError.new('just testing 2') }
477
+
478
+ before do
479
+ allow(SpreeAvatax::Config).to receive_messages(sales_invoice_cancel_error_handler: handler)
480
+ end
481
+
482
+ it 'calls the handler instead of raising the original error' do
483
+ expect {
484
+ subject
485
+ }.to raise_error(new_error)
486
+ end
487
+ end
488
+ end
489
+ end
490
+ end
491
+ end