activemerchant 1.126.0 → 1.129.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +241 -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 +79 -23
  6. data/lib/active_merchant/billing/gateways/adyen.rb +67 -8
  7. data/lib/active_merchant/billing/gateways/airwallex.rb +40 -11
  8. data/lib/active_merchant/billing/gateways/alelo.rb +256 -0
  9. data/lib/active_merchant/billing/gateways/authorize_net.rb +21 -4
  10. data/lib/active_merchant/billing/gateways/beanstream.rb +18 -0
  11. data/lib/active_merchant/billing/gateways/blue_snap.rb +22 -1
  12. data/lib/active_merchant/billing/gateways/bogus.rb +4 -0
  13. data/lib/active_merchant/billing/gateways/borgun.rb +56 -16
  14. data/lib/active_merchant/billing/gateways/braintree_blue.rb +64 -17
  15. data/lib/active_merchant/billing/gateways/card_connect.rb +27 -9
  16. data/lib/active_merchant/billing/gateways/card_stream.rb +23 -0
  17. data/lib/active_merchant/billing/gateways/checkout_v2.rb +228 -57
  18. data/lib/active_merchant/billing/gateways/commerce_hub.rb +361 -0
  19. data/lib/active_merchant/billing/gateways/credorax.rb +47 -27
  20. data/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb +36 -0
  21. data/lib/active_merchant/billing/gateways/cyber_source.rb +100 -26
  22. data/lib/active_merchant/billing/gateways/cyber_source_rest.rb +456 -0
  23. data/lib/active_merchant/billing/gateways/d_local.rb +44 -5
  24. data/lib/active_merchant/billing/gateways/decidir.rb +15 -4
  25. data/lib/active_merchant/billing/gateways/ebanx.rb +36 -24
  26. data/lib/active_merchant/billing/gateways/element.rb +21 -1
  27. data/lib/active_merchant/billing/gateways/global_collect.rb +73 -22
  28. data/lib/active_merchant/billing/gateways/ipg.rb +13 -8
  29. data/lib/active_merchant/billing/gateways/iveri.rb +39 -3
  30. data/lib/active_merchant/billing/gateways/kushki.rb +21 -1
  31. data/lib/active_merchant/billing/gateways/litle.rb +25 -5
  32. data/lib/active_merchant/billing/gateways/mastercard.rb +1 -8
  33. data/lib/active_merchant/billing/gateways/mercado_pago.rb +17 -0
  34. data/lib/active_merchant/billing/gateways/merchant_e_solutions.rb +44 -10
  35. data/lib/active_merchant/billing/gateways/monei.rb +2 -0
  36. data/lib/active_merchant/billing/gateways/moneris.rb +20 -5
  37. data/lib/active_merchant/billing/gateways/mundipagg.rb +3 -0
  38. data/lib/active_merchant/billing/gateways/ogone.rb +35 -7
  39. data/lib/active_merchant/billing/gateways/openpay.rb +20 -3
  40. data/lib/active_merchant/billing/gateways/orbital.rb +43 -22
  41. data/lib/active_merchant/billing/gateways/pay_trace.rb +64 -18
  42. data/lib/active_merchant/billing/gateways/payeezy.rb +59 -4
  43. data/lib/active_merchant/billing/gateways/paymentez.rb +18 -6
  44. data/lib/active_merchant/billing/gateways/paypal/paypal_express_response.rb +4 -0
  45. data/lib/active_merchant/billing/gateways/paysafe.rb +22 -14
  46. data/lib/active_merchant/billing/gateways/payu_latam.rb +3 -0
  47. data/lib/active_merchant/billing/gateways/plexo.rb +308 -0
  48. data/lib/active_merchant/billing/gateways/priority.rb +29 -6
  49. data/lib/active_merchant/billing/gateways/rapyd.rb +110 -49
  50. data/lib/active_merchant/billing/gateways/reach.rb +277 -0
  51. data/lib/active_merchant/billing/gateways/redsys.rb +9 -5
  52. data/lib/active_merchant/billing/gateways/sage_pay.rb +1 -1
  53. data/lib/active_merchant/billing/gateways/securion_pay.rb +40 -0
  54. data/lib/active_merchant/billing/gateways/shift4.rb +342 -0
  55. data/lib/active_merchant/billing/gateways/simetrik.rb +28 -22
  56. data/lib/active_merchant/billing/gateways/stripe.rb +21 -1
  57. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +62 -22
  58. data/lib/active_merchant/billing/gateways/tns.rb +2 -5
  59. data/lib/active_merchant/billing/gateways/trans_first_transaction_express.rb +1 -1
  60. data/lib/active_merchant/billing/gateways/trust_commerce.rb +14 -3
  61. data/lib/active_merchant/billing/gateways/vanco.rb +12 -3
  62. data/lib/active_merchant/billing/gateways/visanet_peru.rb +1 -1
  63. data/lib/active_merchant/billing/gateways/vpos.rb +7 -4
  64. data/lib/active_merchant/billing/gateways/wompi.rb +8 -4
  65. data/lib/active_merchant/billing/gateways/worldpay.rb +117 -9
  66. data/lib/active_merchant/billing/response.rb +15 -1
  67. data/lib/active_merchant/connection.rb +0 -2
  68. data/lib/active_merchant/country.rb +1 -0
  69. data/lib/active_merchant/errors.rb +4 -1
  70. data/lib/active_merchant/version.rb +1 -1
  71. metadata +24 -3
@@ -17,12 +17,16 @@ module ActiveMerchant #:nodoc:
17
17
  self.homepage_url = 'https://www.adyen.com/'
18
18
  self.display_name = 'Adyen'
19
19
 
20
- PAYMENT_API_VERSION = 'v64'
21
- RECURRING_API_VERSION = 'v49'
20
+ PAYMENT_API_VERSION = 'v68'
21
+ RECURRING_API_VERSION = 'v68'
22
22
 
23
23
  STANDARD_ERROR_CODE_MAPPING = {
24
+ '0' => STANDARD_ERROR_CODE[:processing_error],
25
+ '10' => STANDARD_ERROR_CODE[:config_error],
26
+ '100' => STANDARD_ERROR_CODE[:invalid_amount],
24
27
  '101' => STANDARD_ERROR_CODE[:incorrect_number],
25
28
  '103' => STANDARD_ERROR_CODE[:invalid_cvc],
29
+ '104' => STANDARD_ERROR_CODE[:incorrect_address],
26
30
  '131' => STANDARD_ERROR_CODE[:incorrect_address],
27
31
  '132' => STANDARD_ERROR_CODE[:incorrect_address],
28
32
  '133' => STANDARD_ERROR_CODE[:incorrect_address],
@@ -62,6 +66,8 @@ module ActiveMerchant #:nodoc:
62
66
  add_recurring_contract(post, options)
63
67
  add_network_transaction_reference(post, options)
64
68
  add_application_info(post, options)
69
+ add_level_2_data(post, options)
70
+ add_level_3_data(post, options)
65
71
  commit('authorise', post, options)
66
72
  end
67
73
 
@@ -71,6 +77,7 @@ module ActiveMerchant #:nodoc:
71
77
  add_reference(post, authorization, options)
72
78
  add_splits(post, options)
73
79
  add_network_transaction_reference(post, options)
80
+ add_shopper_statement(post, options)
74
81
  commit('capture', post, options)
75
82
  end
76
83
 
@@ -117,7 +124,7 @@ module ActiveMerchant #:nodoc:
117
124
  add_extra_data(post, credit_card, options)
118
125
  add_stored_credentials(post, credit_card, options)
119
126
  add_address(post, options)
120
-
127
+ add_network_transaction_reference(post, options)
121
128
  options[:recurring_contract_type] ||= 'RECURRING'
122
129
  add_recurring_contract(post, options)
123
130
 
@@ -216,7 +223,7 @@ module ActiveMerchant #:nodoc:
216
223
  NETWORK_TOKENIZATION_CARD_SOURCE = {
217
224
  'apple_pay' => 'applepay',
218
225
  'android_pay' => 'androidpay',
219
- 'google_pay' => 'paywithgoogle'
226
+ 'google_pay' => 'googlepay'
220
227
  }
221
228
 
222
229
  def add_extra_data(post, payment, options)
@@ -242,6 +249,48 @@ module ActiveMerchant #:nodoc:
242
249
  add_merchant_data(post, options)
243
250
  end
244
251
 
252
+ def extract_and_transform(mapper, from)
253
+ mapper.each_with_object({}) do |key_map, hsh|
254
+ key, item_key = key_map[0], key_map[1]
255
+ hsh[key] = from[item_key.to_sym]
256
+ end
257
+ end
258
+
259
+ def add_level_2_data(post, options)
260
+ return unless options[:level_2_data].present?
261
+
262
+ mapper = {
263
+ "enhancedSchemeData.totalTaxAmount": 'total_tax_amount',
264
+ "enhancedSchemeData.customerReference": 'customer_reference'
265
+ }
266
+ post[:additionalData].merge!(extract_and_transform(mapper, options[:level_2_data]))
267
+ end
268
+
269
+ def add_level_3_data(post, options)
270
+ return unless options[:level_3_data].present?
271
+
272
+ mapper = { "enhancedSchemeData.freightAmount": 'freight_amount',
273
+ "enhancedSchemeData.destinationStateProvinceCode": 'destination_state_province_code',
274
+ "enhancedSchemeData.shipFromPostalCode": 'ship_from_postal_code',
275
+ "enhancedSchemeData.orderDate": 'order_date',
276
+ "enhancedSchemeData.destinationPostalCode": 'destination_postal_code',
277
+ "enhancedSchemeData.destinationCountryCode": 'destination_country_code',
278
+ "enhancedSchemeData.dutyAmount": 'duty_amount' }
279
+
280
+ post[:additionalData].merge!(extract_and_transform(mapper, options[:level_3_data]))
281
+
282
+ item_detail_keys = %w[description product_code quantity unit_of_measure unit_price discount_amount total_amount commodity_code]
283
+ if options[:level_3_data][:items].present?
284
+ options[:level_3_data][:items].last(9).each.with_index(1) do |item, index|
285
+ mapper = item_detail_keys.each_with_object({}) do |key, hsh|
286
+ hsh["enhancedSchemeData.itemDetailLine#{index}.#{key.camelize(:lower)}"] = key
287
+ end
288
+ post[:additionalData].merge!(extract_and_transform(mapper, item))
289
+ end
290
+ end
291
+ post[:additionalData].compact!
292
+ end
293
+
245
294
  def add_shopper_data(post, options)
246
295
  post[:shopperEmail] = options[:email] if options[:email]
247
296
  post[:shopperEmail] = options[:shopper_email] if options[:shopper_email]
@@ -251,6 +300,14 @@ module ActiveMerchant #:nodoc:
251
300
  post[:additionalData][:updateShopperStatement] = options[:update_shopper_statement] if options[:update_shopper_statement]
252
301
  end
253
302
 
303
+ def add_shopper_statement(post, options)
304
+ return unless options[:shopper_statement]
305
+
306
+ post[:additionalData] = {
307
+ shopperStatement: options[:shopper_statement]
308
+ }
309
+ end
310
+
254
311
  def add_merchant_data(post, options)
255
312
  post[:additionalData][:subMerchantID] = options[:sub_merchant_id] if options[:sub_merchant_id]
256
313
  post[:additionalData][:subMerchantName] = options[:sub_merchant_name] if options[:sub_merchant_name]
@@ -387,7 +444,7 @@ module ActiveMerchant #:nodoc:
387
444
  elsif payment.is_a?(Check)
388
445
  add_bank_account(post, payment, options, action)
389
446
  else
390
- add_mpi_data_for_network_tokenization_card(post, payment) if payment.is_a?(NetworkTokenizationCreditCard)
447
+ add_mpi_data_for_network_tokenization_card(post, payment, options) if payment.is_a?(NetworkTokenizationCreditCard)
391
448
  add_card(post, payment)
392
449
  end
393
450
  end
@@ -396,7 +453,7 @@ module ActiveMerchant #:nodoc:
396
453
  bank = {
397
454
  bankAccountNumber: bank_account.account_number,
398
455
  ownerName: bank_account.name,
399
- countryCode: options[:billing_address][:country]
456
+ countryCode: options[:billing_address].try(:[], :country)
400
457
  }
401
458
 
402
459
  action == 'refundWithData' ? bank[:iban] = bank_account.routing_number : bank[:bankLocationId] = bank_account.routing_number
@@ -438,7 +495,9 @@ module ActiveMerchant #:nodoc:
438
495
  post[:originalReference] = original_reference
439
496
  end
440
497
 
441
- def add_mpi_data_for_network_tokenization_card(post, payment)
498
+ def add_mpi_data_for_network_tokenization_card(post, payment, options)
499
+ return if options[:skip_mpi_data] == 'Y'
500
+
442
501
  post[:mpiData] = {}
443
502
  post[:mpiData][:authenticationResponse] = 'Y'
444
503
  post[:mpiData][:cavv] = payment.payment_cryptogram
@@ -616,7 +675,7 @@ module ActiveMerchant #:nodoc:
616
675
 
617
676
  def success_from(action, response, options)
618
677
  if %w[RedirectShopper ChallengeShopper].include?(response.dig('resultCode')) && !options[:execute_threed] && !options[:threed_dynamic]
619
- response['refusalReason'] = 'Received unexpected 3DS authentication response. Use the execute_threed and/or threed_dynamic options to initiate a proper 3DS flow.'
678
+ response['refusalReason'] = 'Received unexpected 3DS authentication response, but a 3DS initiation flag was not included in the request.'
620
679
  return false
621
680
  end
622
681
  case action.to_s
@@ -21,6 +21,12 @@ module ActiveMerchant #:nodoc:
21
21
  void: '/pa/payment_intents/%{id}/cancel'
22
22
  }
23
23
 
24
+ # Provided by Airwallex for testing purposes
25
+ TEST_NETWORK_TRANSACTION_IDS = {
26
+ visa: '123456789012345',
27
+ master: 'MCC123ABC0101'
28
+ }
29
+
24
30
  def initialize(options = {})
25
31
  requires!(options, :client_id, :client_api_key)
26
32
  @client_id = options[:client_id]
@@ -30,17 +36,15 @@ module ActiveMerchant #:nodoc:
30
36
  end
31
37
 
32
38
  def purchase(money, card, options = {})
33
- requires!(options, :return_url)
34
-
35
39
  payment_intent_id = create_payment_intent(money, options)
36
40
  post = {
37
41
  'request_id' => request_id(options),
38
- 'merchant_order_id' => merchant_order_id(options),
39
- 'return_url' => options[:return_url]
42
+ 'merchant_order_id' => merchant_order_id(options)
40
43
  }
41
44
  add_card(post, card, options)
42
45
  add_descriptor(post, options)
43
46
  add_stored_credential(post, options)
47
+ add_return_url(post, options)
44
48
  post['payment_method_options'] = { 'card' => { 'auto_capture' => false } } if authorization_only?(options)
45
49
 
46
50
  add_three_ds(post, options)
@@ -108,15 +112,19 @@ module ActiveMerchant #:nodoc:
108
112
  private
109
113
 
110
114
  def request_id(options)
111
- options[:request_id] || generate_timestamp
115
+ options[:request_id] || generate_uuid
112
116
  end
113
117
 
114
118
  def merchant_order_id(options)
115
- options[:merchant_order_id] || options[:order_id] || generate_timestamp
119
+ options[:merchant_order_id] || options[:order_id] || generate_uuid
116
120
  end
117
121
 
118
- def generate_timestamp
119
- (Time.now.to_f.round(2) * 100).to_i.to_s
122
+ def add_return_url(post, options)
123
+ post[:return_url] = options[:return_url] if options[:return_url]
124
+ end
125
+
126
+ def generate_uuid
127
+ SecureRandom.uuid
120
128
  end
121
129
 
122
130
  def setup_access_token
@@ -134,13 +142,19 @@ module ActiveMerchant #:nodoc:
134
142
  base_url + ENDPOINTS[action].to_s % { id: id }
135
143
  end
136
144
 
145
+ def add_referrer_data(post)
146
+ post[:referrer_data] = { type: 'spreedly' }
147
+ end
148
+
137
149
  def create_payment_intent(money, options = {})
138
150
  post = {}
139
151
  add_invoice(post, money, options)
140
152
  add_order(post, options)
141
153
  post[:request_id] = "#{request_id(options)}_setup"
142
- post[:merchant_order_id] = "#{merchant_order_id(options)}_setup"
154
+ post[:merchant_order_id] = merchant_order_id(options)
155
+ add_referrer_data(post)
143
156
  add_descriptor(post, options)
157
+ post['payment_method_options'] = { 'card' => { 'risk_control' => { 'three_ds_action' => 'SKIP_3DS' } } } if options[:skip_3ds]
144
158
 
145
159
  response = commit(:setup, post)
146
160
  raise ArgumentError.new(response.message) unless response.success?
@@ -199,7 +213,8 @@ module ActiveMerchant #:nodoc:
199
213
  'expiry_year' => card.year.to_s,
200
214
  'number' => card.number.to_s,
201
215
  'name' => card.name,
202
- 'cvc' => card.verification_value
216
+ 'cvc' => card.verification_value,
217
+ 'brand' => card.brand
203
218
  }
204
219
  }
205
220
  add_billing(post, card, options)
@@ -240,10 +255,23 @@ module ActiveMerchant #:nodoc:
240
255
  external_recurring_data[:merchant_trigger_reason] = 'unscheduled'
241
256
  end
242
257
 
243
- external_recurring_data[:original_transaction_id] = stored_credential.dig(:network_transaction_id)
258
+ external_recurring_data[:original_transaction_id] = test_mit?(options) ? test_network_transaction_id(post) : stored_credential.dig(:network_transaction_id)
244
259
  external_recurring_data[:triggered_by] = stored_credential.dig(:initiator) == 'cardholder' ? 'customer' : 'merchant'
245
260
  end
246
261
 
262
+ def test_network_transaction_id(post)
263
+ case post['payment_method']['card']['brand']
264
+ when 'visa'
265
+ TEST_NETWORK_TRANSACTION_IDS[:visa]
266
+ when 'master'
267
+ TEST_NETWORK_TRANSACTION_IDS[:master]
268
+ end
269
+ end
270
+
271
+ def test_mit?(options)
272
+ test? && options.dig(:stored_credential, :initiator) == 'merchant'
273
+ end
274
+
247
275
  def add_three_ds(post, options)
248
276
  return unless three_d_secure = options[:three_d_secure]
249
277
 
@@ -293,6 +321,7 @@ module ActiveMerchant #:nodoc:
293
321
 
294
322
  def commit(action, post, id = nil)
295
323
  url = build_request_url(action, id)
324
+
296
325
  post_headers = { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
297
326
  response = parse(ssl_post(url, post_data(post), post_headers))
298
327
 
@@ -0,0 +1,256 @@
1
+ require 'jose'
2
+
3
+ module ActiveMerchant #:nodoc:
4
+ module Billing #:nodoc:
5
+ class AleloGateway < Gateway
6
+ class_attribute :prelive_url
7
+
8
+ self.test_url = 'https://sandbox-api.alelo.com.br/alelo/sandbox/'
9
+ self.live_url = 'https://api.alelo.com.br/alelo/prd/'
10
+ self.prelive_url = 'https://api.homologacaoalelo.com.br/alelo/uat/'
11
+
12
+ self.supported_countries = ['BR']
13
+ self.default_currency = 'BRL'
14
+ self.supported_cardtypes = %i[visa master american_express discover]
15
+
16
+ self.homepage_url = 'https://www.alelo.com.br'
17
+ self.display_name = 'Alelo'
18
+
19
+ def initialize(options = {})
20
+ requires!(options, :client_id, :client_secret)
21
+ super
22
+ end
23
+
24
+ def purchase(money, payment, options = {})
25
+ post = {}
26
+ add_order(post, options)
27
+ add_amount(post, money)
28
+ add_payment(post, payment)
29
+ add_geolocation(post, options)
30
+ add_extra_data(post, options)
31
+
32
+ commit('capture/transaction', post, options)
33
+ end
34
+
35
+ def refund(money, authorization, options = {})
36
+ request_id = authorization.split('#').first
37
+ options[:http] = { method: :put, prevent_encrypt: true }
38
+ commit('capture/transaction/refund', { requestId: request_id }, options, :put)
39
+ end
40
+
41
+ def supports_scrubbing?
42
+ true
43
+ end
44
+
45
+ def scrub(transcript)
46
+ force_utf8(transcript.encode).
47
+ gsub(%r((Authorization: Bearer )[\w -]+), '\1[FILTERED]').
48
+ gsub(%r((client_id=|Client-Id:)[\w -]+), '\1[FILTERED]\2').
49
+ gsub(%r((client_secret=|Client-Secret:)[\w -]+), '\1[FILTERED]\2').
50
+ gsub(%r((access_token\":\")[^\"]*), '\1[FILTERED]').
51
+ gsub(%r((publicKey\":\")[^\"]*), '\1[FILTERED]')
52
+ end
53
+
54
+ private
55
+
56
+ def force_utf8(string)
57
+ return nil unless string
58
+
59
+ # binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?')
60
+ string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
61
+ end
62
+
63
+ def add_amount(post, money)
64
+ post[:amount] = amount(money).to_f
65
+ end
66
+
67
+ def add_order(post, options)
68
+ post[:requestId] = options[:order_id]
69
+ end
70
+
71
+ def add_extra_data(post, options)
72
+ post.merge!({
73
+ establishmentCode: options[:establishment_code],
74
+ playerIdentification: options[:player_identification],
75
+ captureType: '3', # send fixed value 3 to ecommerce
76
+ subMerchantCode: options[:sub_merchant_mcc],
77
+ externalTraceNumber: options[:external_trace_number]
78
+ }.compact)
79
+ end
80
+
81
+ def add_geolocation(post, options)
82
+ return if options[:geo_latitude].blank? || options[:geo_longitude].blank?
83
+
84
+ post.merge!(geolocation: {
85
+ latitude: options[:geo_latitude],
86
+ longitude: options[:geo_longitude]
87
+ })
88
+ end
89
+
90
+ def add_payment(post, payment)
91
+ post.merge!({
92
+ cardNumber: payment.number,
93
+ cardholderName: payment.name,
94
+ expirationMonth: payment.month,
95
+ expirationYear: format(payment.year, :two_digits).to_i,
96
+ securityCode: payment.verification_value
97
+ })
98
+ end
99
+
100
+ def fetch_access_token
101
+ params = {
102
+ grant_type: 'client_credentials',
103
+ client_id: @options[:client_id],
104
+ client_secret: @options[:client_secret],
105
+ scope: '/capture'
106
+ }
107
+
108
+ headers = {
109
+ 'Accept' => 'application/json',
110
+ 'Content-Type' => 'application/x-www-form-urlencoded'
111
+ }
112
+
113
+ parsed = parse(ssl_post(url('captura-oauth-provider/oauth/token'), post_data(params), headers))
114
+ Response.new(true, parsed[:access_token], parsed)
115
+ end
116
+
117
+ def remote_encryption_key(access_token)
118
+ response = parse(ssl_get(url('capture/key'), request_headers(access_token)))
119
+ Response.new(true, response[:publicKey], response)
120
+ end
121
+
122
+ def ensure_credentials(try_again = true)
123
+ multiresp = MultiResponse.new
124
+ access_token = @options[:access_token]
125
+ key = @options[:encryption_key]
126
+ uuid = @options[:encryption_uuid]
127
+
128
+ if access_token.blank?
129
+ multiresp.process { fetch_access_token }
130
+ access_token = multiresp.message
131
+ key = nil
132
+ uuid = nil
133
+ end
134
+
135
+ if key.blank?
136
+ multiresp.process { remote_encryption_key(access_token) }
137
+ key = multiresp.message
138
+ uuid = multiresp.params['uuid']
139
+ end
140
+
141
+ {
142
+ key: key,
143
+ uuid: uuid,
144
+ access_token: access_token,
145
+ multiresp: multiresp.responses.present? ? multiresp : nil
146
+ }
147
+ rescue ResponseError => error
148
+ # retry to generate a new access_token when the provided one is expired
149
+ raise error unless try_again && %w(401 404).include?(error.response.code) && @options[:access_token].present?
150
+
151
+ @options.delete(:access_token)
152
+ @options.delete(:encryption_key)
153
+ ensure_credentials false
154
+ end
155
+
156
+ def encrypt_payload(body, credentials, options)
157
+ key = OpenSSL::PKey::RSA.new(Base64.decode64(credentials[:key]))
158
+ jwk = JOSE::JWK.from_key(key)
159
+ alg_enc = { 'alg' => 'RSA-OAEP-256', 'enc' => 'A128CBC-HS256' }
160
+
161
+ token = JOSE::JWE.block_encrypt(jwk, body.to_json, alg_enc).compact
162
+
163
+ encrypted_body = {
164
+ token: token,
165
+ uuid: credentials[:uuid]
166
+ }
167
+
168
+ encrypted_body.to_json
169
+ end
170
+
171
+ def parse(body)
172
+ JSON.parse(body, symbolize_names: true)
173
+ end
174
+
175
+ def post_data(params)
176
+ params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
177
+ end
178
+
179
+ def commit(action, body, options, try_again = true)
180
+ credentials = ensure_credentials
181
+ payload = encrypt_payload(body, credentials, options)
182
+
183
+ if options.dig :http, :method
184
+ payload = body.to_json if options.dig :http, :prevent_encrypt
185
+ response = parse ssl_request(options[:http][:method], url(action), payload, request_headers(credentials[:access_token]))
186
+ else
187
+ response = parse ssl_post(url(action), payload, request_headers(credentials[:access_token]))
188
+ end
189
+
190
+ resp = Response.new(
191
+ success_from(action, response),
192
+ message_from(response),
193
+ response,
194
+ authorization: authorization_from(response, options),
195
+ test: test?
196
+ )
197
+
198
+ return resp unless credentials[:multiresp].present?
199
+
200
+ multiresp = credentials[:multiresp]
201
+ resp.params.merge!({
202
+ 'access_token' => credentials[:access_token],
203
+ 'encryption_key' => credentials[:key],
204
+ 'encryption_uuid' => credentials[:uuid]
205
+ })
206
+ multiresp.process { resp }
207
+
208
+ multiresp
209
+ rescue ActiveMerchant::ResponseError => e
210
+ # Retry on a possible expired encryption key
211
+ if try_again && %w(401 404).include?(e.response.code) && @options[:encryption_key].present?
212
+ @options.delete(:encryption_key)
213
+ commit(action, body, options, false)
214
+ else
215
+ res = parse(e.response.body)
216
+ Response.new(false, res[:messageUser] || res[:error], res, test: test?)
217
+ end
218
+ end
219
+
220
+ def success_from(action, response)
221
+ case action
222
+ when 'capture/transaction/refund'
223
+ response[:status] == 'ESTORNADA'
224
+ when 'capture/transaction'
225
+ response[:status] == 'CONFIRMADA'
226
+ else
227
+ false
228
+ end
229
+ end
230
+
231
+ def message_from(response)
232
+ response[:messages] || response[:messageUser]
233
+ end
234
+
235
+ def authorization_from(response, options)
236
+ [response[:requestId]].join('#')
237
+ end
238
+
239
+ def url(action)
240
+ return prelive_url if @options[:url_override] == 'prelive'
241
+
242
+ "#{test? ? test_url : live_url}#{action}"
243
+ end
244
+
245
+ def request_headers(access_token)
246
+ {
247
+ 'Accept' => 'application/json',
248
+ 'X-IBM-Client-Id' => @options[:client_id],
249
+ 'X-IBM-Client-Secret' => @options[:client_secret],
250
+ 'Content-Type' => 'application/json',
251
+ 'Authorization' => "Bearer #{access_token}"
252
+ }
253
+ end
254
+ end
255
+ end
256
+ end
@@ -90,7 +90,6 @@ module ActiveMerchant
90
90
  }.freeze
91
91
 
92
92
  APPLE_PAY_DATA_DESCRIPTOR = 'COMMON.APPLE.INAPP.PAYMENT'
93
-
94
93
  PAYMENT_METHOD_NOT_SUPPORTED_ERROR = '155'
95
94
  INELIGIBLE_FOR_ISSUING_CREDIT_ERROR = '54'
96
95
 
@@ -176,11 +175,29 @@ module ActiveMerchant
176
175
  end
177
176
  end
178
177
 
179
- def verify(credit_card, options = {})
178
+ def verify(payment_method, options = {})
179
+ amount = amount_for_verify(options)
180
+
180
181
  MultiResponse.run(:use_first_response) do |r|
181
- r.process { authorize(100, credit_card, options) }
182
- r.process(:ignore_result) { void(r.authorization, options) }
182
+ r.process { authorize(amount, payment_method, options) }
183
+ r.process(:ignore_result) { void(r.authorization, options) } unless amount == 0
183
184
  end
185
+ rescue ArgumentError => e
186
+ Response.new(false, e.message)
187
+ end
188
+
189
+ def amount_for_verify(options)
190
+ return 100 unless options[:verify_amount].present?
191
+
192
+ amount = options[:verify_amount]
193
+ raise ArgumentError.new 'verify_amount value must be an integer' unless amount.is_a?(Integer) && !amount.negative? || amount.is_a?(String) && amount.match?(/^\d+$/) && !amount.to_i.negative?
194
+ raise ArgumentError.new 'Billing address including zip code is required for a 0 amount verify' if amount.to_i.zero? && !validate_billing_address_values?(options)
195
+
196
+ amount.to_i
197
+ end
198
+
199
+ def validate_billing_address_values?(options)
200
+ options.dig(:billing_address, :zip).present? && options.dig(:billing_address, :address1).present?
184
201
  end
185
202
 
186
203
  def store(credit_card, options = {})
@@ -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
@@ -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 = {})
@@ -179,6 +181,7 @@ module ActiveMerchant
179
181
  doc.send('store-card', options[:store_card] || false)
180
182
  add_amount(doc, money, options)
181
183
  add_fraud_info(doc, payment_method, options)
184
+ add_stored_credentials(doc, options)
182
185
 
183
186
  if payment_method.is_a?(String)
184
187
  doc.send('vaulted-shopper-id', payment_method)
@@ -190,6 +193,19 @@ module ActiveMerchant
190
193
  end
191
194
  end
192
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
+
193
209
  def add_amount(doc, money, options)
194
210
  currency = options[:currency] || currency(money)
195
211
  doc.amount(localized_amount(money, currency))
@@ -244,6 +260,7 @@ module ActiveMerchant
244
260
  def add_order(doc, options)
245
261
  doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
246
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]
247
264
  add_metadata(doc, options)
248
265
  add_3ds(doc, options[:three_d_secure]) if options[:three_d_secure]
249
266
  add_level_3_data(doc, options)
@@ -350,6 +367,7 @@ module ActiveMerchant
350
367
  def add_alt_transaction_purchase(doc, money, payment_method_details, options)
351
368
  doc.send('merchant-transaction-id', truncate(options[:order_id], 50)) if options[:order_id]
352
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]
353
371
  add_amount(doc, money, options)
354
372
 
355
373
  vaulted_shopper_id = payment_method_details.vaulted_shopper_id
@@ -358,6 +376,7 @@ module ActiveMerchant
358
376
  add_echeck_transaction(doc, payment_method_details.payment_method, options, vaulted_shopper_id.present?) if payment_method_details.check?
359
377
 
360
378
  add_fraud_info(doc, payment_method_details.payment_method, options)
379
+ add_stored_credentials(doc, options)
361
380
  add_metadata(doc, options)
362
381
  end
363
382
 
@@ -512,7 +531,9 @@ module ActiveMerchant
512
531
  end
513
532
 
514
533
  def authorization_from(action, parsed_response, payment_method_details)
515
- 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']
516
537
  end
517
538
 
518
539
  def vaulted_shopper_id(parsed_response, payment_method_details)
@@ -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$/