activemerchant 1.123.0 → 1.126.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +206 -0
  3. data/lib/active_merchant/billing/check.rb +5 -8
  4. data/lib/active_merchant/billing/credit_card.rb +10 -0
  5. data/lib/active_merchant/billing/credit_card_methods.rb +18 -3
  6. data/lib/active_merchant/billing/gateway.rb +3 -2
  7. data/lib/active_merchant/billing/gateways/adyen.rb +66 -11
  8. data/lib/active_merchant/billing/gateways/airwallex.rb +341 -0
  9. data/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +2 -1
  10. data/lib/active_merchant/billing/gateways/blue_pay.rb +1 -1
  11. data/lib/active_merchant/billing/gateways/blue_snap.rb +31 -21
  12. data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +6 -1
  13. data/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +113 -0
  14. data/lib/active_merchant/billing/gateways/braintree_blue.rb +87 -15
  15. data/lib/active_merchant/billing/gateways/card_connect.rb +1 -1
  16. data/lib/active_merchant/billing/gateways/card_stream.rb +17 -13
  17. data/lib/active_merchant/billing/gateways/cashnet.rb +15 -5
  18. data/lib/active_merchant/billing/gateways/checkout_v2.rb +34 -5
  19. data/lib/active_merchant/billing/gateways/credorax.rb +10 -0
  20. data/lib/active_merchant/billing/gateways/cyber_source.rb +24 -36
  21. data/lib/active_merchant/billing/gateways/d_local.rb +61 -6
  22. data/lib/active_merchant/billing/gateways/decidir.rb +17 -1
  23. data/lib/active_merchant/billing/gateways/decidir_plus.rb +344 -0
  24. data/lib/active_merchant/billing/gateways/ebanx.rb +19 -3
  25. data/lib/active_merchant/billing/gateways/elavon.rb +6 -3
  26. data/lib/active_merchant/billing/gateways/element.rb +20 -2
  27. data/lib/active_merchant/billing/gateways/global_collect.rb +137 -32
  28. data/lib/active_merchant/billing/gateways/ipg.rb +415 -0
  29. data/lib/active_merchant/billing/gateways/kushki.rb +7 -0
  30. data/lib/active_merchant/billing/gateways/litle.rb +93 -1
  31. data/lib/active_merchant/billing/gateways/mercado_pago.rb +3 -1
  32. data/lib/active_merchant/billing/gateways/mit.rb +260 -0
  33. data/lib/active_merchant/billing/gateways/moka.rb +24 -11
  34. data/lib/active_merchant/billing/gateways/moneris.rb +35 -8
  35. data/lib/active_merchant/billing/gateways/mundipagg.rb +8 -6
  36. data/lib/active_merchant/billing/gateways/nmi.rb +27 -8
  37. data/lib/active_merchant/billing/gateways/orbital.rb +357 -319
  38. data/lib/active_merchant/billing/gateways/pay_arc.rb +9 -7
  39. data/lib/active_merchant/billing/gateways/pay_conex.rb +3 -1
  40. data/lib/active_merchant/billing/gateways/pay_trace.rb +1 -1
  41. data/lib/active_merchant/billing/gateways/payflow.rb +76 -6
  42. data/lib/active_merchant/billing/gateways/paymentez.rb +35 -9
  43. data/lib/active_merchant/billing/gateways/paysafe.rb +155 -34
  44. data/lib/active_merchant/billing/gateways/payu_latam.rb +31 -16
  45. data/lib/active_merchant/billing/gateways/payway_dot_com.rb +3 -3
  46. data/lib/active_merchant/billing/gateways/pin.rb +31 -4
  47. data/lib/active_merchant/billing/gateways/priority.rb +369 -0
  48. data/lib/active_merchant/billing/gateways/rapyd.rb +258 -0
  49. data/lib/active_merchant/billing/gateways/realex.rb +18 -0
  50. data/lib/active_merchant/billing/gateways/safe_charge.rb +7 -6
  51. data/lib/active_merchant/billing/gateways/simetrik.rb +362 -0
  52. data/lib/active_merchant/billing/gateways/stripe.rb +30 -8
  53. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +175 -72
  54. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +2 -1
  55. data/lib/active_merchant/billing/gateways/trust_commerce.rb +2 -1
  56. data/lib/active_merchant/billing/gateways/usa_epay_transaction.rb +20 -6
  57. data/lib/active_merchant/billing/gateways/visanet_peru.rb +6 -2
  58. data/lib/active_merchant/billing/gateways/wompi.rb +193 -0
  59. data/lib/active_merchant/billing/gateways/worldpay.rb +196 -64
  60. data/lib/active_merchant/billing/network_tokenization_credit_card.rb +1 -1
  61. data/lib/active_merchant/billing/response.rb +4 -0
  62. data/lib/active_merchant/version.rb +1 -1
  63. metadata +11 -2
@@ -0,0 +1,341 @@
1
+ module ActiveMerchant #:nodoc:
2
+ module Billing #:nodoc:
3
+ class AirwallexGateway < Gateway
4
+ self.test_url = 'https://api-demo.airwallex.com/api/v1'
5
+ self.live_url = 'https://pci-api.airwallex.com/api/v1'
6
+
7
+ # per https://www.airwallex.com/docs/online-payments__overview, cards are accepted in all EU countries
8
+ self.supported_countries = %w[AT AU BE BG CY CZ DE DK EE GR ES FI FR GB HK HR HU IE IT LT LU LV MT NL PL PT RO SE SG SI SK]
9
+ self.default_currency = 'AUD'
10
+ self.supported_cardtypes = %i[visa master]
11
+
12
+ self.homepage_url = 'https://airwallex.com/'
13
+ self.display_name = 'Airwallex'
14
+
15
+ ENDPOINTS = {
16
+ login: '/authentication/login',
17
+ setup: '/pa/payment_intents/create',
18
+ sale: '/pa/payment_intents/%{id}/confirm',
19
+ capture: '/pa/payment_intents/%{id}/capture',
20
+ refund: '/pa/refunds/create',
21
+ void: '/pa/payment_intents/%{id}/cancel'
22
+ }
23
+
24
+ def initialize(options = {})
25
+ requires!(options, :client_id, :client_api_key)
26
+ @client_id = options[:client_id]
27
+ @client_api_key = options[:client_api_key]
28
+ super
29
+ @access_token = setup_access_token
30
+ end
31
+
32
+ def purchase(money, card, options = {})
33
+ requires!(options, :return_url)
34
+
35
+ payment_intent_id = create_payment_intent(money, options)
36
+ post = {
37
+ 'request_id' => request_id(options),
38
+ 'merchant_order_id' => merchant_order_id(options),
39
+ 'return_url' => options[:return_url]
40
+ }
41
+ add_card(post, card, options)
42
+ add_descriptor(post, options)
43
+ add_stored_credential(post, options)
44
+ post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options)
45
+
46
+ add_three_ds(post, options)
47
+ commit(:sale, post, payment_intent_id)
48
+ end
49
+
50
+ def authorize(money, payment, options = {})
51
+ # authorize is just a purchase w/o an auto capture
52
+ purchase(money, payment, options.merge({ auto_capture: false }))
53
+ end
54
+
55
+ def capture(money, authorization, options = {})
56
+ raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
57
+
58
+ post = {
59
+ 'request_id' => request_id(options),
60
+ 'merchant_order_id' => merchant_order_id(options),
61
+ 'amount' => amount(money)
62
+ }
63
+ add_descriptor(post, options)
64
+
65
+ commit(:capture, post, authorization)
66
+ end
67
+
68
+ def refund(money, authorization, options = {})
69
+ raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
70
+
71
+ post = {}
72
+ post[:amount] = amount(money)
73
+ post[:payment_intent_id] = authorization
74
+ post[:request_id] = request_id(options)
75
+ post[:merchant_order_id] = merchant_order_id(options)
76
+
77
+ commit(:refund, post)
78
+ end
79
+
80
+ def void(authorization, options = {})
81
+ raise ArgumentError, 'An authorization value must be provided.' if authorization.blank?
82
+
83
+ post = {}
84
+ post[:request_id] = request_id(options)
85
+ post[:merchant_order_id] = merchant_order_id(options)
86
+ add_descriptor(post, options)
87
+
88
+ commit(:void, post, authorization)
89
+ end
90
+
91
+ def verify(credit_card, options = {})
92
+ MultiResponse.run(:use_first_response) do |r|
93
+ r.process { authorize(100, credit_card, options) }
94
+ r.process(:ignore_result) { void(r.authorization, options) }
95
+ end
96
+ end
97
+
98
+ def supports_scrubbing?
99
+ true
100
+ end
101
+
102
+ def scrub(transcript)
103
+ transcript.
104
+ gsub(/(\\\"number\\\":\\\")\d+/, '\1[REDACTED]').
105
+ gsub(/(\\\"cvc\\\":\\\")\d+/, '\1[REDACTED]')
106
+ end
107
+
108
+ private
109
+
110
+ def request_id(options)
111
+ options[:request_id] || generate_timestamp
112
+ end
113
+
114
+ def merchant_order_id(options)
115
+ options[:merchant_order_id] || options[:order_id] || generate_timestamp
116
+ end
117
+
118
+ def generate_timestamp
119
+ (Time.now.to_f.round(2) * 100).to_i.to_s
120
+ end
121
+
122
+ def setup_access_token
123
+ token_headers = {
124
+ 'Content-Type' => 'application/json',
125
+ 'x-client-id' => @client_id,
126
+ 'x-api-key' => @client_api_key
127
+ }
128
+ response = ssl_post(build_request_url(:login), nil, token_headers)
129
+ JSON.parse(response)['token']
130
+ end
131
+
132
+ def build_request_url(action, id = nil)
133
+ base_url = (test? ? test_url : live_url)
134
+ base_url + ENDPOINTS[action].to_s % { id: id }
135
+ end
136
+
137
+ def create_payment_intent(money, options = {})
138
+ post = {}
139
+ add_invoice(post, money, options)
140
+ add_order(post, options)
141
+ post[:request_id] = "#{request_id(options)}_setup"
142
+ post[:merchant_order_id] = "#{merchant_order_id(options)}_setup"
143
+ add_descriptor(post, options)
144
+
145
+ response = commit(:setup, post)
146
+ raise ArgumentError.new(response.message) unless response.success?
147
+
148
+ response.params['id']
149
+ end
150
+
151
+ def add_billing(post, card, options = {})
152
+ return unless has_name_info?(card)
153
+
154
+ billing = post['payment_method']['card']['billing'] || {}
155
+ billing['email'] = options[:email] if options[:email]
156
+ billing['phone'] = options[:phone] if options[:phone]
157
+ billing['first_name'] = card.first_name
158
+ billing['last_name'] = card.last_name
159
+ billing_address = options[:billing_address]
160
+ billing['address'] = build_address(billing_address) if has_required_address_info?(billing_address)
161
+
162
+ post['payment_method']['card']['billing'] = billing
163
+ end
164
+
165
+ def has_name_info?(card)
166
+ # These fields are required if billing data is sent.
167
+ card.first_name && card.last_name
168
+ end
169
+
170
+ def has_required_address_info?(address)
171
+ # These fields are required if address data is sent.
172
+ return unless address
173
+
174
+ address[:address1] && address[:country]
175
+ end
176
+
177
+ def build_address(address)
178
+ return unless address
179
+
180
+ address_data = {} # names r hard
181
+ address_data[:country_code] = address[:country]
182
+ address_data[:street] = address[:address1]
183
+ address_data[:city] = address[:city] if address[:city] # required per doc, not in practice
184
+ address_data[:postcode] = address[:zip] if address[:zip]
185
+ address_data[:state] = address[:state] if address[:state]
186
+ address_data
187
+ end
188
+
189
+ def add_invoice(post, money, options)
190
+ post[:amount] = amount(money)
191
+ post[:currency] = (options[:currency] || currency(money))
192
+ end
193
+
194
+ def add_card(post, card, options = {})
195
+ post['payment_method'] = {
196
+ 'type' => 'card',
197
+ 'card' => {
198
+ 'expiry_month' => format(card.month, :two_digits),
199
+ 'expiry_year' => card.year.to_s,
200
+ 'number' => card.number.to_s,
201
+ 'name' => card.name,
202
+ 'cvc' => card.verification_value
203
+ }
204
+ }
205
+ add_billing(post, card, options)
206
+ end
207
+
208
+ def add_order(post, options)
209
+ return unless shipping_address = options[:shipping_address]
210
+
211
+ physical_address = build_shipping_address(shipping_address)
212
+ first_name, last_name = split_names(shipping_address[:name])
213
+ shipping = {}
214
+ shipping[:first_name] = first_name if first_name
215
+ shipping[:last_name] = last_name if last_name
216
+ shipping[:phone_number] = shipping_address[:phone_number] if shipping_address[:phone_number]
217
+ shipping[:address] = physical_address
218
+ post[:order] = { shipping: shipping }
219
+ end
220
+
221
+ def build_shipping_address(shipping_address)
222
+ address = {}
223
+ address[:city] = shipping_address[:city]
224
+ address[:country_code] = shipping_address[:country]
225
+ address[:postcode] = shipping_address[:zip]
226
+ address[:state] = shipping_address[:state]
227
+ address[:street] = shipping_address[:address1]
228
+ address
229
+ end
230
+
231
+ def add_stored_credential(post, options)
232
+ return unless stored_credential = options[:stored_credential]
233
+
234
+ external_recurring_data = post[:external_recurring_data] = {}
235
+
236
+ case stored_credential.dig(:reason_type)
237
+ when 'recurring', 'installment'
238
+ external_recurring_data[:merchant_trigger_reason] = 'scheduled'
239
+ when 'unscheduled'
240
+ external_recurring_data[:merchant_trigger_reason] = 'unscheduled'
241
+ end
242
+
243
+ external_recurring_data[:original_transaction_id] = stored_credential.dig(:network_transaction_id)
244
+ external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
245
+ end
246
+
247
+ def add_three_ds(post, options)
248
+ return unless three_d_secure = options[:three_d_secure]
249
+
250
+ pm_options = post.dig('payment_method_options', 'card')
251
+
252
+ external_three_ds = {
253
+ 'version': format_three_ds_version(three_d_secure),
254
+ 'eci': three_d_secure[:eci]
255
+ }.merge(three_ds_version_specific_fields(three_d_secure))
256
+
257
+ pm_options ? pm_options.merge!('external_three_ds': external_three_ds) : post['payment_method_options'] = { 'card': { 'external_three_ds': external_three_ds } }
258
+ end
259
+
260
+ def format_three_ds_version(three_d_secure)
261
+ version = three_d_secure[:version].split('.')
262
+
263
+ version.push('0') until version.length == 3
264
+ version.join('.')
265
+ end
266
+
267
+ def three_ds_version_specific_fields(three_d_secure)
268
+ if three_d_secure[:version].to_f >= 2
269
+ {
270
+ 'authentication_value': three_d_secure[:cavv],
271
+ 'ds_transaction_id': three_d_secure[:ds_transaction_id],
272
+ 'three_ds_server_transaction_id': three_d_secure[:three_ds_server_trans_id]
273
+ }
274
+ else
275
+ {
276
+ 'cavv': three_d_secure[:cavv],
277
+ 'xid': three_d_secure[:xid]
278
+ }
279
+ end
280
+ end
281
+
282
+ def authorization_only?(options = {})
283
+ options.include?(:auto_capture) && options[:auto_capture] == false
284
+ end
285
+
286
+ def add_descriptor(post, options)
287
+ post[:descriptor] = options[:description] if options[:description]
288
+ end
289
+
290
+ def parse(body)
291
+ JSON.parse(body)
292
+ end
293
+
294
+ def commit(action, post, id = nil)
295
+ url = build_request_url(action, id)
296
+ post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
297
+ response = parse(ssl_post(url, post_data(post), post_headers))
298
+
299
+ Response.new(
300
+ success_from(response),
301
+ message_from(response),
302
+ response,
303
+ authorization: authorization_from(response),
304
+ avs_result: AVSResult.new(code: response.dig('latest_payment_attempt', 'authentication_data', 'avs_result')),
305
+ cvv_result: CVVResult.new(response.dig('latest_payment_attempt', 'authentication_data', 'cvc_code')),
306
+ test: test?,
307
+ error_code: error_code_from(response)
308
+ )
309
+ end
310
+
311
+ def handle_response(response)
312
+ case response.code.to_i
313
+ when 200...300, 400, 404
314
+ response.body
315
+ else
316
+ raise ResponseError.new(response)
317
+ end
318
+ end
319
+
320
+ def post_data(post)
321
+ post.to_json
322
+ end
323
+
324
+ def success_from(response)
325
+ %w(REQUIRES_PAYMENT_METHOD SUCCEEDED RECEIVED REQUIRES_CAPTURE CANCELLED).include?(response['status'])
326
+ end
327
+
328
+ def message_from(response)
329
+ response.dig('latest_payment_attempt', 'status') || response['status'] || response['message']
330
+ end
331
+
332
+ def authorization_from(response)
333
+ response.dig('latest_payment_attempt', 'payment_intent_id')
334
+ end
335
+
336
+ def error_code_from(response)
337
+ response['provider_original_response_code'] || response['code'] unless success_from(response)
338
+ end
339
+ end
340
+ end
341
+ end
@@ -6,9 +6,10 @@ module ActiveMerchant #:nodoc:
6
6
 
7
7
  self.supported_countries = %w[AL AD AM AT AZ BY BE BA BG HR CY CZ DK EE FI FR DE GR HU IS IE IT KZ LV LI LT LU MK MT MD MC ME NL NO PL PT RO RU SM RS SK SI ES SE CH TR UA GB VA]
8
8
  self.default_currency = 'EUR'
9
- self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND)
9
+ self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND IQD JOD LYD)
10
10
  self.money_format = :cents
11
11
  self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro]
12
+ self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF)
12
13
 
13
14
  self.homepage_url = 'https://www.barclaycardsmartpay.com/'
14
15
  self.display_name = 'Barclaycard Smartpay'
@@ -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'
@@ -78,7 +78,7 @@ module ActiveMerchant
78
78
  def purchase(money, payment_method, options = {})
79
79
  payment_method_details = PaymentMethodDetails.new(payment_method)
80
80
 
81
- commit(:purchase, :post, payment_method_details) do |doc|
81
+ commit(:purchase, options, :post, payment_method_details) do |doc|
82
82
  if payment_method_details.alt_transaction?
83
83
  add_alt_transaction_purchase(doc, money, payment_method_details, options)
84
84
  else
@@ -88,13 +88,13 @@ module ActiveMerchant
88
88
  end
89
89
 
90
90
  def authorize(money, payment_method, options = {})
91
- commit(:authorize) do |doc|
91
+ commit(:authorize, options) do |doc|
92
92
  add_auth_purchase(doc, money, payment_method, options)
93
93
  end
94
94
  end
95
95
 
96
96
  def capture(money, authorization, options = {})
97
- commit(:capture, :put) do |doc|
97
+ commit(:capture, options, :put) do |doc|
98
98
  add_authorization(doc, authorization)
99
99
  add_order(doc, options)
100
100
  add_amount(doc, money, options) if options[:include_capture_amount] == true
@@ -102,15 +102,16 @@ module ActiveMerchant
102
102
  end
103
103
 
104
104
  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)
105
+ options[:endpoint] = options[:merchant_transaction_id] ? "/refund/merchant/#{options[:merchant_transaction_id]}" : "/refund/#{authorization}"
106
+ commit(:refund, options, :post) do |doc|
107
+ add_amount(doc, money, options) if money
108
+ %i[reason cancel_subscription tax_amount].each { |field| send_when_present(doc, field, options) }
109
+ add_metadata(doc, options)
109
110
  end
110
111
  end
111
112
 
112
113
  def void(authorization, options = {})
113
- commit(:void, :put) do |doc|
114
+ commit(:void, options, :put) do |doc|
114
115
  add_authorization(doc, authorization)
115
116
  add_order(doc, options)
116
117
  end
@@ -123,7 +124,7 @@ module ActiveMerchant
123
124
  def store(payment_method, options = {})
124
125
  payment_method_details = PaymentMethodDetails.new(payment_method)
125
126
 
126
- commit(:store, :post, payment_method_details) do |doc|
127
+ commit(:store, options, :post, payment_method_details) do |doc|
127
128
  add_personal_info(doc, payment_method, options)
128
129
  add_echeck_company(doc, payment_method) if payment_method_details.check?
129
130
  doc.send('payment-sources') do
@@ -149,7 +150,7 @@ module ActiveMerchant
149
150
 
150
151
  def verify_credentials
151
152
  begin
152
- ssl_get(url.to_s, headers)
153
+ ssl_get(url.to_s, headers(options))
153
154
  rescue ResponseError => e
154
155
  return false if e.response.code.to_i == 401
155
156
  end
@@ -234,6 +235,7 @@ module ActiveMerchant
234
235
  doc.send('meta-key', truncate(entry[:meta_key], 40))
235
236
  doc.send('meta-value', truncate(entry[:meta_value], 500))
236
237
  doc.send('meta-description', truncate(entry[:meta_description], 40))
238
+ doc.send('is-visible', truncate(entry[:meta_is_visible], 5))
237
239
  end
238
240
  end
239
241
  end
@@ -386,13 +388,12 @@ module ActiveMerchant
386
388
 
387
389
  def parse(response)
388
390
  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)
391
+ return generic_error_response(response.body) if [403, 405, 429].include?(response.code.to_i)
390
392
 
391
393
  parsed = {}
392
394
  doc = Nokogiri::XML(response.body)
393
395
  doc.root.xpath('*').each do |node|
394
396
  name = node.name.downcase
395
-
396
397
  if node.elements.empty?
397
398
  parsed[name] = node.text
398
399
  elsif name == 'transaction-meta-data'
@@ -433,15 +434,15 @@ module ActiveMerchant
433
434
  end
434
435
  end
435
436
 
436
- def api_request(action, request, verb, payment_method_details)
437
- ssl_request(verb, url(action, payment_method_details), request, headers)
437
+ def api_request(action, request, verb, payment_method_details, options)
438
+ ssl_request(verb, url(action, options, payment_method_details), request, headers(options))
438
439
  rescue ResponseError => e
439
440
  e.response
440
441
  end
441
442
 
442
- def commit(action, verb = :post, payment_method_details = PaymentMethodDetails.new())
443
+ def commit(action, options, verb = :post, payment_method_details = PaymentMethodDetails.new())
443
444
  request = build_xml_request(action, payment_method_details) { |doc| yield(doc) }
444
- response = api_request(action, request, verb, payment_method_details)
445
+ response = api_request(action, request, verb, payment_method_details, options)
445
446
  parsed = parse(response)
446
447
 
447
448
  succeeded = success_from(action, response)
@@ -457,9 +458,10 @@ module ActiveMerchant
457
458
  )
458
459
  end
459
460
 
460
- def url(action = nil, payment_method_details = PaymentMethodDetails.new())
461
+ def url(action = nil, options = {}, payment_method_details = PaymentMethodDetails.new())
461
462
  base = test? ? test_url : live_url
462
463
  resource = action == :store ? 'vaulted-shoppers' : payment_method_details.resource_url
464
+ resource += options[:endpoint] if action == :refund
463
465
  "#{base}/#{resource}"
464
466
  end
465
467
 
@@ -532,20 +534,28 @@ module ActiveMerchant
532
534
  end
533
535
 
534
536
  def root_element(action, payment_method_details)
535
- action == :store ? 'vaulted-shopper' : payment_method_details.root_element
537
+ return 'refund' if action == :refund
538
+ return 'vaulted-shopper' if action == :store
539
+
540
+ payment_method_details.root_element
536
541
  end
537
542
 
538
- def headers
539
- {
543
+ def headers(options)
544
+ idempotency_key = options[:idempotency_key] if options[:idempotency_key]
545
+
546
+ headers = {
540
547
  'Content-Type' => 'application/xml',
541
548
  'Authorization' => ('Basic ' + Base64.strict_encode64("#{@options[:api_username]}:#{@options[:api_password]}").strip)
542
549
  }
550
+
551
+ headers['Idempotency-Key'] = idempotency_key if idempotency_key
552
+ headers
543
553
  end
544
554
 
545
555
  def build_xml_request(action, payment_method_details)
546
556
  builder = Nokogiri::XML::Builder.new
547
557
  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?
558
+ doc.send('card-transaction-type', TRANSACTIONS[action]) if TRANSACTIONS[action] && !payment_method_details.alt_transaction? && action != :refund
549
559
  yield(doc)
550
560
  end
551
561
  builder.doc.root.to_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