solidus_avatax 0.2.0

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