activemerchant 1.125.0 → 1.129.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +316 -0
  3. data/lib/active_merchant/billing/check.rb +40 -8
  4. data/lib/active_merchant/billing/credit_card.rb +28 -1
  5. data/lib/active_merchant/billing/credit_card_methods.rb +91 -23
  6. data/lib/active_merchant/billing/gateway.rb +2 -1
  7. data/lib/active_merchant/billing/gateways/adyen.rb +74 -12
  8. data/lib/active_merchant/billing/gateways/airwallex.rb +370 -0
  9. data/lib/active_merchant/billing/gateways/alelo.rb +256 -0
  10. data/lib/active_merchant/billing/gateways/authorize_net.rb +21 -4
  11. data/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +2 -1
  12. data/lib/active_merchant/billing/gateways/beanstream.rb +18 -0
  13. data/lib/active_merchant/billing/gateways/blue_pay.rb +1 -1
  14. data/lib/active_merchant/billing/gateways/blue_snap.rb +53 -22
  15. data/lib/active_merchant/billing/gateways/bogus.rb +4 -0
  16. data/lib/active_merchant/billing/gateways/borgun.rb +56 -16
  17. data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +6 -1
  18. data/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +113 -0
  19. data/lib/active_merchant/billing/gateways/braintree_blue.rb +151 -32
  20. data/lib/active_merchant/billing/gateways/card_connect.rb +28 -10
  21. data/lib/active_merchant/billing/gateways/card_stream.rb +23 -0
  22. data/lib/active_merchant/billing/gateways/checkout_v2.rb +228 -57
  23. data/lib/active_merchant/billing/gateways/commerce_hub.rb +361 -0
  24. data/lib/active_merchant/billing/gateways/credorax.rb +56 -26
  25. data/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb +36 -0
  26. data/lib/active_merchant/billing/gateways/cyber_source.rb +112 -58
  27. data/lib/active_merchant/billing/gateways/cyber_source_rest.rb +456 -0
  28. data/lib/active_merchant/billing/gateways/d_local.rb +93 -5
  29. data/lib/active_merchant/billing/gateways/decidir.rb +32 -5
  30. data/lib/active_merchant/billing/gateways/decidir_plus.rb +185 -14
  31. data/lib/active_merchant/billing/gateways/ebanx.rb +39 -26
  32. data/lib/active_merchant/billing/gateways/element.rb +21 -1
  33. data/lib/active_merchant/billing/gateways/global_collect.rb +98 -37
  34. data/lib/active_merchant/billing/gateways/ipg.rb +14 -10
  35. data/lib/active_merchant/billing/gateways/iveri.rb +39 -3
  36. data/lib/active_merchant/billing/gateways/kushki.rb +21 -1
  37. data/lib/active_merchant/billing/gateways/litle.rb +118 -6
  38. data/lib/active_merchant/billing/gateways/mastercard.rb +1 -8
  39. data/lib/active_merchant/billing/gateways/mercado_pago.rb +17 -0
  40. data/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +44 -10
  41. data/lib/active_merchant/billing/gateways/monei.rb +2 -0
  42. data/lib/active_merchant/billing/gateways/moneris.rb +55 -13
  43. data/lib/active_merchant/billing/gateways/mundipagg.rb +3 -0
  44. data/lib/active_merchant/billing/gateways/nmi.rb +12 -7
  45. data/lib/active_merchant/billing/gateways/ogone.rb +35 -7
  46. data/lib/active_merchant/billing/gateways/openpay.rb +20 -3
  47. data/lib/active_merchant/billing/gateways/orbital.rb +378 -335
  48. data/lib/active_merchant/billing/gateways/pay_trace.rb +64 -18
  49. data/lib/active_merchant/billing/gateways/payeezy.rb +59 -4
  50. data/lib/active_merchant/billing/gateways/payflow.rb +62 -0
  51. data/lib/active_merchant/billing/gateways/paymentez.rb +44 -13
  52. data/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +4 -0
  53. data/lib/active_merchant/billing/gateways/paysafe.rb +37 -29
  54. data/lib/active_merchant/billing/gateways/payu_latam.rb +28 -15
  55. data/lib/active_merchant/billing/gateways/plexo.rb +308 -0
  56. data/lib/active_merchant/billing/gateways/priority.rb +185 -140
  57. data/lib/active_merchant/billing/gateways/rapyd.rb +319 -0
  58. data/lib/active_merchant/billing/gateways/reach.rb +277 -0
  59. data/lib/active_merchant/billing/gateways/redsys.rb +9 -5
  60. data/lib/active_merchant/billing/gateways/safe_charge.rb +1 -4
  61. data/lib/active_merchant/billing/gateways/sage_pay.rb +1 -1
  62. data/lib/active_merchant/billing/gateways/securion_pay.rb +40 -0
  63. data/lib/active_merchant/billing/gateways/shift4.rb +342 -0
  64. data/lib/active_merchant/billing/gateways/simetrik.rb +368 -0
  65. data/lib/active_merchant/billing/gateways/stripe.rb +25 -3
  66. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +155 -70
  67. data/lib/active_merchant/billing/gateways/tns.rb +2 -5
  68. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +1 -1
  69. data/lib/active_merchant/billing/gateways/trust_commerce.rb +14 -3
  70. data/lib/active_merchant/billing/gateways/vanco.rb +12 -3
  71. data/lib/active_merchant/billing/gateways/visanet_peru.rb +6 -2
  72. data/lib/active_merchant/billing/gateways/vpos.rb +7 -4
  73. data/lib/active_merchant/billing/gateways/wompi.rb +8 -4
  74. data/lib/active_merchant/billing/gateways/worldpay.rb +117 -9
  75. data/lib/active_merchant/billing/response.rb +15 -1
  76. data/lib/active_merchant/connection.rb +0 -2
  77. data/lib/active_merchant/country.rb +1 -0
  78. data/lib/active_merchant/errors.rb +4 -1
  79. data/lib/active_merchant/version.rb +1 -1
  80. metadata +28 -3
@@ -76,6 +76,7 @@ module ActiveMerchant #:nodoc:
76
76
  add_transaction_type(post, :authorization)
77
77
  add_customer_ip(post, options)
78
78
  add_recurring_payment(post, options)
79
+ add_three_ds(post, options)
79
80
  commit(post)
80
81
  end
81
82
 
@@ -88,6 +89,7 @@ module ActiveMerchant #:nodoc:
88
89
  add_transaction_type(post, purchase_action(source))
89
90
  add_customer_ip(post, options)
90
91
  add_recurring_payment(post, options)
92
+ add_three_ds(post, options)
91
93
  commit(post)
92
94
  end
93
95
 
@@ -215,6 +217,22 @@ module ActiveMerchant #:nodoc:
215
217
  def build_response(*args)
216
218
  Response.new(*args)
217
219
  end
220
+
221
+ def add_three_ds(post, options)
222
+ return unless three_d_secure = options[:three_d_secure]
223
+
224
+ post[:SecureXID] = (three_d_secure[:ds_transaction_id] || three_d_secure[:xid]) if three_d_secure.slice(:ds_transaction_id, :xid).values.any?
225
+ post[:SecureECI] = formatted_three_ds_eci(three_d_secure[:eci]) if three_d_secure[:eci].present?
226
+ post[:SecureCAVV] = three_d_secure[:cavv] if three_d_secure[:cavv].present?
227
+ end
228
+
229
+ def formatted_three_ds_eci(val)
230
+ case val
231
+ when '05', '02' then 5
232
+ when '06', '01' then 6
233
+ else val.to_i
234
+ end
235
+ end
218
236
  end
219
237
  end
220
238
  end
@@ -483,7 +483,7 @@ module ActiveMerchant #:nodoc:
483
483
  return unless reason_type = options.dig(:stored_credential, :reason_type)
484
484
 
485
485
  case reason_type
486
- when 'recurring' || 'installment'
486
+ when 'recurring', 'installment'
487
487
  'Y'
488
488
  when 'unscheduled'
489
489
  'N'
@@ -68,6 +68,8 @@ module ActiveMerchant
68
68
  'business_savings' => 'CORPORATE_SAVINGS'
69
69
  }
70
70
 
71
+ SHOPPER_INITIATOR = %w(CUSTOMER CARDHOLDER)
72
+
71
73
  STATE_CODE_COUNTRIES = %w(US CA)
72
74
 
73
75
  def initialize(options = {})
@@ -78,7 +80,7 @@ module ActiveMerchant
78
80
  def purchase(money, payment_method, options = {})
79
81
  payment_method_details = PaymentMethodDetails.new(payment_method)
80
82
 
81
- commit(:purchase, :post, payment_method_details) do |doc|
83
+ commit(:purchase, options, :post, payment_method_details) do |doc|
82
84
  if payment_method_details.alt_transaction?
83
85
  add_alt_transaction_purchase(doc, money, payment_method_details, options)
84
86
  else
@@ -88,13 +90,13 @@ module ActiveMerchant
88
90
  end
89
91
 
90
92
  def authorize(money, payment_method, options = {})
91
- commit(:authorize) do |doc|
93
+ commit(:authorize, options) do |doc|
92
94
  add_auth_purchase(doc, money, payment_method, options)
93
95
  end
94
96
  end
95
97
 
96
98
  def capture(money, authorization, options = {})
97
- commit(:capture, :put) do |doc|
99
+ commit(:capture, options, :put) do |doc|
98
100
  add_authorization(doc, authorization)
99
101
  add_order(doc, options)
100
102
  add_amount(doc, money, options) if options[:include_capture_amount] == true
@@ -102,15 +104,16 @@ module ActiveMerchant
102
104
  end
103
105
 
104
106
  def refund(money, authorization, options = {})
105
- commit(:refund, :put) do |doc|
106
- add_authorization(doc, authorization)
107
- add_amount(doc, money, options)
108
- add_order(doc, options)
107
+ options[:endpoint] = options[:merchant_transaction_id] ? "/refund/merchant/#{options[:merchant_transaction_id]}" : "/refund/#{authorization}"
108
+ commit(:refund, options, :post) do |doc|
109
+ add_amount(doc, money, options) if money
110
+ %i[reason cancel_subscription tax_amount].each { |field| send_when_present(doc, field, options) }
111
+ add_metadata(doc, options)
109
112
  end
110
113
  end
111
114
 
112
115
  def void(authorization, options = {})
113
- commit(:void, :put) do |doc|
116
+ commit(:void, options, :put) do |doc|
114
117
  add_authorization(doc, authorization)
115
118
  add_order(doc, options)
116
119
  end
@@ -123,7 +126,7 @@ module ActiveMerchant
123
126
  def store(payment_method, options = {})
124
127
  payment_method_details = PaymentMethodDetails.new(payment_method)
125
128
 
126
- commit(:store, :post, payment_method_details) do |doc|
129
+ commit(:store, options, :post, payment_method_details) do |doc|
127
130
  add_personal_info(doc, payment_method, options)
128
131
  add_echeck_company(doc, payment_method) if payment_method_details.check?
129
132
  doc.send('payment-sources') do
@@ -149,7 +152,7 @@ module ActiveMerchant
149
152
 
150
153
  def verify_credentials
151
154
  begin
152
- ssl_get(url.to_s, headers)
155
+ ssl_get(url.to_s, headers(options))
153
156
  rescue ResponseError => e
154
157
  return false if e.response.code.to_i == 401
155
158
  end
@@ -178,6 +181,7 @@ module ActiveMerchant
178
181
  doc.send('store-card', options[:store_card] || false)
179
182
  add_amount(doc, money, options)
180
183
  add_fraud_info(doc, payment_method, options)
184
+ add_stored_credentials(doc, options)
181
185
 
182
186
  if payment_method.is_a?(String)
183
187
  doc.send('vaulted-shopper-id', payment_method)
@@ -189,6 +193,19 @@ module ActiveMerchant
189
193
  end
190
194
  end
191
195
 
196
+ def add_stored_credentials(doc, options)
197
+ return unless stored_credential = options[:stored_credential]
198
+
199
+ initiator = stored_credential[:initiator]&.upcase
200
+ initiator = 'SHOPPER' if SHOPPER_INITIATOR.include?(initiator)
201
+ doc.send('transaction-initiator', initiator) if stored_credential[:initiator]
202
+ if stored_credential[:network_transaction_id]
203
+ doc.send('network-transaction-info') do
204
+ doc.send('original-network-transaction-id', stored_credential[:network_transaction_id])
205
+ end
206
+ end
207
+ end
208
+
192
209
  def add_amount(doc, money, options)
193
210
  currency = options[:currency] || currency(money)
194
211
  doc.amount(localized_amount(money, currency))
@@ -234,6 +251,7 @@ module ActiveMerchant
234
251
  doc.send('meta-key', truncate(entry[:meta_key], 40))
235
252
  doc.send('meta-value', truncate(entry[:meta_value], 500))
236
253
  doc.send('meta-description', truncate(entry[:meta_description], 40))
254
+ doc.send('is-visible', truncate(entry[:meta_is_visible], 5))
237
255
  end
238
256
  end
239
257
  end
@@ -242,6 +260,7 @@ module ActiveMerchant
242
260
  def add_order(doc, options)
243
261
  doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
244
262
  doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor]
263
+ doc.send('descriptor-phone-number', options[:descriptor_phone_number]) if options[:descriptor_phone_number]
245
264
  add_metadata(doc, options)
246
265
  add_3ds(doc, options[:three_d_secure]) if options[:three_d_secure]
247
266
  add_level_3_data(doc, options)
@@ -348,6 +367,7 @@ module ActiveMerchant
348
367
  def add_alt_transaction_purchase(doc, money, payment_method_details, options)
349
368
  doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
350
369
  doc.send('soft-descriptor', options[:soft_descriptor]) if options[:soft_descriptor]
370
+ doc.send('descriptor-phone-number', options[:descriptor_phone_number]) if options[:descriptor_phone_number]
351
371
  add_amount(doc, money, options)
352
372
 
353
373
  vaulted_shopper_id = payment_method_details.vaulted_shopper_id
@@ -356,6 +376,7 @@ module ActiveMerchant
356
376
  add_echeck_transaction(doc, payment_method_details.payment_method, options, vaulted_shopper_id.present?) if payment_method_details.check?
357
377
 
358
378
  add_fraud_info(doc, payment_method_details.payment_method, options)
379
+ add_stored_credentials(doc, options)
359
380
  add_metadata(doc, options)
360
381
  end
361
382
 
@@ -386,13 +407,12 @@ module ActiveMerchant
386
407
 
387
408
  def parse(response)
388
409
  return bad_authentication_response if response.code.to_i == 401
389
- return generic_error_response(response.body) if [403, 429].include?(response.code.to_i)
410
+ return generic_error_response(response.body) if [403, 405, 429].include?(response.code.to_i)
390
411
 
391
412
  parsed = {}
392
413
  doc = Nokogiri::XML(response.body)
393
414
  doc.root.xpath('*').each do |node|
394
415
  name = node.name.downcase
395
-
396
416
  if node.elements.empty?
397
417
  parsed[name] = node.text
398
418
  elsif name == 'transaction-meta-data'
@@ -433,15 +453,15 @@ module ActiveMerchant
433
453
  end
434
454
  end
435
455
 
436
- def api_request(action, request, verb, payment_method_details)
437
- ssl_request(verb, url(action, payment_method_details), request, headers)
456
+ def api_request(action, request, verb, payment_method_details, options)
457
+ ssl_request(verb, url(action, options, payment_method_details), request, headers(options))
438
458
  rescue ResponseError => e
439
459
  e.response
440
460
  end
441
461
 
442
- def commit(action, verb = :post, payment_method_details = PaymentMethodDetails.new())
462
+ def commit(action, options, verb = :post, payment_method_details = PaymentMethodDetails.new())
443
463
  request = build_xml_request(action, payment_method_details) { |doc| yield(doc) }
444
- response = api_request(action, request, verb, payment_method_details)
464
+ response = api_request(action, request, verb, payment_method_details, options)
445
465
  parsed = parse(response)
446
466
 
447
467
  succeeded = success_from(action, response)
@@ -457,9 +477,10 @@ module ActiveMerchant
457
477
  )
458
478
  end
459
479
 
460
- def url(action = nil, payment_method_details = PaymentMethodDetails.new())
480
+ def url(action = nil, options = {}, payment_method_details = PaymentMethodDetails.new())
461
481
  base = test? ? test_url : live_url
462
482
  resource = action == :store ? 'vaulted-shoppers' : payment_method_details.resource_url
483
+ resource += options[:endpoint] if action == :refund
463
484
  "#{base}/#{resource}"
464
485
  end
465
486
 
@@ -510,7 +531,9 @@ module ActiveMerchant
510
531
  end
511
532
 
512
533
  def authorization_from(action, parsed_response, payment_method_details)
513
- action == :store ? vaulted_shopper_id(parsed_response, payment_method_details) : parsed_response['transaction-id']
534
+ return vaulted_shopper_id(parsed_response, payment_method_details) if action == :store
535
+
536
+ parsed_response['refund-transaction-id'] || parsed_response['transaction-id']
514
537
  end
515
538
 
516
539
  def vaulted_shopper_id(parsed_response, payment_method_details)
@@ -532,20 +555,28 @@ module ActiveMerchant
532
555
  end
533
556
 
534
557
  def root_element(action, payment_method_details)
535
- action == :store ? 'vaulted-shopper' : payment_method_details.root_element
558
+ return 'refund' if action == :refund
559
+ return 'vaulted-shopper' if action == :store
560
+
561
+ payment_method_details.root_element
536
562
  end
537
563
 
538
- def headers
539
- {
564
+ def headers(options)
565
+ idempotency_key = options[:idempotency_key] if options[:idempotency_key]
566
+
567
+ headers = {
540
568
  'Content-Type' => 'application/xml',
541
569
  'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_username]}:#{@options[:api_password]}").strip)
542
570
  }
571
+
572
+ headers['Idempotency-Key'] = idempotency_key if idempotency_key
573
+ headers
543
574
  end
544
575
 
545
576
  def build_xml_request(action, payment_method_details)
546
577
  builder = Nokogiri::XML::Builder.new
547
578
  builder.__send__(root_element(action, payment_method_details), root_attributes) do |doc|
548
- doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction?
579
+ doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction? && action != :refund
549
580
  yield(doc)
550
581
  end
551
582
  builder.doc.root.to_xml
@@ -90,6 +90,10 @@ module ActiveMerchant #:nodoc:
90
90
  end
91
91
  end
92
92
 
93
+ def verify(credit_card, options = {})
94
+ authorize(0, credit_card, options)
95
+ end
96
+
93
97
  def store(paysource, options = {})
94
98
  case normalize(paysource)
95
99
  when /1$/
@@ -23,18 +23,34 @@ module ActiveMerchant #:nodoc:
23
23
 
24
24
  def purchase(money, payment, options = {})
25
25
  post = {}
26
- post[:TransType] = '1'
26
+ action = ''
27
+ if options[:apply_3d_secure] == '1'
28
+ add_3ds_preauth_fields(post, options)
29
+ action = '3ds_preauth'
30
+ else
31
+ post[:TransType] = '1'
32
+ add_3ds_fields(post, options)
33
+ action = 'sale'
34
+ end
27
35
  add_invoice(post, money, options)
28
36
  add_payment_method(post, payment)
29
- commit('sale', post)
37
+ commit(action, post, options)
30
38
  end
31
39
 
32
40
  def authorize(money, payment, options = {})
33
41
  post = {}
34
- post[:TransType] = '5'
42
+ action = ''
43
+ if options[:apply_3d_secure] == '1'
44
+ add_3ds_preauth_fields(post, options)
45
+ action = '3ds_preauth'
46
+ else
47
+ post[:TransType] = '5'
48
+ add_3ds_fields(post, options)
49
+ action = 'authonly'
50
+ end
35
51
  add_invoice(post, money, options)
36
52
  add_payment_method(post, payment)
37
- commit('authonly', post, options)
53
+ commit(action, post, options)
38
54
  end
39
55
 
40
56
  def capture(money, authorization, options = {})
@@ -81,9 +97,26 @@ module ActiveMerchant #:nodoc:
81
97
  CURRENCY_CODES['EUR'] = '978'
82
98
  CURRENCY_CODES['USD'] = '840'
83
99
 
100
+ def add_3ds_fields(post, options)
101
+ post[:ThreeDSMessageId] = options[:three_ds_message_id] if options[:three_ds_message_id]
102
+ post[:ThreeDS_PARes] = options[:three_ds_pares] if options[:three_ds_pares]
103
+ post[:ThreeDS_CRes] = options[:three_ds_cres] if options[:three_ds_cres]
104
+ end
105
+
106
+ def add_3ds_preauth_fields(post, options)
107
+ post[:SaleDescription] = options[:sale_description] || ''
108
+ post[:MerchantReturnURL] = options[:merchant_return_url] if options[:merchant_return_url]
109
+ end
110
+
84
111
  def add_invoice(post, money, options)
85
112
  post[:TrAmount] = amount(money)
86
113
  post[:TrCurrency] = CURRENCY_CODES[options[:currency] || currency(money)]
114
+ # The ISK currency must have a currency exponent of 2 on the 3DS request but not on the auth request
115
+ if post[:TrCurrency] == '352' && options[:apply_3d_secure] == '1'
116
+ post[:TrCurrencyExponent] = 2
117
+ else
118
+ post[:TrCurrencyExponent] = 0
119
+ end
87
120
  post[:TerminalID] = options[:terminal_id] || '1'
88
121
  end
89
122
 
@@ -103,11 +136,11 @@ module ActiveMerchant #:nodoc:
103
136
  post[:AuthCode] = authcode
104
137
  end
105
138
 
106
- def parse(xml)
139
+ def parse(xml, options = nil)
107
140
  response = {}
108
141
 
109
142
  doc = Nokogiri::XML(CGI.unescapeHTML(xml))
110
- body = doc.xpath('//getAuthorizationReply')
143
+ body = options[:apply_3d_secure] == '1' ? doc.xpath('//get3DSAuthenticationReply') : doc.xpath('//getAuthorizationReply')
111
144
  body = doc.xpath('//cancelAuthorizationReply') if body.length == 0
112
145
  body.children.each do |node|
113
146
  if node.text?
@@ -121,7 +154,6 @@ module ActiveMerchant #:nodoc:
121
154
  end
122
155
  end
123
156
  end
124
-
125
157
  response
126
158
  end
127
159
 
@@ -132,7 +164,7 @@ module ActiveMerchant #:nodoc:
132
164
 
133
165
  request = build_request(action, post, options)
134
166
  raw = ssl_post(url(action), request, headers)
135
- pairs = parse(raw)
167
+ pairs = parse(raw, options)
136
168
  success = success_from(pairs)
137
169
 
138
170
  Response.new(
@@ -145,7 +177,7 @@ module ActiveMerchant #:nodoc:
145
177
  end
146
178
 
147
179
  def success_from(response)
148
- (response[:actioncode] == '000')
180
+ (response[:actioncode] == '000') || (response[:status_resultcode] == '0')
149
181
  end
150
182
 
151
183
  def message_from(succeeded, response)
@@ -182,16 +214,17 @@ module ActiveMerchant #:nodoc:
182
214
 
183
215
  def build_request(action, post, options = {})
184
216
  mode = action == 'void' ? 'cancel' : 'get'
217
+ transaction_type = action == '3ds_preauth' ? '3DSAuthentication' : 'Authorization'
185
218
  xml = Builder::XmlMarkup.new indent: 18
186
219
  xml.instruct!(:xml, version: '1.0', encoding: 'utf-8')
187
- xml.tag!("#{mode}Authorization") do
220
+ xml.tag!("#{mode}#{transaction_type}") do
188
221
  post.each do |field, value|
189
222
  xml.tag!(field, value)
190
223
  end
191
224
  build_airline_xml(xml, options[:passenger_itinerary_data]) if options[:passenger_itinerary_data]
192
225
  end
193
226
  inner = CGI.escapeHTML(xml.target!)
194
- envelope(mode).sub(/{{ :body }}/, inner)
227
+ envelope(mode, action).sub(/{{ :body }}/, inner)
195
228
  end
196
229
 
197
230
  def build_airline_xml(xml, airline_data)
@@ -204,16 +237,23 @@ module ActiveMerchant #:nodoc:
204
237
  end
205
238
  end
206
239
 
207
- def envelope(mode)
240
+ def envelope(mode, action)
241
+ if action == '3ds_preauth'
242
+ transaction_action = "#{mode}3DSAuthentication"
243
+ request_action = "#{mode}Auth3DSReqXml"
244
+ else
245
+ transaction_action = "#{mode}AuthorizationInput"
246
+ request_action = "#{mode}AuthReqXml"
247
+ end
208
248
  <<-XML
209
249
  <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:aut="http://Borgun/Heimir/pub/ws/Authorization">
210
250
  <soapenv:Header/>
211
251
  <soapenv:Body>
212
- <aut:#{mode}AuthorizationInput>
213
- <#{mode}AuthReqXml>
252
+ <aut:#{transaction_action}>
253
+ <#{request_action}>
214
254
  {{ :body }}
215
- </#{mode}AuthReqXml>
216
- </aut:#{mode}AuthorizationInput>
255
+ </#{request_action}>
256
+ </aut:#{transaction_action}>
217
257
  </soapenv:Body>
218
258
  </soapenv:Envelope>
219
259
  XML
@@ -18,6 +18,11 @@ module BraintreeCommon
18
18
  transcript.
19
19
  gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
20
20
  gsub(%r((&?ccnumber=)\d*(&?)), '\1[FILTERED]\2').
21
- gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2')
21
+ gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2').
22
+ gsub(%r((<account-number>)\d+(</account-number>)), '\1[FILTERED]\2').
23
+ gsub(%r((<payment-method-nonce>)[^<]+(</payment-method-nonce>)), '\1[FILTERED]\2').
24
+ gsub(%r((<payment-method-token>)[^<]+(</payment-method-token>)), '\1[FILTERED]\2').
25
+ gsub(%r((<value>)[^<]{100,}(</value>)), '\1[FILTERED]\2').
26
+ gsub(%r((<token>)[^<]+(</token>)), '\1[FILTERED]\2')
22
27
  end
23
28
  end
@@ -0,0 +1,113 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class TokenNonce #:nodoc:
4
+ include PostsData
5
+ # This class emulates the behavior of the front-end js library to
6
+ # create token nonce for a bank account base on the docs:
7
+ # https://developer.paypal.com/braintree/docs/guides/ach/client-side
8
+
9
+ attr_reader :braintree_gateway, :options
10
+
11
+ def initialize(gateway, options = {})
12
+ @braintree_gateway = gateway
13
+ @options = options
14
+ end
15
+
16
+ def url
17
+ sandbox = @braintree_gateway.config.environment == :sandbox
18
+ "https://payments#{'.sandbox' if sandbox}.braintree-api.com/graphql"
19
+ end
20
+
21
+ def create_token_nonce_for_payment_method(payment_method)
22
+ headers = {
23
+ 'Accept' => 'application/json',
24
+ 'Authorization' => "Bearer #{client_token}",
25
+ 'Content-Type' => 'application/json',
26
+ 'Braintree-Version' => '2018-05-10'
27
+ }
28
+ resp = ssl_post(url, build_nonce_request(payment_method), headers)
29
+ json_response = JSON.parse(resp)
30
+
31
+ message = json_response['errors'].map { |err| err['message'] }.join("\n") if json_response['errors'].present?
32
+ token = json_response.dig('data', 'tokenizeUsBankAccount', 'paymentMethod', 'id')
33
+
34
+ return token, message
35
+ end
36
+
37
+ def client_token
38
+ base64_token = @braintree_gateway.client_token.generate
39
+ JSON.parse(Base64.decode64(base64_token))['authorizationFingerprint']
40
+ end
41
+
42
+ private
43
+
44
+ def graphql_query
45
+ <<-GRAPHQL
46
+ mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
47
+ tokenizeUsBankAccount(input: $input) {
48
+ paymentMethod {
49
+ id
50
+ details {
51
+ ... on UsBankAccountDetails {
52
+ last4
53
+ }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ GRAPHQL
59
+ end
60
+
61
+ def billing_address_from_options
62
+ return nil if options[:billing_address].blank?
63
+
64
+ address = options[:billing_address]
65
+
66
+ {
67
+ streetAddress: address[:address1],
68
+ extendedAddress: address[:address2],
69
+ city: address[:city],
70
+ state: address[:state],
71
+ zipCode: address[:zip]
72
+ }.compact
73
+ end
74
+
75
+ def build_nonce_request(payment_method)
76
+ input = {
77
+ usBankAccount: {
78
+ achMandate: options[:ach_mandate],
79
+ routingNumber: payment_method.routing_number,
80
+ accountNumber: payment_method.account_number,
81
+ accountType: payment_method.account_type.upcase,
82
+ billingAddress: billing_address_from_options
83
+ }
84
+ }
85
+
86
+ if payment_method.account_holder_type == 'personal'
87
+ input[:usBankAccount][:individualOwner] = {
88
+ firstName: payment_method.first_name,
89
+ lastName: payment_method.last_name
90
+ }
91
+ else
92
+ input[:usBankAccount][:businessOwner] = {
93
+ businessName: payment_method.name
94
+ }
95
+ end
96
+
97
+ {
98
+ clientSdkMetadata: {
99
+ platform: 'web',
100
+ source: 'client',
101
+ integration: 'custom',
102
+ sessionId: SecureRandom.uuid,
103
+ version: '3.83.0'
104
+ },
105
+ query: graphql_query,
106
+ variables: {
107
+ input: input
108
+ }
109
+ }.to_json
110
+ end
111
+ end
112
+ end
113
+ end