activemerchant 1.123.0 → 1.126.0

Sign up to get free protection for your applications and to get access to all the features.
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