activemerchant 1.121.0 → 1.125.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +217 -0
  3. data/README.md +1 -1
  4. data/lib/active_merchant/billing/check.rb +13 -19
  5. data/lib/active_merchant/billing/credit_card.rb +13 -0
  6. data/lib/active_merchant/billing/credit_card_formatting.rb +1 -0
  7. data/lib/active_merchant/billing/credit_card_methods.rb +24 -12
  8. data/lib/active_merchant/billing/gateway.rb +1 -1
  9. data/lib/active_merchant/billing/gateways/adyen.rb +75 -27
  10. data/lib/active_merchant/billing/gateways/authorize_net.rb +10 -8
  11. data/lib/active_merchant/billing/gateways/blue_pay.rb +29 -0
  12. data/lib/active_merchant/billing/gateways/blue_snap.rb +2 -2
  13. data/lib/active_merchant/billing/gateways/braintree_blue.rb +6 -3
  14. data/lib/active_merchant/billing/gateways/card_stream.rb +17 -13
  15. data/lib/active_merchant/billing/gateways/cashnet.rb +15 -5
  16. data/lib/active_merchant/billing/gateways/checkout_v2.rb +33 -4
  17. data/lib/active_merchant/billing/gateways/credorax.rb +2 -1
  18. data/lib/active_merchant/billing/gateways/cyber_source.rb +41 -6
  19. data/lib/active_merchant/billing/gateways/d_local.rb +12 -6
  20. data/lib/active_merchant/billing/gateways/decidir.rb +7 -1
  21. data/lib/active_merchant/billing/gateways/decidir_plus.rb +173 -0
  22. data/lib/active_merchant/billing/gateways/ebanx.rb +16 -1
  23. data/lib/active_merchant/billing/gateways/elavon.rb +65 -30
  24. data/lib/active_merchant/billing/gateways/element.rb +22 -2
  25. data/lib/active_merchant/billing/gateways/global_collect.rb +130 -26
  26. data/lib/active_merchant/billing/gateways/ipg.rb +416 -0
  27. data/lib/active_merchant/billing/gateways/kushki.rb +30 -0
  28. data/lib/active_merchant/billing/gateways/mercado_pago.rb +6 -3
  29. data/lib/active_merchant/billing/gateways/merchant_warrior.rb +2 -0
  30. data/lib/active_merchant/billing/gateways/mit.rb +260 -0
  31. data/lib/active_merchant/billing/gateways/moka.rb +290 -0
  32. data/lib/active_merchant/billing/gateways/monei.rb +228 -144
  33. data/lib/active_merchant/billing/gateways/mundipagg.rb +22 -11
  34. data/lib/active_merchant/billing/gateways/nmi.rb +29 -10
  35. data/lib/active_merchant/billing/gateways/orbital.rb +46 -8
  36. data/lib/active_merchant/billing/gateways/pay_arc.rb +392 -0
  37. data/lib/active_merchant/billing/gateways/pay_conex.rb +3 -1
  38. data/lib/active_merchant/billing/gateways/pay_trace.rb +404 -0
  39. data/lib/active_merchant/billing/gateways/payeezy.rb +4 -0
  40. data/lib/active_merchant/billing/gateways/payflow/payflow_common_api.rb +1 -0
  41. data/lib/active_merchant/billing/gateways/payflow.rb +21 -4
  42. data/lib/active_merchant/billing/gateways/payment_express.rb +2 -2
  43. data/lib/active_merchant/billing/gateways/paymentez.rb +14 -2
  44. data/lib/active_merchant/billing/gateways/paysafe.rb +412 -0
  45. data/lib/active_merchant/billing/gateways/payu_latam.rb +9 -4
  46. data/lib/active_merchant/billing/gateways/payway_dot_com.rb +3 -3
  47. data/lib/active_merchant/billing/gateways/pin.rb +31 -4
  48. data/lib/active_merchant/billing/gateways/priority.rb +347 -0
  49. data/lib/active_merchant/billing/gateways/realex.rb +18 -0
  50. data/lib/active_merchant/billing/gateways/redsys.rb +35 -32
  51. data/lib/active_merchant/billing/gateways/safe_charge.rb +8 -2
  52. data/lib/active_merchant/billing/gateways/spreedly_core.rb +13 -4
  53. data/lib/active_merchant/billing/gateways/stripe.rb +27 -7
  54. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +115 -39
  55. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +2 -1
  56. data/lib/active_merchant/billing/gateways/trust_commerce.rb +2 -1
  57. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +21 -7
  58. data/lib/active_merchant/billing/gateways/vpos.rb +49 -6
  59. data/lib/active_merchant/billing/gateways/wompi.rb +193 -0
  60. data/lib/active_merchant/billing/gateways/worldpay.rb +226 -62
  61. data/lib/active_merchant/billing/network_tokenization_credit_card.rb +1 -1
  62. data/lib/active_merchant/billing/response.rb +4 -0
  63. data/lib/active_merchant/billing/three_d_secure_eci_mapper.rb +27 -0
  64. data/lib/active_merchant/billing.rb +1 -0
  65. data/lib/active_merchant/version.rb +1 -1
  66. metadata +13 -3
@@ -153,6 +153,10 @@ module ActiveMerchant #:nodoc:
153
153
  commit(build_refund_request(money, identification, options), :refund, money, options)
154
154
  end
155
155
 
156
+ def adjust(money, authorization, options = {})
157
+ commit(build_adjust_request(money, authorization, options), :adjust, money, options)
158
+ end
159
+
156
160
  def verify(payment, options = {})
157
161
  MultiResponse.run(:use_first_response) do |r|
158
162
  r.process { authorize(100, payment, options) }
@@ -285,6 +289,7 @@ module ActiveMerchant #:nodoc:
285
289
 
286
290
  def build_auth_request(money, creditcard_or_reference, options)
287
291
  xml = Builder::XmlMarkup.new indent: 2
292
+ add_customer_id(xml, options)
288
293
  add_payment_method_or_subscription(xml, money, creditcard_or_reference, options)
289
294
  add_threeds_2_ucaf_data(xml, creditcard_or_reference, options)
290
295
  add_decision_manager_fields(xml, options)
@@ -300,7 +305,15 @@ module ActiveMerchant #:nodoc:
300
305
  add_merchant_description(xml, options)
301
306
  add_sales_slip_number(xml, options)
302
307
  add_airline_data(xml, options)
308
+ xml.target!
309
+ end
303
310
 
311
+ def build_adjust_request(money, authorization, options)
312
+ _, request_id = authorization.split(';')
313
+
314
+ xml = Builder::XmlMarkup.new indent: 2
315
+ add_purchase_data(xml, money, true, options)
316
+ add_incremental_auth_service(xml, request_id, options)
304
317
  xml.target!
305
318
  end
306
319
 
@@ -334,6 +347,7 @@ module ActiveMerchant #:nodoc:
334
347
 
335
348
  def build_purchase_request(money, payment_method_or_reference, options)
336
349
  xml = Builder::XmlMarkup.new indent: 2
350
+ add_customer_id(xml, options)
337
351
  add_payment_method_or_subscription(xml, money, payment_method_or_reference, options)
338
352
  add_threeds_2_ucaf_data(xml, payment_method_or_reference, options)
339
353
  add_decision_manager_fields(xml, options)
@@ -472,8 +486,8 @@ module ActiveMerchant #:nodoc:
472
486
 
473
487
  unless network_tokenization?(payment_method)
474
488
  xml.tag! 'businessRules' do
475
- xml.tag!('ignoreAVSResult', 'true') if extract_option(prioritized_options, :ignore_avs)
476
- xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv)
489
+ xml.tag!('ignoreAVSResult', 'true') if extract_option(prioritized_options, :ignore_avs).to_s == 'true'
490
+ xml.tag!('ignoreCVResult', 'true') if extract_option(prioritized_options, :ignore_cvv).to_s == 'true'
477
491
  end
478
492
  end
479
493
  end
@@ -486,6 +500,8 @@ module ActiveMerchant #:nodoc:
486
500
  end
487
501
 
488
502
  def add_line_item_data(xml, options)
503
+ return unless options[:line_items]
504
+
489
505
  options[:line_items].each_with_index do |value, index|
490
506
  xml.tag! 'item', { 'id' => index } do
491
507
  xml.tag! 'unitPrice', localized_amount(value[:declared_value].to_i, options[:currency] || default_currency)
@@ -493,6 +509,8 @@ module ActiveMerchant #:nodoc:
493
509
  xml.tag! 'productCode', value[:code] || 'shipping_only'
494
510
  xml.tag! 'productName', value[:description]
495
511
  xml.tag! 'productSKU', value[:sku]
512
+ xml.tag! 'taxAmount', value[:tax_amount] if value[:tax_amount]
513
+ xml.tag! 'nationalTax', value[:national_tax] if value[:national_tax]
496
514
  end
497
515
  end
498
516
  end
@@ -508,13 +526,21 @@ module ActiveMerchant #:nodoc:
508
526
  end
509
527
 
510
528
  def add_merchant_descriptor(xml, options)
511
- return unless options[:merchant_descriptor]
529
+ return unless options[:merchant_descriptor] || options[:user_po] || options[:taxable]
512
530
 
513
531
  xml.tag! 'invoiceHeader' do
514
- xml.tag! 'merchantDescriptor', options[:merchant_descriptor]
532
+ xml.tag! 'merchantDescriptor', options[:merchant_descriptor] if options[:merchant_descriptor]
533
+ xml.tag! 'userPO', options[:user_po] if options[:user_po]
534
+ xml.tag! 'taxable', options[:taxable] if options[:taxable]
515
535
  end
516
536
  end
517
537
 
538
+ def add_customer_id(xml, options)
539
+ return unless options[:customer_id]
540
+
541
+ xml.tag! 'customerID', options[:customer_id]
542
+ end
543
+
518
544
  def add_merchant_description(xml, options)
519
545
  return unless options[:merchant_descriptor_name] || options[:merchant_descriptor_address1] || options[:merchant_descriptor_locality]
520
546
 
@@ -585,7 +611,7 @@ module ActiveMerchant #:nodoc:
585
611
  xml.tag! 'accountNumber', creditcard.number
586
612
  xml.tag! 'expirationMonth', format(creditcard.month, :two_digits)
587
613
  xml.tag! 'expirationYear', format(creditcard.year, :four_digits)
588
- xml.tag!('cvNumber', creditcard.verification_value) unless @options[:ignore_cvv] || creditcard.verification_value.blank?
614
+ xml.tag!('cvNumber', creditcard.verification_value) unless @options[:ignore_cvv].to_s == 'true' || creditcard.verification_value.blank?
589
615
  xml.tag! 'cardType', @@credit_card_codes[card_brand(creditcard).to_sym]
590
616
  end
591
617
  end
@@ -608,11 +634,12 @@ module ActiveMerchant #:nodoc:
608
634
  end
609
635
 
610
636
  def add_other_tax(xml, options)
611
- return unless options[:local_tax_amount] || options[:national_tax_amount]
637
+ return unless options[:local_tax_amount] || options[:national_tax_amount] || options[:national_tax_indicator]
612
638
 
613
639
  xml.tag! 'otherTax' do
614
640
  xml.tag! 'localTaxAmount', options[:local_tax_amount] if options[:local_tax_amount]
615
641
  xml.tag! 'nationalTaxAmount', options[:national_tax_amount] if options[:national_tax_amount]
642
+ xml.tag! 'nationalTaxIndicator', options[:national_tax_indicator] if options[:national_tax_indicator]
616
643
  end
617
644
  end
618
645
 
@@ -658,6 +685,13 @@ module ActiveMerchant #:nodoc:
658
685
  end
659
686
  end
660
687
 
688
+ def add_incremental_auth_service(xml, authorization, options)
689
+ xml.tag! 'ccIncrementalAuthService', { 'run' => 'true' } do
690
+ xml.tag! 'authRequestID', authorization
691
+ end
692
+ xml.tag! 'subsequentAuthReason', options[:auth_reason]
693
+ end
694
+
661
695
  def add_normalized_threeds_2_data(xml, payment_method, options)
662
696
  threeds_2_options = options[:three_d_secure]
663
697
  cc_brand = card_brand(payment_method).to_sym
@@ -860,6 +894,7 @@ module ActiveMerchant #:nodoc:
860
894
  else
861
895
  add_address(xml, payment_method_or_reference, options[:billing_address], options)
862
896
  add_address(xml, payment_method_or_reference, options[:shipping_address], options, true)
897
+ add_line_item_data(xml, options)
863
898
  add_purchase_data(xml, money, true, options)
864
899
  add_installments(xml, options)
865
900
  add_creditcard(xml, payment_method_or_reference)
@@ -4,7 +4,7 @@ module ActiveMerchant #:nodoc:
4
4
  self.test_url = 'https://sandbox.dlocal.com'
5
5
  self.live_url = 'https://api.dlocal.com'
6
6
 
7
- self.supported_countries = %w[AR BR CL CO MX PE UY TR]
7
+ self.supported_countries = %w[AR BD BO BR CL CM CN CO CR DO EC EG GH IN ID KE MY MX MA NG PA PY PE PH SN ZA TR UY VN]
8
8
  self.default_currency = 'USD'
9
9
  self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro naranja cabal elo alia carnet]
10
10
 
@@ -26,6 +26,7 @@ module ActiveMerchant #:nodoc:
26
26
  def authorize(money, payment, options = {})
27
27
  post = {}
28
28
  add_auth_purchase_params(post, money, payment, 'authorize', options)
29
+ post[:card][:verify] = true if options[:verify].to_s == 'true'
29
30
 
30
31
  commit('authorize', post, options)
31
32
  end
@@ -52,10 +53,7 @@ module ActiveMerchant #:nodoc:
52
53
  end
53
54
 
54
55
  def verify(credit_card, options = {})
55
- MultiResponse.run(:use_first_response) do |r|
56
- r.process { authorize(100, credit_card, options) }
57
- r.process(:ignore_result) { void(r.authorization, options) }
58
- end
56
+ authorize(0, credit_card, options.merge(verify: 'true'))
59
57
  end
60
58
 
61
59
  def supports_scrubbing?
@@ -78,6 +76,7 @@ module ActiveMerchant #:nodoc:
78
76
  add_country(post, card, options)
79
77
  add_payer(post, card, options)
80
78
  add_card(post, card, action, options)
79
+ add_additional_data(post, options)
81
80
  post[:order_id] = options[:order_id] || generate_unique_id
82
81
  post[:description] = options[:description] if options[:description]
83
82
  end
@@ -87,6 +86,10 @@ module ActiveMerchant #:nodoc:
87
86
  post[:currency] = (options[:currency] || currency(money))
88
87
  end
89
88
 
89
+ def add_additional_data(post, options)
90
+ post[:additional_risk_data] = options[:additional_data]
91
+ end
92
+
90
93
  def add_country(post, card, options)
91
94
  return unless address = options[:billing_address] || options[:address]
92
95
 
@@ -109,6 +112,8 @@ module ActiveMerchant #:nodoc:
109
112
  post[:payer][:document] = options[:document] if options[:document]
110
113
  post[:payer][:document2] = options[:document2] if options[:document2]
111
114
  post[:payer][:user_reference] = options[:user_reference] if options[:user_reference]
115
+ post[:payer][:event_uuid] = options[:device_id] if options[:device_id]
116
+ post[:payer][:onboarding_ip_address] = options[:ip] if options[:ip]
112
117
  post[:payer][:address] = add_address(post, card, options)
113
118
  end
114
119
 
@@ -184,7 +189,7 @@ module ActiveMerchant #:nodoc:
184
189
  def success_from(action, response)
185
190
  return false unless response['status_code']
186
191
 
187
- %w[100 200 400 600].include? response['status_code'].to_s
192
+ %w[100 200 400 600 700].include? response['status_code'].to_s
188
193
  end
189
194
 
190
195
  def message_from(action, response)
@@ -228,6 +233,7 @@ module ActiveMerchant #:nodoc:
228
233
  'X-Date' => timestamp,
229
234
  'X-Login' => @options[:login],
230
235
  'X-Trans-Key' => @options[:trans_key],
236
+ 'X-Version' => '2.1',
231
237
  'Authorization' => signature(post, timestamp)
232
238
  }
233
239
  headers.merge('X-Idempotency-Key' => options[:idempotency_key]) if options[:idempotency_key]
@@ -282,7 +282,13 @@ module ActiveMerchant #:nodoc:
282
282
  if error = response.dig('status_details', 'error')
283
283
  message = "#{error.dig('reason', 'description')} | #{error['type']}"
284
284
  elsif response['error_type']
285
- message = response['validation_errors'].map { |errors| "#{errors['code']}: #{errors['param']}" }.join(', ') if response['validation_errors']
285
+ if response['validation_errors'].is_a?(Array)
286
+ message = response['validation_errors'].map { |errors| "#{errors['code']}: #{errors['param']}" }.join(', ')
287
+ elsif response['validation_errors'].is_a?(Hash)
288
+ errors = response['validation_errors'].map { |k, v| "#{k}: #{v}" }.join(', ')
289
+ message = "#{response['error_type']} - #{errors}"
290
+ end
291
+
286
292
  message ||= response['error_type']
287
293
  end
288
294
 
@@ -0,0 +1,173 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class DecidirPlusGateway < Gateway
4
+ self.test_url = 'https://developers.decidir.com/api/v2'
5
+ self.live_url = 'https://live.decidir.com/api/v2'
6
+
7
+ self.supported_countries = ['AR']
8
+ self.default_currency = 'ARS'
9
+ self.supported_cardtypes = %i[visa master american_express discover]
10
+
11
+ self.homepage_url = 'http://decidir.com.ar/home'
12
+ self.display_name = 'Decidir Plus'
13
+
14
+ def initialize(options = {})
15
+ requires!(options, :public_key, :private_key)
16
+ super
17
+ end
18
+
19
+ def purchase(money, payment, options = {})
20
+ post = {}
21
+
22
+ add_payment(post, payment, options)
23
+ add_purchase_data(post, money, payment, options)
24
+
25
+ commit(:post, 'payments', post)
26
+ end
27
+
28
+ def refund(money, authorization, options = {})
29
+ post = {}
30
+ post[:amount] = money
31
+
32
+ commit(:post, "payments/#{add_reference(authorization)}/refunds")
33
+ end
34
+
35
+ def store(payment, options = {})
36
+ post = {}
37
+ add_payment(post, payment, options)
38
+
39
+ commit(:post, 'tokens', post)
40
+ end
41
+
42
+ def supports_scrubbing?
43
+ true
44
+ end
45
+
46
+ def scrub(transcript)
47
+ transcript.
48
+ gsub(%r((Apikey: )\w+), '\1[FILTERED]').
49
+ gsub(%r(("card_number\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]').
50
+ gsub(%r(("security_code\\?"\s*:\s*\\?")[^"]*)i, '\1[FILTERED]')
51
+ end
52
+
53
+ private
54
+
55
+ def add_reference(authorization)
56
+ return unless authorization
57
+
58
+ authorization.split('|')[0]
59
+ end
60
+
61
+ def add_payment(post, payment, options = {})
62
+ if payment.is_a?(String)
63
+ token, bin = payment.split('|')
64
+ post[:token] = token
65
+ post[:bin] = bin
66
+ else
67
+ post[:card_number] = payment.number
68
+ post[:card_expiration_month] = payment.month.to_s.rjust(2, '0')
69
+ post[:card_expiration_year] = payment.year.to_s[-2..-1]
70
+ post[:security_code] = payment.verification_value.to_s
71
+ post[:card_holder_name] = payment.name
72
+ post[:card_holder_identification] = {}
73
+ post[:card_holder_identification][:type] = options[:dni]
74
+ post[:card_holder_identification][:number] = options[:card_holder_identification_number]
75
+ end
76
+ end
77
+
78
+ def add_purchase_data(post, money, payment, options = {})
79
+ post[:site_transaction_id] = options[:site_transaction_id] || SecureRandom.hex
80
+ post[:payment_method_id] = 1
81
+ post[:amount] = money
82
+ post[:currency] = options[:currency] || self.default_currency
83
+ post[:installments] = options[:installments] || 1
84
+ post[:payment_type] = options[:payment_type] || 'single'
85
+ add_sub_payments(post, options)
86
+ end
87
+
88
+ def add_sub_payments(post, options)
89
+ # sub_payments field is required for purchase transactions, even if empty
90
+ post[:sub_payments] = []
91
+
92
+ return unless sub_payments = options[:sub_payments]
93
+
94
+ sub_payments.each do |sub_payment|
95
+ sub_payment_hash = {
96
+ site_id: sub_payment[:site_id],
97
+ installments: sub_payment[:installments],
98
+ amount: sub_payment[:amount]
99
+ }
100
+ post[:sub_payments] << sub_payment_hash
101
+ end
102
+ end
103
+
104
+ def parse(body)
105
+ JSON.parse(body)
106
+ end
107
+
108
+ def commit(method, endpoint, parameters = {}, options = {})
109
+ begin
110
+ raw_response = ssl_request(method, url(endpoint), post_data(parameters), headers(endpoint))
111
+ response = parse(raw_response)
112
+ rescue ResponseError => e
113
+ raw_response = e.response.body
114
+ response = parse(raw_response)
115
+ end
116
+
117
+ Response.new(
118
+ success_from(response),
119
+ message_from(response),
120
+ response,
121
+ authorization: authorization_from(response),
122
+ avs_result: AVSResult.new(code: response['some_avs_response_key']),
123
+ cvv_result: CVVResult.new(response['some_cvv_response_key']),
124
+ test: test?,
125
+ error_code: error_code_from(response)
126
+ )
127
+ end
128
+
129
+ def headers(endpoint)
130
+ {
131
+ 'Content-Type' => 'application/json',
132
+ 'apikey' => endpoint == 'tokens' ? @options[:public_key] : @options[:private_key]
133
+ }
134
+ end
135
+
136
+ def url(action, options = {})
137
+ base_url = (test? ? test_url : live_url)
138
+
139
+ return "#{base_url}/#{action}"
140
+ end
141
+
142
+ def success_from(response)
143
+ response.dig('status') == 'approved' || response.dig('status') == 'active'
144
+ end
145
+
146
+ def message_from(response)
147
+ response.dig('status') || error_message(response) || response.dig('message')
148
+ end
149
+
150
+ def authorization_from(response)
151
+ return nil unless response.dig('id') || response.dig('bin')
152
+
153
+ "#{response.dig('id')}|#{response.dig('bin')}"
154
+ end
155
+
156
+ def post_data(parameters = {})
157
+ parameters.to_json
158
+ end
159
+
160
+ def error_code_from(response)
161
+ response.dig('error_type') unless success_from(response)
162
+ end
163
+
164
+ def error_message(response)
165
+ return error_code_from(response) unless validation_errors = response.dig('validation_errors')
166
+
167
+ validation_errors = validation_errors[0]
168
+
169
+ "#{validation_errors.dig('code')}: #{validation_errors.dig('param')}"
170
+ end
171
+ end
172
+ end
173
+ end
@@ -214,6 +214,7 @@ module ActiveMerchant #:nodoc:
214
214
  post[:metadata] = options[:metadata] if options[:metadata]
215
215
  post[:metadata] = {} if post[:metadata].nil?
216
216
  post[:metadata][:merchant_payment_code] = options[:order_id] if options[:order_id]
217
+ post[:processing_type] = options[:processing_type] if options[:processing_type]
217
218
  end
218
219
 
219
220
  def parse(body)
@@ -222,7 +223,8 @@ module ActiveMerchant #:nodoc:
222
223
 
223
224
  def commit(action, parameters)
224
225
  url = url_for((test? ? test_url : live_url), action, parameters)
225
- response = parse(ssl_request(HTTP_METHOD[action], url, post_data(action, parameters), { 'x-ebanx-client-user-agent': "ActiveMerchant/#{ActiveMerchant::VERSION}" }))
226
+
227
+ response = parse(ssl_request(HTTP_METHOD[action], url, post_data(action, parameters), headers(parameters)))
226
228
 
227
229
  success = success_from(action, response)
228
230
 
@@ -236,6 +238,19 @@ module ActiveMerchant #:nodoc:
236
238
  )
237
239
  end
238
240
 
241
+ def headers(params)
242
+ processing_type = params[:processing_type]
243
+ commit_headers = { 'x-ebanx-client-user-agent': "ActiveMerchant/#{ActiveMerchant::VERSION}" }
244
+
245
+ add_processing_type_to_commit_headers(commit_headers, processing_type) if processing_type == 'local'
246
+
247
+ commit_headers
248
+ end
249
+
250
+ def add_processing_type_to_commit_headers(commit_headers, processing_type)
251
+ commit_headers['x-ebanx-api-processing-type'] = processing_type
252
+ end
253
+
239
254
  def success_from(action, response)
240
255
  if %i[purchase capture refund].include?(action)
241
256
  response.try(:[], 'payment').try(:[], 'status') == 'CO'
@@ -201,8 +201,8 @@ module ActiveMerchant #:nodoc:
201
201
  private
202
202
 
203
203
  def add_invoice(xml, options)
204
- xml.ssl_invoice_number truncate((options[:order_id] || options[:invoice]), 25)
205
- xml.ssl_description truncate(options[:description], 255)
204
+ xml.ssl_invoice_number url_encode_truncate((options[:order_id] || options[:invoice]), 25)
205
+ xml.ssl_description url_encode_truncate(options[:description], 255)
206
206
  end
207
207
 
208
208
  def add_approval_code(xml, authorization)
@@ -219,8 +219,8 @@ module ActiveMerchant #:nodoc:
219
219
 
220
220
  add_verification_value(xml, creditcard) if creditcard.verification_value?
221
221
 
222
- xml.ssl_first_name truncate(creditcard.first_name, 20)
223
- xml.ssl_last_name truncate(creditcard.last_name, 30)
222
+ xml.ssl_first_name url_encode_truncate(creditcard.first_name, 20)
223
+ xml.ssl_last_name url_encode_truncate(creditcard.last_name, 30)
224
224
  end
225
225
 
226
226
  def add_currency(xml, money, options)
@@ -240,7 +240,7 @@ module ActiveMerchant #:nodoc:
240
240
  end
241
241
 
242
242
  def add_customer_email(xml, options)
243
- xml.ssl_email truncate(options[:email], 100) unless empty?(options[:email])
243
+ xml.ssl_email url_encode_truncate(options[:email], 100) unless empty?(options[:email])
244
244
  end
245
245
 
246
246
  def add_salestax(xml, options)
@@ -253,27 +253,27 @@ module ActiveMerchant #:nodoc:
253
253
  billing_address = options[:billing_address] || options[:address]
254
254
 
255
255
  if billing_address
256
- xml.ssl_avs_address truncate(billing_address[:address1], 30)
257
- xml.ssl_address2 truncate(billing_address[:address2], 30)
258
- xml.ssl_avs_zip truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9)
259
- xml.ssl_city truncate(billing_address[:city], 30)
260
- xml.ssl_state truncate(billing_address[:state], 10)
261
- xml.ssl_company truncate(billing_address[:company], 50)
262
- xml.ssl_phone truncate(billing_address[:phone], 20)
263
- xml.ssl_country truncate(billing_address[:country], 50)
256
+ xml.ssl_avs_address url_encode_truncate(billing_address[:address1], 30)
257
+ xml.ssl_address2 url_encode_truncate(billing_address[:address2], 30)
258
+ xml.ssl_avs_zip url_encode_truncate(billing_address[:zip].to_s.gsub(/[^a-zA-Z0-9]/, ''), 9)
259
+ xml.ssl_city url_encode_truncate(billing_address[:city], 30)
260
+ xml.ssl_state url_encode_truncate(billing_address[:state], 10)
261
+ xml.ssl_company url_encode_truncate(billing_address[:company], 50)
262
+ xml.ssl_phone url_encode_truncate(billing_address[:phone], 20)
263
+ xml.ssl_country url_encode_truncate(billing_address[:country], 50)
264
264
  end
265
265
 
266
266
  if shipping_address = options[:shipping_address]
267
- xml.ssl_ship_to_address1 truncate(shipping_address[:address1], 30)
268
- xml.ssl_ship_to_address2 truncate(shipping_address[:address2], 30)
269
- xml.ssl_ship_to_city truncate(shipping_address[:city], 30)
270
- xml.ssl_ship_to_company truncate(shipping_address[:company], 50)
271
- xml.ssl_ship_to_country truncate(shipping_address[:country], 50)
272
- xml.ssl_ship_to_first_name truncate(shipping_address[:first_name], 20)
273
- xml.ssl_ship_to_last_name truncate(shipping_address[:last_name], 30)
274
- xml.ssl_ship_to_phone truncate(shipping_address[:phone], 10)
275
- xml.ssl_ship_to_state truncate(shipping_address[:state], 2)
276
- xml.ssl_ship_to_zip truncate(shipping_address[:zip], 10)
267
+ xml.ssl_ship_to_address1 url_encode_truncate(shipping_address[:address1], 30)
268
+ xml.ssl_ship_to_address2 url_encode_truncate(shipping_address[:address2], 30)
269
+ xml.ssl_ship_to_city url_encode_truncate(shipping_address[:city], 30)
270
+ xml.ssl_ship_to_company url_encode_truncate(shipping_address[:company], 50)
271
+ xml.ssl_ship_to_country url_encode_truncate(shipping_address[:country], 50)
272
+ xml.ssl_ship_to_first_name url_encode_truncate(shipping_address[:first_name], 20)
273
+ xml.ssl_ship_to_last_name url_encode_truncate(shipping_address[:last_name], 30)
274
+ xml.ssl_ship_to_phone url_encode_truncate(shipping_address[:phone], 10)
275
+ xml.ssl_ship_to_state url_encode_truncate(shipping_address[:state], 2)
276
+ xml.ssl_ship_to_zip url_encode_truncate(shipping_address[:zip], 10)
277
277
  end
278
278
  end
279
279
 
@@ -293,9 +293,12 @@ module ActiveMerchant #:nodoc:
293
293
  xml.ssl_cardholder_ip options[:ip] if options.has_key?(:ip)
294
294
  end
295
295
 
296
+ # add_recurring_token is a field that can be sent in to obtain a token from Elavon for use with their tokenization program
296
297
  def add_auth_purchase_params(xml, options)
297
298
  xml.ssl_dynamic_dba options[:dba] if options.has_key?(:dba)
298
299
  xml.ssl_merchant_initiated_unscheduled merchant_initiated_unscheduled(options) if merchant_initiated_unscheduled(options)
300
+ xml.ssl_add_token options[:add_recurring_token] if options.has_key?(:add_recurring_token)
301
+ xml.ssl_token options[:ssl_token] if options[:ssl_token]
299
302
  xml.ssl_customer_code options[:customer] if options.has_key?(:customer)
300
303
  xml.ssl_customer_number options[:customer_number] if options.has_key?(:customer_number)
301
304
  xml.ssl_entry_mode entry_mode(options) if entry_mode(options)
@@ -367,7 +370,7 @@ module ActiveMerchant #:nodoc:
367
370
 
368
371
  def merchant_initiated_unscheduled(options)
369
372
  return options[:merchant_initiated_unscheduled] if options[:merchant_initiated_unscheduled]
370
- return 'Y' if options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled'
373
+ return 'Y' if options.dig(:stored_credential, :initiator) == 'merchant' && options.dig(:stored_credential, :reason_type) == 'unscheduled' || options.dig(:stored_credential, :reason_type) == 'recurring'
371
374
  end
372
375
 
373
376
  def entry_mode(options)
@@ -390,15 +393,17 @@ module ActiveMerchant #:nodoc:
390
393
 
391
394
  def commit(request)
392
395
  request = "xmldata=#{request}".delete('&')
396
+ store_action = request.match?('CCGETTOKEN')
393
397
 
394
398
  response = parse(ssl_post(test? ? self.test_url : self.live_url, request, headers))
399
+ response = hash_html_decode(response)
395
400
 
396
401
  Response.new(
397
402
  response[:result] == '0',
398
403
  response[:result_message] || response[:errorMessage],
399
404
  response,
400
405
  test: @options[:test] || test?,
401
- authorization: authorization_from(response),
406
+ authorization: authorization_from(response, store_action),
402
407
  error_code: response[:errorCode],
403
408
  avs_result: { code: response[:avs_response] },
404
409
  cvv_result: response[:cvv2_response],
@@ -413,7 +418,7 @@ module ActiveMerchant #:nodoc:
413
418
  def headers
414
419
  {
415
420
  'Accept' => 'application/xml',
416
- 'Content-type' => 'application/x-www-form-urlencoded'
421
+ 'Content-type' => 'application/x-www-form-urlencoded;charset=utf8'
417
422
  }
418
423
  end
419
424
 
@@ -424,16 +429,46 @@ module ActiveMerchant #:nodoc:
424
429
  response.deep_transform_keys { |key| key.gsub('ssl_', '').to_sym }
425
430
  end
426
431
 
427
- def authorization_from(response)
432
+ def authorization_from(response, store_action)
433
+ return response[:token] if store_action
434
+
428
435
  [response[:approval_code], response[:txn_id]].join(';')
429
436
  end
430
437
 
431
- def truncate(value, size)
438
+ def url_encode_truncate(value, size)
432
439
  return nil unless value
433
440
 
434
- difference = value.force_encoding('iso-8859-1').length - value.length
441
+ encoded = url_encode(value)
442
+
443
+ while encoded.length > size
444
+ value.chop!
445
+ encoded = url_encode(value)
446
+ end
447
+ encoded
448
+ end
449
+
450
+ def url_encode(value)
451
+ if value.is_a?(String)
452
+ encoded = CGI.escape(value)
453
+ encoded = encoded.tr('+', ' ') # don't encode spaces
454
+ encoded = encoded.gsub('%26', '%26amp;') # account for Elavon's weird '&' handling
455
+ encoded
456
+ else
457
+ value.to_s
458
+ end
459
+ end
435
460
 
436
- return value.delete('&"<>').to_s[0, (size - difference)]
461
+ def hash_html_decode(hash)
462
+ hash.each do |k, v|
463
+ if v.is_a?(String)
464
+ # decode all string params
465
+ v = v.gsub('&amp;amp;', '&amp;') # account for Elavon's weird '&' handling
466
+ hash[k] = CGI.unescape_html(v)
467
+ elsif v.is_a?(Hash)
468
+ hash_html_decode(v)
469
+ end
470
+ end
471
+ hash
437
472
  end
438
473
  end
439
474
  end
@@ -82,6 +82,19 @@ module ActiveMerchant #:nodoc:
82
82
  commit('CreditCardReturn', request, money)
83
83
  end
84
84
 
85
+ def credit(money, payment, options = {})
86
+ request = build_soap_request do |xml|
87
+ xml.CreditCardCredit(xmlns: 'https://transaction.elementexpress.com') do
88
+ add_credentials(xml)
89
+ add_payment_method(xml, payment)
90
+ add_transaction(xml, money, options)
91
+ add_terminal(xml, options)
92
+ end
93
+ end
94
+
95
+ commit('CreditCardCredit', request, money)
96
+ end
97
+
85
98
  def void(authorization, options = {})
86
99
  trans_id, trans_amount = split_authorization(authorization)
87
100
  options.merge!({ trans_id: trans_id, trans_amount: trans_amount, reversal_type: 'Full' })
@@ -186,15 +199,22 @@ module ActiveMerchant #:nodoc:
186
199
  xml.ReversalType options[:reversal_type] if options[:reversal_type]
187
200
  xml.TransactionID options[:trans_id] if options[:trans_id]
188
201
  xml.TransactionAmount amount(money.to_i) if money
189
- xml.MarketCode 'Default' if money
202
+ xml.MarketCode market_code(money, options) if options[:market_code] || money
190
203
  xml.ReferenceNumber options[:order_id] || SecureRandom.hex(20)
191
-
204
+ xml.TicketNumber options[:ticket_number] if options[:ticket_number]
205
+ xml.MerchantSuppliedTransactionId options[:merchant_supplied_transaction_id] if options[:merchant_supplied_transaction_id]
192
206
  xml.PaymentType options[:payment_type] if options[:payment_type]
193
207
  xml.SubmissionType options[:submission_type] if options[:submission_type]
194
208
  xml.DuplicateCheckDisableFlag options[:duplicate_check_disable_flag].to_s == 'true' ? 'True' : 'False' unless options[:duplicate_check_disable_flag].nil?
209
+ xml.DuplicateOverrideFlag options[:duplicate_override_flag].to_s == 'true' ? 'True' : 'False' unless options[:duplicate_override_flag].nil?
210
+ xml.MerchantDescriptor options[:merchant_descriptor] if options[:merchant_descriptor]
195
211
  end
196
212
  end
197
213
 
214
+ def market_code(money, options)
215
+ options[:market_code] || 'Default'
216
+ end
217
+
198
218
  def add_terminal(xml, options)
199
219
  xml.terminal do
200
220
  xml.TerminalID options[:terminal_id] || '01'