solidus_avatax_certified 1.0.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 (165) hide show
  1. checksums.yaml +7 -0
  2. data/.env_example +4 -0
  3. data/.gitignore +16 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/GEM_VERSION +1 -0
  7. data/Gemfile +18 -0
  8. data/LICENSE +26 -0
  9. data/README.md +27 -0
  10. data/Rakefile +21 -0
  11. data/app/assets/javascripts/spree/backend/address_validator.js +84 -0
  12. data/app/assets/javascripts/spree/backend/solidus_avatax_certified.js +5 -0
  13. data/app/assets/javascripts/spree/backend/use_code_picker.js +51 -0
  14. data/app/assets/javascripts/spree/frontend/address_validator.js +87 -0
  15. data/app/assets/javascripts/spree/frontend/flash.js.coffee +26 -0
  16. data/app/assets/javascripts/spree/frontend/solidus_avatax_certified.js +4 -0
  17. data/app/assets/stylesheets/spree/backend/solidus_avatax_certified.css +4 -0
  18. data/app/assets/stylesheets/spree/frontend/solidus_avatax_certified.css +4 -0
  19. data/app/controllers/spree/admin/avalara_entity_use_codes_controller.rb +15 -0
  20. data/app/controllers/spree/admin/avatax_settings_controller.rb +82 -0
  21. data/app/controllers/spree/admin/users_controller_decorator.rb +11 -0
  22. data/app/controllers/spree/checkout_controller_decorator.rb +22 -0
  23. data/app/models/solidus_avatax_certified/address.rb +95 -0
  24. data/app/models/solidus_avatax_certified/avatax_log.rb +69 -0
  25. data/app/models/solidus_avatax_certified/line.rb +146 -0
  26. data/app/models/solidus_avatax_certified/preference_updater.rb +74 -0
  27. data/app/models/spree/address_decorator.rb +13 -0
  28. data/app/models/spree/adjustment_decorator.rb +10 -0
  29. data/app/models/spree/avalara_entity_use_code.rb +5 -0
  30. data/app/models/spree/avalara_preference.rb +72 -0
  31. data/app/models/spree/avalara_transaction.rb +181 -0
  32. data/app/models/spree/calculator/avalara_transaction.rb +86 -0
  33. data/app/models/spree/line_item_decorator.rb +25 -0
  34. data/app/models/spree/order_decorator.rb +85 -0
  35. data/app/models/spree/payment_decorator.rb +23 -0
  36. data/app/models/spree/refund_decorator.rb +30 -0
  37. data/app/models/spree/shipment_decorator.rb +28 -0
  38. data/app/models/spree/shipping_rate_decorator.rb +25 -0
  39. data/app/models/spree/user_decorator.rb +3 -0
  40. data/app/models/tax_svc.rb +143 -0
  41. data/app/overrides/spree/admin/shared/_configuration_menu.rb +10 -0
  42. data/app/overrides/spree/admin/users/sidebar.rb +8 -0
  43. data/app/overrides/spree/admin/users/validation_button.rb +6 -0
  44. data/app/views/spree/admin/avalara_entity_use_codes/_form.html.erb +27 -0
  45. data/app/views/spree/admin/avalara_entity_use_codes/edit.html.erb +16 -0
  46. data/app/views/spree/admin/avalara_entity_use_codes/index.html.erb +33 -0
  47. data/app/views/spree/admin/avalara_entity_use_codes/new.html.erb +17 -0
  48. data/app/views/spree/admin/avalara_entity_use_codes/show.html.erb +24 -0
  49. data/app/views/spree/admin/avatax_settings/edit.html.erb +183 -0
  50. data/app/views/spree/admin/avatax_settings/ping_my_service.js.erb +8 -0
  51. data/app/views/spree/admin/avatax_settings/show.html.erb +124 -0
  52. data/app/views/spree/admin/avatax_settings/validate_address.js.erb +8 -0
  53. data/app/views/spree/admin/users/avalara_information.html.erb +53 -0
  54. data/app/views/spree/checkout/_address.html.erb +35 -0
  55. data/app/views/spree/checkout/_address_validator_button.html.erb +3 -0
  56. data/bin/rails +7 -0
  57. data/circle.yml +17 -0
  58. data/config/initializers/avatax.rb +5 -0
  59. data/config/initializers/spree.rb +3 -0
  60. data/config/locales/en.yml +35 -0
  61. data/config/routes.rb +18 -0
  62. data/db/migrate/20160624154205_create_spree_avalara_transactions.rb +9 -0
  63. data/db/migrate/20160624154423_create_spree_avalara_entity_use_codes.rb +9 -0
  64. data/db/migrate/20160624155217_add_columns_to_spree_users.rb +6 -0
  65. data/db/migrate/20160628200504_create_spree_avalara_preferences.rb +10 -0
  66. data/db/migrate/20160805193512_add_vat_id_to_spree_users.rb +5 -0
  67. data/db/seeds.rb +1 -0
  68. data/lib/generators/solidus_avatax_certified/install/install_generator.rb +45 -0
  69. data/lib/solidus_avatax_certified/engine.rb +22 -0
  70. data/lib/solidus_avatax_certified/preference_seeder.rb +69 -0
  71. data/lib/solidus_avatax_certified/seeder.rb +117 -0
  72. data/lib/solidus_avatax_certified.rb +2 -0
  73. data/lib/tasks/load_seeds.rake +15 -0
  74. data/setup.md +12 -0
  75. data/solidus_avatax_certified.gemspec +42 -0
  76. data/spec/controllers/spree/admin/avalara_entity_use_codes_controller_spec.rb +53 -0
  77. data/spec/controllers/spree/admin/avatax_settings_controller_spec.rb +67 -0
  78. data/spec/factories/avalara_entity_use_code_factory.rb +6 -0
  79. data/spec/factories/avalara_factories.rb +76 -0
  80. data/spec/factories/avalara_order_factory.rb +62 -0
  81. data/spec/factories/avalara_preference_factory.rb +7 -0
  82. data/spec/factories/avalara_shipment_factory.rb +27 -0
  83. data/spec/factories/avalara_shipping_method_factory.rb +23 -0
  84. data/spec/factories/tax_rate_factory.rb +19 -0
  85. data/spec/features/certification_spec.rb +77 -0
  86. data/spec/features/checkout_spec.rb +156 -0
  87. data/spec/features/vat_spec.rb +153 -0
  88. data/spec/models/solidus_avatax_certified/address_spec.rb +80 -0
  89. data/spec/models/solidus_avatax_certified/avatax_log_spec.rb +97 -0
  90. data/spec/models/solidus_avatax_certified/line_spec.rb +100 -0
  91. data/spec/models/spree/address_decorator_spec.rb +44 -0
  92. data/spec/models/spree/adjustment_decorator_spec.rb +25 -0
  93. data/spec/models/spree/avalara_entity_use_code_spec.rb +5 -0
  94. data/spec/models/spree/avalara_preference_spec.rb +173 -0
  95. data/spec/models/spree/avalara_transaction_spec.rb +203 -0
  96. data/spec/models/spree/calculator/avalara_transaction_spec.rb +129 -0
  97. data/spec/models/spree/line_item_decorator_spec.rb +33 -0
  98. data/spec/models/spree/order_decorator_spec.rb +164 -0
  99. data/spec/models/spree/payment_decorator_spec.rb +117 -0
  100. data/spec/models/spree/refund_decorator_spec.rb +81 -0
  101. data/spec/models/spree/shipment_decorator_spec.rb +36 -0
  102. data/spec/models/spree/shipping_rate_decorator_spec.rb +17 -0
  103. data/spec/models/spree/user_decorator_spec.rb +5 -0
  104. data/spec/spec_helper.rb +81 -0
  105. data/spec/support/avatax_config_preferences.rb +8 -0
  106. data/spec/support/vcr.rb +9 -0
  107. data/spec/vcr/Certification/Exempt_sales_should_be_reflected_in_the_test_data_through_use_of_ExemptionNo_or_CustomerUsageType_/does_not_add_additional_tax.yml +276 -0
  108. data/spec/vcr/Certification/Transactions_have_been_committed_/commits_an_order.yml +336 -0
  109. data/spec/vcr/Certification/Transactions_have_been_voided/cancelled_/should_be_successful/returns_ResultCode_with_value_Success.yml +522 -0
  110. data/spec/vcr/Certification/return_orders/_commit_avatax_final/should_commit_avatax_final.yml +404 -0
  111. data/spec/vcr/Checkout/complete_order/has_tax_and_shipping_tax_adjustments_on_page.yml +72 -0
  112. data/spec/vcr/Checkout/complete_order/order_line_items_and_shipments_have_an_additional_tax_total_sum_of_0_40.yml +72 -0
  113. data/spec/vcr/Checkout/payment/tax_included/has_tax_and_shipping_tax_adjustments_on_page.yml +72 -0
  114. data/spec/vcr/Checkout/payment/tax_included/order_line_items_and_shipments_have_an_included_tax_total_sum_of_0_38.yml +72 -0
  115. data/spec/vcr/Checkout/payment/tax_not_included/on_payment_page/has_tax_and_shipping_tax_adjustments_on_page.yml +72 -0
  116. data/spec/vcr/Checkout/payment/tax_not_included/on_payment_page/order_line_items_and_shipments_and_shipments_have_an_additional_tax_total_sum_of_0_40.yml +72 -0
  117. data/spec/vcr/Checkout/payment/with_promotion/tax_not_included/has_adjusted_tax_amount_after_promotion_applied.yml +73 -0
  118. data/spec/vcr/SolidusAvataxCertified_Address/_validate/fails_when_information_is_incorrect.yml +54 -0
  119. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_included_in_price/should_be_equal_to_the_item_pre-tax_total_rate.yml +137 -0
  120. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_included_in_price/should_be_equal_to_the_previous_included_tax_total_is_order_is_at_address.yml +138 -0
  121. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_included_in_price/should_be_equal_to_the_previous_included_tax_total_is_order_is_at_cart.yml +137 -0
  122. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/should_be_equal_to_the_item_pre-tax_total_rate.yml +136 -0
  123. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/should_be_equal_to_the_previous_additional_tax_total_if_order_is_at_address.yml +137 -0
  124. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/should_be_equal_to_the_previous_additional_tax_total_if_order_is_at_cart.yml +136 -0
  125. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/should_be_equal_to_the_previous_tax_total_if_preference_tax_calculation_is_false.yml +136 -0
  126. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/when_the_line_item_is_discounted/should_be_equal_to_the_items_pre-tax_total_rate.yml +276 -0
  127. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_computing_a_line_item/when_tax_is_not_included_in_price/when_the_order_is_discounted/should_be_equal_to_the_items_pre-tax_total_rate.yml +206 -0
  128. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_given_a_shipment/computing_normal_shipment/should_be_equal_4_0.yml +141 -0
  129. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_given_a_shipment/when_tax_is_included_in_price/should_be_equal_to_3_85.yml +143 -0
  130. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_given_a_shipment/with_promotion/should_equal_3_96.yml +210 -0
  131. data/spec/vcr/Spree_Calculator_AvalaraTransaction/_compute/when_given_an_order/when_computing_an_order/should_raise_error.yml +136 -0
  132. data/spec/vcr/Spree_Order/_cancel_avalara/should_receive_cancel_order_when_cancel_avalara_is_called.yml +136 -0
  133. data/spec/vcr/Spree_Order/_cancel_avalara/state_machine_event_cancel/should_recieve_cancel_avalara_when_event_cancel_is_called.yml +259 -0
  134. data/spec/vcr/Spree_Order/_validate_ship_address/should_return_false_if_validation_failed.yml +54 -0
  135. data/spec/vcr/Spree_Order/_validate_ship_address/should_return_the_response_if_refuse_checkout_on_address_validation_is_disabled.yml +59 -0
  136. data/spec/vcr/Spree_Order/_validate_ship_address/should_return_the_response_if_validation_is_success.yml +59 -0
  137. data/spec/vcr/Spree_Payment/_avalara_finalize/should_update_the_amount_to_be_the_order_total.yml +136 -0
  138. data/spec/vcr/Spree_Payment/_cancel_avalara/committed_order/should_be_successful/ResultCode_returns_Success.yml +205 -0
  139. data/spec/vcr/Spree_Payment/_cancel_avalara/uncommitted_order/should_fail/ResultCode_returns_Error.yml +191 -0
  140. data/spec/vcr/Spree_Payment/_purchase_/receive_avalara_finalize.yml +136 -0
  141. data/spec/vcr/Spree_Payment/_void_transaction_/receive_cancel_avalara.yml +136 -0
  142. data/spec/vcr/Spree_Refund/full_refund/returns_correct_tax_calculations.yml +270 -0
  143. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_does_not_have_Nexus_Jurisdition_Registered/TotalTax_is_equal_to_order_included_tax_total.yml +137 -0
  144. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_does_not_have_Nexus_Jurisdition_Registered/origin_country_tax_is_returned.yml +138 -0
  145. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_does_not_have_Nexus_Jurisdition_Registered/with_BusinessIdentificationNo/origin_country_zero_rate_is_returned.yml +143 -0
  146. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_has_Nexus_Jurisdition_Registered/TotalTax_is_equal_to_order_included_tax_total.yml +139 -0
  147. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_has_Nexus_Jurisdition_Registered/destination_country_tax_is_returned.yml +139 -0
  148. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_another_EU_country/Seller_has_Nexus_Jurisdition_Registered/with_BusinessIdentificationNo/origin_country_zero_rate_is_returned.yml +144 -0
  149. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_same_EU_country/TotalTax_is_equal_to_order_included_tax_total.yml +137 -0
  150. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_in_same_EU_country/tax_detail_country_equals_to_IT.yml +138 -0
  151. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_does_not_have_Nexus_Jurisdition_registered/TotalTax_is_equal_to_order_included_tax_total.yml +137 -0
  152. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_does_not_have_Nexus_Jurisdition_registered/tax_detail_country_equals_to_IT.yml +137 -0
  153. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_does_not_have_Nexus_Jurisdition_registered/total_tax_is_0_since_utah_is_not_in_jurisdiction.yml +137 -0
  154. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_does_not_have_Nexus_Jurisdition_registered/with_BusinessIdentificationNo/origin_country_zero_rate_is_returned.yml +138 -0
  155. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_has_Nexus_Jurisdiction_Registered/TotalTax_is_equal_to_order_included_tax_total.yml +195 -0
  156. data/spec/vcr/VAT/Seller_in_EU_country_Buyer_is_outside_EU/Seller_has_Nexus_Jurisdiction_Registered/tax_detail_region_equals_to_AL.yml +195 -0
  157. data/spec/vcr/address_validation_success.yml +59 -0
  158. data/spec/vcr/capture_with_customer_usage_type.yml +72 -0
  159. data/spec/vcr/order_cancel.yml +258 -0
  160. data/spec/vcr/order_capture.yml +72 -0
  161. data/spec/vcr/order_capture_finalize.yml +141 -0
  162. data/spec/vcr/order_capture_with_promo.yml +73 -0
  163. data/spec/vcr/order_return_capture.yml +68 -0
  164. data/spec/vcr/tax_included_order.yml +73 -0
  165. metadata +581 -0
@@ -0,0 +1,69 @@
1
+ module SolidusAvataxCertified
2
+ class AvataxLog
3
+ def initialize(path_name, file_name, log_info = nil, schedule = nil)
4
+ if !Spree::AvalaraPreference.log_to_stdout.is_true?
5
+ schedule = 'weekly' unless schedule != nil
6
+ @logger ||= Logger.new("#{Rails.root}/log/#{path_name}.log", schedule)
7
+ progname(file_name.split('/').last.chomp('.rb'))
8
+ info(log_info) unless log_info.nil?
9
+ else
10
+ log_info = "-#{file_name} #{log_info}"
11
+ @logger ||= Logger.new(STDOUT)
12
+ end
13
+ end
14
+
15
+ def logger
16
+ @logger
17
+ end
18
+
19
+ def enabled?
20
+ Spree::AvalaraPreference.log.is_true? || Spree::AvalaraPreference.log_to_stdout.is_true?
21
+ end
22
+
23
+ def progname(progname = nil)
24
+ if enabled?
25
+ progname.nil? ? logger.progname : logger.progname = progname
26
+ end
27
+ end
28
+
29
+ def info(log_info = nil)
30
+ if enabled?
31
+ unless log_info.nil?
32
+ logger.info "[AVATAX] #{log_info}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def info_and_debug(log_info, response)
38
+ if enabled?
39
+ logger.info "[AVATAX] #{log_info}"
40
+ if response.is_a?(Hash)
41
+ logger.debug "[AVATAX] #{JSON.generate(response)}"
42
+ else
43
+ logger.debug "[AVATAX] #{response}"
44
+ end
45
+ end
46
+ end
47
+
48
+
49
+ def debug(obj, text = nil)
50
+ if enabled?
51
+ logger.debug "[AVATAX] #{obj.inspect}"
52
+ if text.nil?
53
+ obj
54
+ else
55
+ logger.debug "[AVATAX] text"
56
+ text
57
+ end
58
+ end
59
+ end
60
+
61
+ def error(log_info = nil)
62
+ if enabled?
63
+ unless log_info.nil?
64
+ logger.error "[AVATAX] #{log_info}"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,146 @@
1
+ module SolidusAvataxCertified
2
+ class Line
3
+ attr_reader :order, :lines
4
+
5
+ def initialize(order, invoice_type, refund = nil)
6
+ @order = order
7
+ @invoice_type = invoice_type
8
+ @lines = []
9
+ @refund = refund
10
+ @refunds = []
11
+ build_lines
12
+ end
13
+
14
+ def build_lines
15
+ if %w(ReturnInvoice ReturnOrder).include?(@invoice_type)
16
+ refund_lines
17
+ else
18
+ item_lines_array
19
+ shipment_lines_array
20
+ end
21
+
22
+ logger.debug @lines
23
+ end
24
+
25
+ def item_line(line_item)
26
+ {
27
+ LineNo: "#{line_item.id}-LI",
28
+ Description: line_item.name[0..255],
29
+ TaxCode: line_item.tax_category.try(:tax_code) || '',
30
+ ItemCode: line_item.variant.sku,
31
+ Qty: line_item.quantity,
32
+ Amount: line_item.amount.to_f,
33
+ OriginCode: get_stock_location(line_item),
34
+ DestinationCode: 'Dest',
35
+ CustomerUsageType: order.customer_usage_type,
36
+ Discounted: discounted?(line_item),
37
+ TaxIncluded: tax_included_in_price?(line_item)
38
+ }
39
+ end
40
+
41
+ def item_lines_array
42
+ order.line_items.each do |line_item|
43
+ lines << item_line(line_item)
44
+ end
45
+ end
46
+
47
+ def shipment_lines_array
48
+ order.shipments.each do |shipment|
49
+ next unless shipment.tax_category
50
+ lines << shipment_line(shipment)
51
+ end
52
+ end
53
+
54
+ def shipment_line(shipment)
55
+ {
56
+ LineNo: "#{shipment.id}-FR",
57
+ ItemCode: shipment.shipping_method.name,
58
+ Qty: 1,
59
+ Amount: shipment.discounted_amount.to_f,
60
+ OriginCode: "#{shipment.stock_location_id}",
61
+ DestinationCode: 'Dest',
62
+ CustomerUsageType: order.customer_usage_type,
63
+ Description: 'Shipping Charge',
64
+ TaxCode: shipment.shipping_method_tax_code,
65
+ Discounted: false,
66
+ TaxIncluded: tax_included_in_price?(shipment)
67
+ }
68
+ end
69
+
70
+ def refund_lines
71
+ return lines << refund_line if @refund.reimbursement.nil?
72
+
73
+ return_items = @refund.reimbursement.customer_return.return_items
74
+ inventory_units = Spree::InventoryUnit.where(id: return_items.pluck(:inventory_unit_id))
75
+
76
+ inventory_units.group_by(&:line_item_id).each_value do |inv_unit|
77
+
78
+ inv_unit_ids = inv_unit.map { |iu| iu.id }
79
+ return_items = Spree::ReturnItem.where(inventory_unit_id: inv_unit_ids)
80
+ quantity = inv_unit.uniq.count
81
+
82
+ amount = if return_items.first.respond_to?(:amount)
83
+ return_items.sum(:amount)
84
+ else
85
+ return_items.sum(:pre_tax_amount)
86
+ end
87
+
88
+ lines << return_item_line(inv_unit.first.line_item, quantity, amount)
89
+ end
90
+ end
91
+
92
+ def refund_line
93
+ {
94
+ LineNo: "#{@refund.id}-RA",
95
+ ItemCode: @refund.transaction_id || 'Refund',
96
+ Qty: 1,
97
+ Amount: -@refund.amount.to_f,
98
+ OriginCode: 'Orig',
99
+ DestinationCode: 'Dest',
100
+ CustomerUsageType: order.customer_usage_type,
101
+ Description: 'Refund',
102
+ TaxIncluded: true
103
+ }
104
+ end
105
+
106
+ def return_item_line(line_item, quantity, amount)
107
+ {
108
+ LineNo: "#{line_item.id}-LI",
109
+ Description: line_item.name[0..255],
110
+ TaxCode: line_item.tax_category.try(:tax_code) || '',
111
+ ItemCode: line_item.variant.sku,
112
+ Qty: quantity,
113
+ Amount: -amount.to_f,
114
+ OriginCode: get_stock_location(line_item),
115
+ DestinationCode: 'Dest',
116
+ CustomerUsageType: order.customer_usage_type
117
+ }
118
+ end
119
+
120
+ def get_stock_location(li)
121
+ inventory_units = li.inventory_units
122
+
123
+ return 'Orig' if inventory_units.blank?
124
+
125
+ stock_loc_id = inventory_units.first.try(:shipment).try(:stock_location_id)
126
+
127
+ stock_loc_id.nil? ? 'Orig' : "#{stock_loc_id}"
128
+ end
129
+
130
+ private
131
+
132
+ def discounted?(line_item)
133
+ line_item.adjustments.promotion.eligible.any? || order.adjustments.promotion.eligible.any?
134
+ end
135
+
136
+ def logger
137
+ @logger ||= SolidusAvataxCertified::AvataxLog.new('avalara_order_lines', 'SolidusAvataxCertified::Line', "Building Lines for Order#: #{order.number}")
138
+ end
139
+
140
+ def tax_included_in_price?(item)
141
+ # Need better error handling
142
+ # if no tax rates, raise error tax rates needs to be set up
143
+ order.tax_zone.tax_rates.where(tax_category: item.tax_category).try(:first).included_in_price
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,74 @@
1
+ module SolidusAvataxCertified
2
+ class PreferenceUpdater
3
+
4
+ def initialize(params)
5
+ @avatax_origin = params[:address]
6
+ @avatax_preferences = params[:settings]
7
+ end
8
+
9
+ def update
10
+ update_validation_enabled_countries
11
+ update_origin_address
12
+ update_stored_preferences
13
+ update_boolean_preferences
14
+ end
15
+
16
+ private
17
+
18
+ def update_stored_preferences
19
+ Spree::AvalaraPreference.storable_envs.each do |preference|
20
+ if !ENV["AVATAX_#{preference.name.upcase}"].blank?
21
+ update_value(preference, ENV["AVATAX_#{preference.name.upcase}"])
22
+ else
23
+ update_value(preference, @avatax_preferences[preference.name.downcase])
24
+ end
25
+ end
26
+ end
27
+
28
+ def update_boolean_preferences
29
+ Spree::AvalaraPreference.booleans.each do |boolean|
30
+ if !@avatax_preferences[boolean.name].nil?
31
+ update_value(boolean, 'true')
32
+ else
33
+ update_value(boolean, 'false')
34
+ end
35
+ end
36
+ end
37
+
38
+ def update_origin_address
39
+ set_region
40
+ set_country
41
+ update_value(Spree::AvalaraPreference.origin_address, @avatax_origin.to_json)
42
+ end
43
+
44
+ def update_validation_enabled_countries
45
+ if @avatax_preferences['address_validation_enabled_countries'].present?
46
+ update_value(Spree::AvalaraPreference.validation_enabled_countries, @avatax_preferences['address_validation_enabled_countries'].join(','))
47
+ end
48
+ end
49
+
50
+ def update_value(preference, param)
51
+ if value_changed?(preference, param)
52
+ preference.update_attributes(value: param)
53
+ end
54
+ end
55
+
56
+ def value_changed?(preference, param)
57
+ preference.value != param
58
+ end
59
+
60
+ def set_region
61
+ region = @avatax_origin['Region']
62
+ unless region.blank?
63
+ @avatax_origin['Region'] = Spree::State.find(region).try(:abbr)
64
+ end
65
+ end
66
+
67
+ def set_country
68
+ country = @avatax_origin['Country']
69
+ unless country.blank?
70
+ @avatax_origin['Country'] = Spree::Country.find(country).try(:iso)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,13 @@
1
+ Spree::Address.class_eval do
2
+ def validation_enabled?
3
+ Spree::AvalaraPreference.address_validation.is_true? && country_validation_enabled?
4
+ end
5
+
6
+ def country_validation_enabled?
7
+ Spree::Address.validation_enabled_countries.include?(country.try(:name))
8
+ end
9
+
10
+ def self.validation_enabled_countries
11
+ Spree::AvalaraPreference.validation_enabled_countries_array
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+ Spree::Adjustment.class_eval do
2
+ scope :not_tax, -> { where.not(source_type: 'Spree::TaxRate') }
3
+
4
+ def avatax_cache_key
5
+ key = ['Spree::Adjustment']
6
+ key << self.id
7
+ key << self.amount
8
+ key.join('-')
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Spree
2
+ class AvalaraEntityUseCode < Spree::Base
3
+ has_many :users
4
+ end
5
+ end
@@ -0,0 +1,72 @@
1
+ module Spree
2
+ class AvalaraPreference < Spree::Base
3
+ validates_uniqueness_of :name
4
+ validates_presence_of :name, :object_type
5
+
6
+ scope :booleans, -> { where(object_type: 'boolean') }
7
+ scope :strings, -> { where(object_type: 'string') }
8
+ scope :arrays, -> { where(object_type: 'array') }
9
+ scope :jsons, -> { where(object_type: 'json') }
10
+ scope :storable_envs, -> { where(name: ['company_code', 'license_key', 'account', 'endpoint']) }
11
+
12
+ def self.company_code
13
+ find_by(name: 'company_code')
14
+ end
15
+
16
+ def self.license_key
17
+ find_by(name: 'license_key')
18
+ end
19
+
20
+ def self.account
21
+ find_by(name: 'account')
22
+ end
23
+
24
+ def self.endpoint
25
+ find_by(name: 'endpoint')
26
+ end
27
+
28
+ def self.origin_address
29
+ find_by(name: 'origin_address')
30
+ end
31
+
32
+ def self.log
33
+ find_by(name: 'log')
34
+ end
35
+
36
+ def self.log_to_stdout
37
+ find_by(name: 'log_to_stdout')
38
+ end
39
+
40
+ def self.address_validation
41
+ find_by(name: 'address_validation')
42
+ end
43
+
44
+ def self.refuse_checkout_address_validation_error
45
+ find_by(name: 'refuse_checkout_address_validation_error')
46
+ end
47
+
48
+ def self.tax_calculation
49
+ find_by(name: 'tax_calculation')
50
+ end
51
+
52
+ def self.document_commit
53
+ find_by(name: 'document_commit')
54
+ end
55
+
56
+ def self.validation_enabled_countries
57
+ find_by(name: 'address_validation_enabled_countries')
58
+ end
59
+
60
+ def self.validation_enabled_countries_array
61
+ find_by(name: 'address_validation_enabled_countries').value.split(',')
62
+ end
63
+
64
+ def self.customer_can_validate
65
+ find_by(name: 'customer_can_validate')
66
+ end
67
+
68
+ def is_true?
69
+ [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].include?(value)
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,181 @@
1
+ require 'logging'
2
+ require_dependency 'spree/order'
3
+
4
+ module Spree
5
+ class AvalaraTransaction < Spree::Base
6
+ belongs_to :order
7
+ validates :order, presence: true
8
+ validates :order_id, uniqueness: true
9
+ # has_many :adjustments, as: :source
10
+
11
+ def lookup_avatax
12
+ post_order_to_avalara(false, 'SalesOrder')
13
+ end
14
+
15
+ def commit_avatax(invoice_dt = nil, refund = nil)
16
+ if tax_calculation_enabled?
17
+ if %w(ReturnInvoice ReturnOrder).include?(invoice_dt)
18
+ post_return_to_avalara(false, invoice_dt, refund)
19
+ else
20
+ post_order_to_avalara(false, invoice_dt)
21
+ end
22
+ else
23
+ { TotalTax: '0.00' }
24
+ end
25
+ end
26
+
27
+ def commit_avatax_final(invoice_dt = nil, refund = nil)
28
+ if document_committing_enabled?
29
+ if tax_calculation_enabled?
30
+ if %w(ReturnInvoice ReturnOrder).include?(invoice_dt)
31
+ post_return_to_avalara(true, invoice_dt, refund)
32
+ else
33
+ post_order_to_avalara(true, invoice_dt)
34
+ end
35
+ else
36
+ { TotalTax: '0.00' }
37
+ end
38
+ else
39
+ logger.debug 'avalara document committing disabled'
40
+ 'avalara document committing disabled'
41
+ end
42
+ end
43
+
44
+ def cancel_order
45
+ cancel_order_to_avalara('SalesInvoice') if tax_calculation_enabled?
46
+ end
47
+
48
+ private
49
+
50
+ def cancel_order_to_avalara(doc_type = 'SalesInvoice')
51
+ logger.info('cancel order to avalara')
52
+
53
+ cancel_tax_request = {
54
+ CompanyCode: Spree::AvalaraPreference.company_code.value,
55
+ DocType: doc_type,
56
+ DocCode: order.number,
57
+ CancelCode: 'DocVoided'
58
+ }
59
+
60
+ mytax = TaxSvc.new
61
+ cancel_tax_result = mytax.cancel_tax(cancel_tax_request)
62
+
63
+ logger.debug cancel_tax_result
64
+
65
+ if cancel_tax_result == 'Error in Cancel Tax'
66
+ return 'Error in Cancel Tax'
67
+ else
68
+ return cancel_tax_result
69
+ end
70
+ end
71
+
72
+ def post_order_to_avalara(commit = false, invoice_detail = nil)
73
+ logger.info('post order to avalara')
74
+ avatax_address = SolidusAvataxCertified::Address.new(order)
75
+ avatax_line = SolidusAvataxCertified::Line.new(order, invoice_detail)
76
+
77
+ doc_date = order.completed? ? order.completed_at.strftime('%F') : Date.today.strftime('%F')
78
+
79
+ gettaxes = {
80
+ DocCode: order.number,
81
+ DocDate: doc_date,
82
+ Discount: order.all_adjustments.promotion.eligible.sum(:amount).abs.to_s,
83
+ Commit: commit,
84
+ DocType: invoice_detail ? invoice_detail : 'SalesOrder',
85
+ Addresses: avatax_address.addresses,
86
+ Lines: avatax_line.lines
87
+ }.merge(base_tax_hash)
88
+
89
+ if !business_id_no.blank?
90
+ gettaxes[:BusinessIdentificationNo] = business_id_no
91
+ end
92
+
93
+ logger.debug gettaxes
94
+
95
+ mytax = TaxSvc.new
96
+
97
+ tax_result = mytax.get_tax(gettaxes)
98
+
99
+ logger.info_and_debug('tax result', tax_result)
100
+
101
+ return { TotalTax: '0.00' } if tax_result == 'error in Tax'
102
+ return tax_result if tax_result['ResultCode'] == 'Success'
103
+ end
104
+
105
+ def post_return_to_avalara(commit = false, invoice_detail = nil, refund = nil)
106
+ logger.info('starting post return order to avalara')
107
+
108
+ avatax_address = SolidusAvataxCertified::Address.new(order)
109
+ avatax_line = SolidusAvataxCertified::Line.new(order, invoice_detail, refund)
110
+
111
+ taxoverride = {
112
+ TaxOverrideType: 'TaxDate',
113
+ Reason: refund.try(:reason).try(:name).limit(255) || 'Return',
114
+ TaxDate: order.completed_at.strftime('%F')
115
+ }
116
+
117
+ gettaxes = {
118
+ DocCode: order.number.to_s + '.' + refund.id.to_s,
119
+ DocDate: Date.today.strftime('%F'),
120
+ Commit: commit,
121
+ DocType: invoice_detail ? invoice_detail : 'ReturnOrder',
122
+ Addresses: avatax_address.addresses,
123
+ Lines: avatax_line.lines
124
+ }.merge(base_tax_hash)
125
+
126
+ if !business_id_no.blank?
127
+ gettaxes[:BusinessIdentificationNo] = business_id_no
128
+ end
129
+
130
+ gettaxes[:TaxOverride] = taxoverride
131
+
132
+ logger.debug gettaxes
133
+
134
+ mytax = TaxSvc.new
135
+
136
+ tax_result = mytax.get_tax(gettaxes)
137
+
138
+ logger.info_and_debug('tax result', tax_result)
139
+
140
+ return { TotalTax: '0.00' } if tax_result == 'error in Tax'
141
+ return tax_result if tax_result['ResultCode'] == 'Success'
142
+ end
143
+
144
+ def base_tax_hash
145
+ {
146
+ CustomerCode: customer_code,
147
+ CompanyCode: Spree::AvalaraPreference.company_code.value,
148
+ CustomerUsageType: order.customer_usage_type,
149
+ ExemptionNo: order.user.try(:exemption_number),
150
+ Client: avatax_client_version,
151
+ ReferenceCode: order.number,
152
+ DetailLevel: 'Tax',
153
+ CurrencyCode: order.currency
154
+ }
155
+ end
156
+
157
+ def customer_code
158
+ order.user ? order.user.id : order.email
159
+ end
160
+
161
+ def business_id_no
162
+ order.user.try(:vat_id)
163
+ end
164
+
165
+ def avatax_client_version
166
+ AVATAX_CLIENT_VERSION || 'a0o33000004FH8l'
167
+ end
168
+
169
+ def document_committing_enabled?
170
+ Spree::AvalaraPreference.document_commit.is_true?
171
+ end
172
+
173
+ def tax_calculation_enabled?
174
+ Spree::AvalaraPreference.tax_calculation.is_true?
175
+ end
176
+
177
+ def logger
178
+ @logger ||= SolidusAvataxCertified::AvataxLog.new('post_order_to_avalara', __FILE__)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,86 @@
1
+ module Spree
2
+ class Calculator::AvalaraTransaction < Calculator::DefaultTax
3
+ def self.description
4
+ Spree.t(:avalara_transaction_calculator)
5
+ end
6
+
7
+ def compute_order(order)
8
+ raise 'Spree::AvalaraTransaction is designed to calculate taxes at the shipment and line-item levels.'
9
+ end
10
+
11
+ def compute_shipment_or_line_item(item)
12
+ order = item.order
13
+ item_address = order.ship_address || order.billing_address
14
+ prev_tax_amount = prev_tax_amount(item)
15
+
16
+ return prev_tax_amount unless Spree::AvalaraPreference.tax_calculation.is_true?
17
+ return prev_tax_amount if %w(address cart).include?(order.state)
18
+ return prev_tax_amount if item_address.nil?
19
+ return prev_tax_amount unless calculable.zone.include?(item_address)
20
+
21
+ avalara_response = get_avalara_response(order)
22
+ tax_for_item(item, avalara_response)
23
+ end
24
+
25
+ alias_method :compute_shipment, :compute_shipment_or_line_item
26
+ alias_method :compute_line_item, :compute_shipment_or_line_item
27
+
28
+ def compute_shipping_rate(shipping_rate)
29
+ return 0
30
+ end
31
+
32
+ private
33
+
34
+ def prev_tax_amount(item)
35
+ if rate.included_in_price
36
+ item.included_tax_total
37
+ else
38
+ item.additional_tax_total
39
+ end
40
+ end
41
+
42
+ def get_avalara_response(order)
43
+ Rails.cache.fetch(cache_key(order), time_to_idle: 5.minutes) do
44
+ order.avalara_capture
45
+ end
46
+ end
47
+
48
+
49
+ def long_cache_key(order)
50
+ key = order.avatax_cache_key
51
+ key << (order.ship_address.try(:cache_key) || order.bill_address.try(:cache_key)).to_s
52
+ order.line_items.each do |line_item|
53
+ key << line_item.avatax_cache_key
54
+ end
55
+ order.shipments.each do |shipment|
56
+ key << shipment.avatax_cache_key
57
+ end
58
+ order.all_adjustments.not_tax do |adj|
59
+ key << adj.avatax_cache_key
60
+ end
61
+ key
62
+ end
63
+
64
+ # long keys blow up in dev with the default ActiveSupport::Cache::FileStore
65
+ # This transparently shrinks 'em
66
+ def cache_key(order)
67
+ long_key = long_cache_key(order)
68
+ short_key = Digest::SHA1.hexdigest(long_key)
69
+ "avtx_#{short_key}"
70
+ end
71
+
72
+ def tax_for_item(item, avalara_response)
73
+ prev_tax_amount = prev_tax_amount(item)
74
+
75
+ return prev_tax_amount if avalara_response.nil?
76
+ return 0 if avalara_response[:TotalTax] == '0.00'
77
+
78
+ avalara_response['TaxLines'].each do |line|
79
+ if line['LineNo'] == "#{item.id}-#{item.avatax_line_code}"
80
+ return line['TaxCalculated'].to_f
81
+ end
82
+ end
83
+ 0
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,25 @@
1
+ Spree::LineItem.class_eval do
2
+ def to_hash
3
+ {
4
+ 'Index' => id,
5
+ 'Name' => name,
6
+ 'ItemID' => sku,
7
+ 'Price' => price.to_s,
8
+ 'Qty' => quantity,
9
+ 'TaxCategory' => tax_category
10
+ }
11
+ end
12
+
13
+ def avatax_cache_key
14
+ key = ['Spree::LineItem']
15
+ key << self.id
16
+ key << self.quantity
17
+ key << self.price
18
+ key << self.promo_total
19
+ key.join('-')
20
+ end
21
+
22
+ def avatax_line_code
23
+ 'LI'
24
+ end
25
+ end