spree_exactor 1.1.1.112920126

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 (39) hide show
  1. data/.rspec +1 -0
  2. data/Gemfile.lock +231 -0
  3. data/README.md +21 -0
  4. data/app/assets/javascripts/admin/exactor_settings.js +2 -0
  5. data/app/assets/javascripts/admin/spree_exactor.js +1 -0
  6. data/app/assets/javascripts/store/spree_exactor.js +1 -0
  7. data/app/assets/stylesheets/admin/spree_exactor.css +3 -0
  8. data/app/assets/stylesheets/exactor_settings.css +4 -0
  9. data/app/assets/stylesheets/store/spree_exactor.css +3 -0
  10. data/app/controllers/spree/admin/exactor_settings_controller.rb +34 -0
  11. data/app/helpers/exactor_api_objects.rb +387 -0
  12. data/app/helpers/exactor_api_service.rb +96 -0
  13. data/app/helpers/spree_exactor_connector.rb +380 -0
  14. data/app/models/spree/adjustment_decorator.rb +41 -0
  15. data/app/models/spree/calculator/exactor_tax_calculator.rb +87 -0
  16. data/app/models/spree/exactor_setting.rb +140 -0
  17. data/app/models/spree/line_item_decorator.rb +31 -0
  18. data/app/models/spree/order_decorator.rb +126 -0
  19. data/app/models/spree/payment_decorator.rb +15 -0
  20. data/app/models/spree/tax_category_decorator.rb +31 -0
  21. data/app/models/spree/tax_rate_decorator.rb +46 -0
  22. data/app/models/spree/user_decorator.rb +7 -0
  23. data/app/models/spree/zone_decorator.rb +31 -0
  24. data/app/overrides/spree_exactor_overrides.rb +27 -0
  25. data/app/views/spree/admin/exactor_settings/show.html.erb +113 -0
  26. data/config/locales/en.yml +10 -0
  27. data/config/routes.rb +6 -0
  28. data/db/migrate/20120624223025_create_exactor_settings.rb +20 -0
  29. data/db/migrate/20120627130223_add_exactor_info_to_order.rb +8 -0
  30. data/db/migrate/20120705103207_add_exactor_indicator_to_tax_rates.rb +5 -0
  31. data/db/migrate/20120713152512_modify_dependent_entities.rb +6 -0
  32. data/db/migrate/20120813124740_extend_user.rb +5 -0
  33. data/db/migrate/20120815104739_add_exactor_tor_inline_tax.rb +5 -0
  34. data/lib/generators/spree_exactor/install/install_generator.rb +80 -0
  35. data/lib/generators/spree_exactor/uninstall/uninstall_generator.rb +24 -0
  36. data/lib/spree_exactor/engine.rb +25 -0
  37. data/lib/spree_exactor.rb +2 -0
  38. data/spree_exactor.gemspec +66 -0
  39. metadata +197 -0
@@ -0,0 +1,96 @@
1
+ require 'exactor_api_objects'
2
+ require 'net/http'
3
+ require 'xmlsimple'
4
+
5
+ module ExactorApi
6
+ API_URI = URI('https://taxrequest.exactor.com/request/xml')
7
+
8
+ class ExactorApiService
9
+
10
+ def send_request (taxRequest)
11
+
12
+ unless taxRequest.is_a? TaxRequest
13
+ return nil
14
+ end
15
+ request = create_request(taxRequest)
16
+ request.add_field("Content-Type","application/xml")
17
+ response = Net::HTTP.start(API_URI.hostname, 80) do |http|
18
+ http.request(request)
19
+ end
20
+
21
+ case response
22
+ when Net::HTTPSuccess then
23
+ return XmlSimple.xml_in(response.body)
24
+ when Net::HTTPBadRequest then
25
+ raise ArgumentError.new(response.body)
26
+ else
27
+ return nil
28
+ end
29
+
30
+ return nil
31
+ end
32
+
33
+ private
34
+ def create_request (taxRequest)
35
+ request = Net::HTTP::Post.new(API_URI.path)
36
+ request.body= serialize_to_xml(taxRequest)
37
+ return request
38
+ end
39
+
40
+ private
41
+ def serialize_to_xml(exactor_object)
42
+ return '<?xml version="1.0" encoding="utf-8" ?>'+serialize_object(exactor_object)
43
+ end
44
+
45
+ private
46
+ def serialize_object (exactorObject, object_name=nil)
47
+ result = "";
48
+ objectName = object_name.nil? ? objectName=exactorObject.class.to_s : obtain_object_name(object_name)
49
+ unless exactorObject.is_a? ComplexExactorObject or exactorObject.is_a? Array
50
+ return create_simple_tag(exactorObject, objectName)
51
+ else
52
+ if exactorObject.is_a? Array
53
+ exactorObject.each { |object| result << serialize_object(object, objectName) }
54
+ return result
55
+ end
56
+ end
57
+ attributes = {}
58
+ if exactorObject.instance_variable_defined?(:@__xmlattr)
59
+ attributes = exactorObject.instance_variable_get(:@__xmlattr)
60
+ end
61
+ result << create_open_tag(objectName, attributes)
62
+ exactorObject.instance_variable_names.each do |var_name|
63
+ var = exactorObject.instance_variable_get(var_name)
64
+ unless (var.nil? or var_name.starts_with? "@__")
65
+ result << serialize_object(var, var_name)
66
+ end
67
+ end
68
+ result << create_close_tag(objectName)
69
+ return result
70
+ end
71
+
72
+ private
73
+ def create_simple_tag(obj, name="")
74
+ value = CGI::escapeHTML(obj.to_s)
75
+ return "<#{name}>#{value}</#{name}>"
76
+ end
77
+
78
+ private
79
+ def create_open_tag(name, attributes = {})
80
+ attr_string = ""
81
+ attributes.each {|key, value| attr_string<<" #{key}=\"#{value}\" "}
82
+ return "<#{name} #{attr_string}>"
83
+ end
84
+
85
+ private
86
+ def create_close_tag(name)
87
+ return "</#{name}>"
88
+ end
89
+
90
+ private
91
+ def obtain_object_name(name)
92
+ return name.start_with?("@") ? name.sub("@", ""):name
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,380 @@
1
+ require 'exactor_api_service'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+
5
+ module SpreeExactorConnector
6
+ class SpreeExactorConnector
7
+ PLUGIN_VERSION = "20121018"
8
+ TEST_TRANSACTION_NAME = "Spree Test Transaction"
9
+ TEST_ITEM_DESRIPTION = "Exactor Account Verification. Plug-in version:"
10
+ PLUGIN_NAME = "SpreeCommerce"
11
+ SHIPPING_EUC_NO_HANDLING = "EUC-13010204"
12
+ SHIPPING_EUC_WITH_HANDLING = "EUC-13010101"
13
+ DISCOUNT_EUC="EUC-99010102"
14
+ NON_TAXABLE_EUC="EUC-99030101"
15
+ MARKUP_EUC="EUC-99010202"
16
+
17
+
18
+ def initialize
19
+ @settings = Spree::ExactorSetting.get_settings()
20
+ end
21
+
22
+ def send_test_transaction (addressType, merchantId, userId)
23
+ exactorApi = ExactorApi::ExactorApiService.new
24
+ testRequest = TaxRequest.new
25
+ testRequest.MerchantId=merchantId
26
+ testRequest.UserId=userId
27
+ testInvoice = InvoiceRequest.new
28
+ addressType.FullName = TEST_TRANSACTION_NAME
29
+ testInvoice.ShipFrom=addressType
30
+ testInvoice.ShipTo=addressType
31
+ testInvoice.BillTo=addressType
32
+ testInvoice.SaleDate=Date.current.to_s
33
+ testLineItem = InvoiceRequest::LineItem.new(nil, "#{TEST_ITEM_DESRIPTION} #{PLUGIN_VERSION}" , 1, 0.0)
34
+ testInvoice.LineItem=testLineItem
35
+ testRequest.InvoiceRequest= testInvoice
36
+ testRequest.xmlattr_version=PLUGIN_VERSION
37
+ testRequest.xmlattr_plugin = PLUGIN_NAME
38
+ return exactorApi.send_request(testRequest)
39
+ end
40
+
41
+
42
+
43
+ def calculate_tax_for_order(order)
44
+ response = nil
45
+ if @settings.nil?
46
+ response = create_tax_response(0)
47
+ return response
48
+ end
49
+ taxRequest = create_tax_request_skeleton()
50
+ taxRequest.InvoiceRequest=InvoiceRequest.new
51
+ taxRequest.InvoiceRequest.ShipFrom=@settings.get_ship_from()
52
+ taxRequest.InvoiceRequest.LineItem=[]
53
+ map_order_to_invoice!(order, taxRequest.InvoiceRequest)
54
+ exactorApi = ExactorApi::ExactorApiService.new
55
+ return map_calculation_response(exactorApi.send_request(taxRequest))
56
+ end
57
+
58
+ def commit_transaction(order)
59
+ taxRequest = create_tax_request_skeleton()
60
+ taxRequest.CommitRequest=CommitRequest.new
61
+ taxRequest.CommitRequest.PriorTransactionId=order.exactor_invoice_transaction
62
+ taxRequest.CommitRequest.InvoiceNumber=order.number
63
+ taxRequest.CommitRequest.CommitDate=Date.current.to_s
64
+ exactorApi = ExactorApi::ExactorApiService.new
65
+ return map_commit_response(exactorApi.send_request(taxRequest))
66
+ end
67
+
68
+ def commit_refund_transaction(order)
69
+ taxRequest = create_tax_request_skeleton()
70
+ taxRequest.CommitRequest=CommitRequest.new
71
+ taxRequest.CommitRequest.CommitDate=Date.current.to_s
72
+ taxRequest.CommitRequest.InvoiceNumber=order.number
73
+ invoice = InvoiceRequest.new
74
+ invoice.ShipFrom=@settings.get_ship_from()
75
+ invoice.ShipTo=map_from_spree_address(order.ship_address)
76
+ invoice.BillTo=map_from_spree_address(order.bill_address)
77
+ invoice.SaleDate=Date.current.to_s
78
+ invoice.PurchaseOrderNumber=order.number.to_s
79
+ if order.user.anonymous?.nil?
80
+ invoice.ExemptionId=order.user.exactor_exemption_id
81
+ end
82
+ #SPECIFICATION: refund_amount = amount + [amount/(total-S&H)] * total tax.
83
+ amount=order.current_payment.amount - (order.current_payment.amount*order.tax_total)/(order.total)
84
+ amount=(-1)*amount if amount>0
85
+ invoice.LineItem=[build_refund_lineitem(amount)]
86
+ taxRequest.CommitRequest.InvoiceRequest=invoice
87
+ exactorApi = ExactorApi::ExactorApiService.new
88
+ return map_commit_response(exactorApi.send_request(taxRequest))
89
+ end
90
+
91
+ def refund_transaction(order)
92
+ taxRequest = create_tax_request_skeleton()
93
+ taxRequest.RefundRequest=RefundRequest.new
94
+ taxRequest.RefundRequest.RefundDate=Date.current.to_s
95
+ taxRequest.RefundRequest.PriorTransactionId=order.exactor_commit_transaction
96
+ exactorApi = ExactorApi::ExactorApiService.new
97
+ return map_refund_response(exactorApi.send_request(taxRequest))
98
+ end
99
+
100
+ def do_commit!(order)
101
+ response = commit_transaction(order)
102
+ if (response.is_valid)
103
+ order.update_column(:exactor_invoice_transaction,nil)
104
+ order.update_column(:exactor_commit_transaction,response.commit_id)
105
+ else
106
+ order.errors[:base]<<"Tax calculation failure: #{response.error_code}:#{response.error_description}"
107
+ end
108
+ return response
109
+ end
110
+
111
+ def do_cancel!(order)
112
+ if can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::TRANSACTION_STATE, order) or can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::SHIPMENT_STATE, order)
113
+ if !order.exactor_commit_transaction.nil?
114
+ response = refund_transaction(order)
115
+ if (response.is_valid)
116
+ order.update_column(:exactor_commit_transaction, nil)
117
+ else
118
+ order.errors[:base]<<"Tax calculation failure: #{response.error_code}:#{response.error_description}"
119
+ end
120
+ end
121
+ end
122
+ end
123
+
124
+ def do_shipped!(order)
125
+ if (can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::SHIPMENT_STATE, order))
126
+ update_tax_if_needed(order)
127
+ resp = do_commit!(order)
128
+ if (!resp.is_valid)
129
+ order.shipment.errors[:base]<<"Tax synchronization failure: #{resp.error_code}:#{resp.error_description}"
130
+ end
131
+ end
132
+ end
133
+
134
+ def do_checkout!(order)
135
+ if (can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::TRANSACTION_STATE, order))
136
+ do_commit!(order)
137
+ end
138
+ end
139
+
140
+ def do_payment_completed!(order)
141
+ if (can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::PAYMENT_STATE, order))
142
+ update_tax_if_needed(order)
143
+ resp = nil
144
+ if order.exactor_commit_transaction
145
+ resp = refund_transaction(order)
146
+ end
147
+ if (!resp or resp.is_valid)
148
+ resp = do_commit!(order)
149
+ end
150
+ if (!resp.is_valid)
151
+ order.current_payment.errors[:base]<<"Tax synchronization failure: #{resp.error_code}:#{resp.error_description}"
152
+ end
153
+ end
154
+ end
155
+
156
+ def do_refund_payment!(order)
157
+ if (can_be_handled?(Spree::ExactorSetting::COMMIT_OPTIONS::PAYMENT_STATE, order))
158
+ response=nil
159
+ if (order.current_payment.amount==order.total)
160
+ response=refund_transaction(order)
161
+ else
162
+ if (!have_non_taxable_items?(order))
163
+ response = commit_refund_transaction(order)
164
+ else
165
+ return
166
+ end
167
+ end
168
+ if (response and response.is_valid)
169
+ order.update_column(:exactor_commit_transaction, nil)
170
+ else
171
+ order.errors[:base]<<"Tax synchronization failure: #{response.error_code}:#{response.error_description}"
172
+ order.current_payment.errors[:base]<<"Tax synchronization failure: #{response.error_code}:#{response.error_description}"
173
+ end
174
+ end
175
+ end
176
+
177
+
178
+
179
+ private
180
+
181
+ def can_be_handled?(commit_type, order)
182
+ if (!@settings.nil?)
183
+ if (@settings.commit_option== commit_type)
184
+ order_date = order.completed_at.nil? ? Date.current : order.completed_at
185
+ if (order_date >= @settings.effective_date)
186
+ return true
187
+ end
188
+ end
189
+ end
190
+ return false
191
+ end
192
+
193
+ def update_tax_if_needed(order)
194
+ if order.exactor_invoice_transaction.nil?
195
+ order.update_column(:exactor_order_hash_sum, nil)
196
+ order.update_adjustments
197
+ end
198
+ end
199
+
200
+ def have_non_taxable_items?(order)
201
+ order.line_items.each do |line_item|
202
+ if (!line_item.is_exactor_taxable?)
203
+ return true
204
+ end
205
+ end
206
+ return false
207
+ end
208
+
209
+ def create_tax_response(tax, invoice_id=nil)
210
+ calc_response = CalculationResponse.new
211
+ calc_response.total_tax=tax
212
+ calc_response.invoice_id=invoice_id
213
+ return calc_response
214
+ end
215
+
216
+ def create_tax_request_skeleton()
217
+ taxRequest = TaxRequest.new
218
+ taxRequest.MerchantId=@settings.merchant_id
219
+ taxRequest.UserId=@settings.user_id
220
+ taxRequest.xmlattr_version=PLUGIN_VERSION
221
+ taxRequest.xmlattr_plugin = PLUGIN_NAME
222
+ return taxRequest
223
+ end
224
+
225
+ def map_from_spree_address(address)
226
+ exactor_address = AddressType.new
227
+ exactor_address.FullName="#{address.firstname} #{address.lastname}"
228
+ exactor_address.Street1=address.address1
229
+ exactor_address.Street2=address.address2
230
+ exactor_address.City=address.city.to_s
231
+ exactor_address.StateOrProvince=address.state_id.nil?() ?address.state_name : address.state.to_s()
232
+ exactor_address.PostalCode=address.zipcode
233
+ exactor_address.Country=address.country
234
+ return exactor_address
235
+ end
236
+
237
+ def get_sku_according_to_settings(line_item)
238
+ sku_source = @settings.sku_source
239
+ sku = "";
240
+ if (sku_source == Spree::ExactorSetting::SKU_SOURCES::SKU_FIELD)
241
+ sku = line_item.variant.sku
242
+ elsif (sku_source == Spree::ExactorSetting::SKU_SOURCES::TAX_CATEGORY)
243
+ sku = line_item.product.tax_category.name unless line_item.product.tax_category.nil?
244
+ end
245
+ return sku
246
+ end
247
+
248
+ def map_from_spree_lineitem(line_item)
249
+ exactor_line = InvoiceRequest::LineItem.new
250
+ exactor_line.SKU=get_sku_according_to_settings(line_item)
251
+ exactor_line.Description=line_item.product.description
252
+ exactor_line.Quantity=line_item.quantity
253
+ exactor_line.GrossAmount=line_item.amount()
254
+ exactor_line.xmlattr_id= "_#{line_item.id}"
255
+ return exactor_line
256
+ end
257
+
258
+ def map_order_to_invoice!(order, invoice)
259
+ invoice.ShipTo=map_from_spree_address(order.ship_address)
260
+ invoice.BillTo=map_from_spree_address(order.bill_address)
261
+ order.line_items.each {|line_item| invoice.LineItem << map_from_spree_lineitem(line_item);}
262
+ invoice.LineItem.push(add_shipping_lineitem(order.ship_total))
263
+ index=0
264
+ order.adjustments.each do |adjustment|
265
+ line_item = map_adjustment_as_lineitem(adjustment, index);
266
+ if line_item
267
+ index+=1
268
+ invoice.LineItem << line_item
269
+ end
270
+ end
271
+ invoice.SaleDate=Date.current.to_s
272
+ invoice.PurchaseOrderNumber=order.number.to_s
273
+ if order.user.anonymous?.nil?
274
+ invoice.ExemptionId=order.user.exactor_exemption_id
275
+ end
276
+ return invoice
277
+ end
278
+
279
+
280
+ def add_shipping_lineitem(shipping_amount)
281
+ exactor_line = InvoiceRequest::LineItem.new
282
+ exactor_line.SKU=@settings.shipping_include_handling ? SHIPPING_EUC_WITH_HANDLING : SHIPPING_EUC_NO_HANDLING
283
+ exactor_line.Description="Shipping"
284
+ exactor_line.Quantity=1
285
+ exactor_line.GrossAmount=shipping_amount
286
+ exactor_line.xmlattr_id="SHIPPING"
287
+ return exactor_line
288
+ end
289
+
290
+ def build_refund_lineitem(amount)
291
+ refund_line = InvoiceRequest::LineItem.new
292
+ refund_line.Description="Payment refund"
293
+ refund_line.Quantity=1
294
+ refund_line.GrossAmount=amount
295
+ refund_line.xmlattr_id="_0"
296
+ return refund_line
297
+ end
298
+
299
+ def map_adjustment_as_lineitem(adjustment, index)
300
+ #filters tax and shipping
301
+ if adjustment.originator.present? and (adjustment.originator.is_a?(Spree::TaxRate) or adjustment.originator.is_a?(Spree::ShippingMethod))
302
+ return nil
303
+ end
304
+ #filter non-eligible adjustments
305
+ return nil unless adjustment.eligible?
306
+ amount = adjustment.amount
307
+ euc=amount<0 ? DISCOUNT_EUC : MARKUP_EUC
308
+ description= "Adjustment"
309
+ if adjustment.originator.calculator.is_a?(Spree::Calculator::FreeShipping)
310
+ amount = -1*adjustment.source.ship_total
311
+ euc=@settings.shipping_include_handling ? SHIPPING_EUC_WITH_HANDLING : SHIPPING_EUC_NO_HANDLING
312
+ description="Free shipping"
313
+ end
314
+ exactor_line = InvoiceRequest::LineItem.new(euc,description,1,amount)
315
+ exactor_line.xmlattr_id="ADJUSTMENT_#{index}"
316
+ return exactor_line
317
+ end
318
+
319
+ def map_error(error_response)
320
+ #response['ErrorResponse'][0]['ErrorDescription'][0]}. (Code #{response['ErrorResponse'][0]['ErrorCode'][0]
321
+ response = ErrorResponse.new
322
+ response.error_code=error_response['ErrorResponse'][0]['ErrorCode'][0].to_i
323
+ response.error_description=error_response['ErrorResponse'][0]['ErrorDescription'][0]
324
+ response.is_valid=false
325
+ return response
326
+ end
327
+
328
+ def map_invoice(invoice)
329
+ response = CalculationResponse.new
330
+ response.is_valid=true
331
+ response.invoice_id=invoice['InvoiceResponse'][0]['TransactionId'][0]
332
+ response.total_tax=invoice['InvoiceResponse'][0]['TotalTaxAmount'][0].to_d
333
+ invoice['InvoiceResponse'][0]['LineItem'].each do |line_item|
334
+ response.add_line_item(line_item["id"], line_item["TotalTaxAmount"][0].to_d)
335
+ end
336
+ return response
337
+ end
338
+
339
+ def map_commit(commit)
340
+ response = CommitResponse.new
341
+ response.is_valid=true
342
+ response.commit_id=commit['CommitResponse'][0]['TransactionId'][0]
343
+ if commit['CommitResponse'][0].has_key?('InvoiceResponse')
344
+ response.invoice=map_invoice(commit['CommitResponse'][0])
345
+ end
346
+ return response
347
+ end
348
+
349
+ def map_refund(refund)
350
+ response = RefundResponse.new
351
+ response.is_valid=true
352
+ return response
353
+ end
354
+
355
+ def map_calculation_response(response)
356
+ if response.has_key? 'ErrorResponse'
357
+ return map_error(response)
358
+ else
359
+ return map_invoice(response)
360
+ end
361
+ end
362
+
363
+ def map_commit_response(response)
364
+ if response.has_key? 'ErrorResponse'
365
+ return map_error(response)
366
+ else
367
+ return map_commit(response)
368
+ end
369
+ end
370
+
371
+ def map_refund_response(response)
372
+ if response.has_key? 'ErrorResponse'
373
+ return map_error(response)
374
+ else
375
+ return map_refund(response)
376
+ end
377
+ end
378
+
379
+ end
380
+ end