activemerchant 1.116.0 → 1.121.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +148 -1
  3. data/README.md +4 -2
  4. data/lib/active_merchant/billing/check.rb +10 -0
  5. data/lib/active_merchant/billing/credit_card.rb +3 -0
  6. data/lib/active_merchant/billing/credit_card_methods.rb +80 -15
  7. data/lib/active_merchant/billing/gateways/adyen.rb +29 -8
  8. data/lib/active_merchant/billing/gateways/authorize_net.rb +37 -1
  9. data/lib/active_merchant/billing/gateways/authorize_net_cim.rb +4 -0
  10. data/lib/active_merchant/billing/gateways/blue_snap.rb +3 -1
  11. data/lib/active_merchant/billing/gateways/braintree_blue.rb +54 -7
  12. data/lib/active_merchant/billing/gateways/cashnet.rb +7 -2
  13. data/lib/active_merchant/billing/gateways/checkout_v2.rb +33 -2
  14. data/lib/active_merchant/billing/gateways/credorax.rb +30 -14
  15. data/lib/active_merchant/billing/gateways/cyber_source.rb +51 -8
  16. data/lib/active_merchant/billing/gateways/d_local.rb +1 -1
  17. data/lib/active_merchant/billing/gateways/decidir.rb +22 -2
  18. data/lib/active_merchant/billing/gateways/elavon.rb +54 -2
  19. data/lib/active_merchant/billing/gateways/eway_rapid.rb +13 -0
  20. data/lib/active_merchant/billing/gateways/firstdata_e4_v27.rb +17 -6
  21. data/lib/active_merchant/billing/gateways/forte.rb +12 -0
  22. data/lib/active_merchant/billing/gateways/global_collect.rb +25 -6
  23. data/lib/active_merchant/billing/gateways/hps.rb +65 -2
  24. data/lib/active_merchant/billing/gateways/litle.rb +21 -5
  25. data/lib/active_merchant/billing/gateways/mercado_pago.rb +2 -2
  26. data/lib/active_merchant/billing/gateways/netbanx.rb +37 -2
  27. data/lib/active_merchant/billing/gateways/orbital.rb +178 -45
  28. data/lib/active_merchant/billing/gateways/payeezy.rb +53 -11
  29. data/lib/active_merchant/billing/gateways/payment_express.rb +10 -5
  30. data/lib/active_merchant/billing/gateways/paymentez.rb +21 -1
  31. data/lib/active_merchant/billing/gateways/paypal.rb +10 -2
  32. data/lib/active_merchant/billing/gateways/paypal/paypal_common_api.rb +1 -0
  33. data/lib/active_merchant/billing/gateways/paypal_express.rb +1 -0
  34. data/lib/active_merchant/billing/gateways/payway_dot_com.rb +253 -0
  35. data/lib/active_merchant/billing/gateways/pin.rb +11 -0
  36. data/lib/active_merchant/billing/gateways/qvalent.rb +23 -9
  37. data/lib/active_merchant/billing/gateways/redsys.rb +101 -5
  38. data/lib/active_merchant/billing/gateways/safe_charge.rb +39 -6
  39. data/lib/active_merchant/billing/gateways/stripe.rb +9 -9
  40. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +82 -25
  41. data/lib/active_merchant/billing/gateways/vpos.rb +177 -0
  42. data/lib/active_merchant/billing/gateways/worldpay.rb +31 -14
  43. data/lib/active_merchant/billing/response.rb +2 -1
  44. data/lib/active_merchant/version.rb +1 -1
  45. data/lib/certs/cacert.pem +1582 -2431
  46. metadata +5 -3
@@ -30,6 +30,7 @@ module ActiveMerchant #:nodoc:
30
30
  add_address(post, creditcard, options)
31
31
  add_capture(post, options)
32
32
  add_metadata(post, options)
33
+ add_3ds(post, options)
33
34
 
34
35
  commit(:post, 'charges', post, options)
35
36
  end
@@ -158,6 +159,16 @@ module ActiveMerchant #:nodoc:
158
159
  post[:metadata] = options[:metadata] if options[:metadata]
159
160
  end
160
161
 
162
+ def add_3ds(post, options)
163
+ if options[:three_d_secure]
164
+ post[:three_d_secure] = {}
165
+ post[:three_d_secure][:version] = options[:three_d_secure][:version] if options[:three_d_secure][:version]
166
+ post[:three_d_secure][:eci] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
167
+ post[:three_d_secure][:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
168
+ post[:three_d_secure][:transaction_id] = options[:three_d_secure][:ds_transaction_id] || options[:three_d_secure][:xid]
169
+ end
170
+ end
171
+
161
172
  def headers(params = {})
162
173
  result = {
163
174
  'Content-Type' => 'application/json',
@@ -17,7 +17,7 @@ module ActiveMerchant #:nodoc:
17
17
  }
18
18
 
19
19
  def initialize(options = {})
20
- requires!(options, :username, :password, :merchant, :pem, :pem_password)
20
+ requires!(options, :username, :password, :merchant, :pem)
21
21
  super
22
22
  end
23
23
 
@@ -30,6 +30,7 @@ module ActiveMerchant #:nodoc:
30
30
  add_stored_credential_data(post, payment_method, options)
31
31
  add_customer_data(post, options)
32
32
  add_soft_descriptors(post, options)
33
+ add_customer_reference(post, options)
33
34
 
34
35
  commit('capture', post)
35
36
  end
@@ -43,6 +44,7 @@ module ActiveMerchant #:nodoc:
43
44
  add_stored_credential_data(post, payment_method, options)
44
45
  add_customer_data(post, options)
45
46
  add_soft_descriptors(post, options)
47
+ add_customer_reference(post, options)
46
48
 
47
49
  commit('preauth', post)
48
50
  end
@@ -53,6 +55,7 @@ module ActiveMerchant #:nodoc:
53
55
  add_reference(post, authorization, options)
54
56
  add_customer_data(post, options)
55
57
  add_soft_descriptors(post, options)
58
+ add_customer_reference(post, options)
56
59
 
57
60
  commit('captureWithoutAuth', post)
58
61
  end
@@ -64,6 +67,7 @@ module ActiveMerchant #:nodoc:
64
67
  add_customer_data(post, options)
65
68
  add_soft_descriptors(post, options)
66
69
  post['order.ECI'] = options[:eci] || 'SSL'
70
+ add_customer_reference(post, options)
67
71
 
68
72
  commit('refund', post)
69
73
  end
@@ -76,6 +80,7 @@ module ActiveMerchant #:nodoc:
76
80
  add_payment_method(post, payment_method)
77
81
  add_customer_data(post, options)
78
82
  add_soft_descriptors(post, options)
83
+ add_customer_reference(post, options)
79
84
 
80
85
  commit('refund', post)
81
86
  end
@@ -85,6 +90,7 @@ module ActiveMerchant #:nodoc:
85
90
  add_reference(post, authorization, options)
86
91
  add_customer_data(post, options)
87
92
  add_soft_descriptors(post, options)
93
+ add_customer_reference(post, options)
88
94
 
89
95
  commit('reversal', post)
90
96
  end
@@ -92,7 +98,7 @@ module ActiveMerchant #:nodoc:
92
98
  def store(payment_method, options = {})
93
99
  post = {}
94
100
  add_payment_method(post, payment_method)
95
- add_card_reference(post)
101
+ add_card_reference(post, options)
96
102
 
97
103
  commit('registerAccount', post)
98
104
  end
@@ -149,12 +155,16 @@ module ActiveMerchant #:nodoc:
149
155
  return unless payment_method.brand == 'visa'
150
156
 
151
157
  stored_credential = options[:stored_credential]
152
- if stored_credential[:initial_transaction]
153
- post['card.storedCredentialUsage'] = 'INITIAL_STORAGE'
154
- elsif stored_credential[:reason_type] == ('recurring' || 'installment')
158
+ if stored_credential[:reason_type] == 'unscheduled'
159
+ if stored_credential[:initiator] == 'merchant'
160
+ post['card.storedCredentialUsage'] = 'UNSCHEDULED_MIT'
161
+ elsif stored_credential[:initiator] == 'customer'
162
+ post['card.storedCredentialUsage'] = 'UNSCHEDULED_CIT'
163
+ end
164
+ elsif stored_credential[:reason_type] == 'recurring'
155
165
  post['card.storedCredentialUsage'] = 'RECURRING'
156
- elsif stored_credential[:reason_type] == 'unscheduled'
157
- post['card.storedCredentialUsage'] = 'UNSCHEDULED'
166
+ elsif stored_credential[:reason_type] == 'installment'
167
+ post['card.storedCredentialUsage'] = 'INSTALLMENT'
158
168
  end
159
169
  end
160
170
 
@@ -181,8 +191,12 @@ module ActiveMerchant #:nodoc:
181
191
  post['card.CVN'] = payment_method.verification_value
182
192
  end
183
193
 
184
- def add_card_reference(post)
185
- post['customer.customerReferenceNumber'] = options[:order_id]
194
+ def add_card_reference(post, options)
195
+ post['customer.customerReferenceNumber'] = options[:customer_reference_number] || options[:order_id]
196
+ end
197
+
198
+ def add_customer_reference(post, options)
199
+ post['customer.customerReferenceNumber'] = options[:customer_reference_number] if options[:customer_reference_number]
186
200
  end
187
201
 
188
202
  def add_reference(post, authorization, options)
@@ -126,6 +126,7 @@ module ActiveMerchant #:nodoc:
126
126
  184 => 'Authentication error',
127
127
  190 => 'Refusal with no specific reason',
128
128
  191 => 'Expiry date incorrect',
129
+ 195 => 'Requires SCA authentication',
129
130
 
130
131
  201 => 'Card expired',
131
132
  202 => 'Card blocked temporarily or under suspicion of fraud',
@@ -170,6 +171,10 @@ module ActiveMerchant #:nodoc:
170
171
  9914 => 'KO Confirmation'
171
172
  }
172
173
 
174
+ # Expected values as per documentation
175
+ THREE_DS_V1 = '1.0.2'
176
+ THREE_DS_V2 = '2.1.0'
177
+
173
178
  # Creates a new instance
174
179
  #
175
180
  # Redsys requires a login and secret_key, and optionally also accepts a
@@ -197,10 +202,13 @@ module ActiveMerchant #:nodoc:
197
202
  add_amount(data, money, options)
198
203
  add_order(data, options[:order_id])
199
204
  add_payment(data, payment)
205
+ add_external_mpi_fields(data, options)
200
206
  add_threeds(data, options) if options[:execute_threed]
207
+ add_stored_credential_options(data, options)
201
208
  data[:description] = options[:description]
202
209
  data[:store_in_vault] = options[:store]
203
210
  data[:sca_exemption] = options[:sca_exemption]
211
+ data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled]
204
212
 
205
213
  commit data, options
206
214
  end
@@ -213,10 +221,13 @@ module ActiveMerchant #:nodoc:
213
221
  add_amount(data, money, options)
214
222
  add_order(data, options[:order_id])
215
223
  add_payment(data, payment)
224
+ add_external_mpi_fields(data, options)
216
225
  add_threeds(data, options) if options[:execute_threed]
226
+ add_stored_credential_options(data, options)
217
227
  data[:description] = options[:description]
218
228
  data[:store_in_vault] = options[:store]
219
229
  data[:sca_exemption] = options[:sca_exemption]
230
+ data[:sca_exemption_direct_payment_enabled] = options[:sca_exemption_direct_payment_enabled]
220
231
 
221
232
  commit data, options
222
233
  end
@@ -321,6 +332,46 @@ module ActiveMerchant #:nodoc:
321
332
  end
322
333
  end
323
334
 
335
+ def add_external_mpi_fields(data, options)
336
+ return unless options[:three_d_secure]
337
+
338
+ if options[:three_d_secure][:version] == THREE_DS_V2
339
+ data[:threeDSServerTransID] = options[:three_d_secure][:three_ds_server_trans_id] if options[:three_d_secure][:three_ds_server_trans_id]
340
+ data[:dsTransID] = options[:three_d_secure][:ds_transaction_id] if options[:three_d_secure][:ds_transaction_id]
341
+ data[:authenticacionValue] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
342
+ data[:protocolVersion] = options[:three_d_secure][:version] if options[:three_d_secure][:version]
343
+ data[:authenticacionMethod] = options[:authentication_method] if options[:authentication_method]
344
+ data[:authenticacionType] = options[:authentication_type] if options[:authentication_type]
345
+ data[:authenticacionFlow] = options[:authentication_flow] if options[:authentication_flow]
346
+ data[:eci_v2] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
347
+ elsif options[:three_d_secure][:version] == THREE_DS_V1
348
+ data[:txid] = options[:three_d_secure][:xid] if options[:three_d_secure][:xid]
349
+ data[:cavv] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
350
+ data[:eci_v1] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
351
+ end
352
+ end
353
+
354
+ def add_stored_credential_options(data, options)
355
+ return unless options[:stored_credential]
356
+
357
+ case options[:stored_credential][:initial_transaction]
358
+ when true
359
+ data[:DS_MERCHANT_COF_INI] = 'S'
360
+ when false
361
+ data[:DS_MERCHANT_COF_INI] = 'N'
362
+ data[:DS_MERCHANT_COF_TXNID] = options[:stored_credential][:network_transaction_id] if options[:stored_credential][:network_transaction_id]
363
+ end
364
+
365
+ case options[:stored_credential][:reason_type]
366
+ when 'recurring'
367
+ data[:DS_MERCHANT_COF_TYPE] = 'R'
368
+ when 'installment'
369
+ data[:DS_MERCHANT_COF_TYPE] = 'I'
370
+ when 'unscheduled'
371
+ return
372
+ end
373
+ end
374
+
324
375
  def add_threeds(data, options)
325
376
  data[:threeds] = { threeDSInfo: 'CardData' } if options[:execute_threed] == true
326
377
  end
@@ -348,7 +399,8 @@ module ActiveMerchant #:nodoc:
348
399
  REQUEST
349
400
  parse(ssl_post(threeds_url, request, headers(action)), action)
350
401
  else
351
- parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data, options))}", headers), action)
402
+ xmlreq = xml_request_from(data, options)
403
+ parse(ssl_post(url, "entrada=#{CGI.escape(xmlreq)}", headers), action)
352
404
  end
353
405
  end
354
406
 
@@ -418,7 +470,14 @@ module ActiveMerchant #:nodoc:
418
470
  xml.target!
419
471
  end
420
472
 
473
+ # Template Method to allow AM API clients to override decision to escape, based on their own criteria.
474
+ def escape_special_chars?(data, options = {})
475
+ data[:threeds]
476
+ end
477
+
421
478
  def build_merchant_data(xml, data, options = {})
479
+ # See https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2/wsdl/SerClsWSEntradaV2.wsdl
480
+ # (which results from calling #threeds_url + '?WSDL', https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2?WSDL)
422
481
  xml.DATOSENTRADA do
423
482
  # Basic elements
424
483
  xml.DS_Version 0.1
@@ -426,7 +485,7 @@ module ActiveMerchant #:nodoc:
426
485
  xml.DS_MERCHANT_AMOUNT data[:amount]
427
486
  xml.DS_MERCHANT_ORDER data[:order_id]
428
487
  xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
429
- if data[:description] && data[:threeds]
488
+ if data[:description] && escape_special_chars?(data, options)
430
489
  xml.DS_MERCHANT_PRODUCTDESCRIPTION CGI.escape(data[:description])
431
490
  else
432
491
  xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description]
@@ -434,11 +493,18 @@ module ActiveMerchant #:nodoc:
434
493
  xml.DS_MERCHANT_TERMINAL options[:terminal] || @options[:terminal]
435
494
  xml.DS_MERCHANT_MERCHANTCODE @options[:login]
436
495
  xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication?
437
- xml.DS_MERCHANT_EXCEP_SCA data[:sca_exemption] if data[:sca_exemption]
496
+
497
+ action = determine_3ds_action(data[:threeds]) if data[:threeds]
498
+ if action == 'iniciaPeticion' && data[:sca_exemption]
499
+ xml.DS_MERCHANT_EXCEP_SCA 'Y'
500
+ else
501
+ xml.DS_MERCHANT_EXCEP_SCA data[:sca_exemption] if data[:sca_exemption]
502
+ xml.DS_MERCHANT_DIRECTPAYMENT data[:sca_exemption_direct_payment_enabled] if data[:sca_exemption_direct_payment_enabled]
503
+ end
438
504
 
439
505
  # Only when card is present
440
506
  if data[:card]
441
- if data[:card][:name] && data[:threeds]
507
+ if data[:card][:name] && escape_special_chars?(data, options)
442
508
  xml.DS_MERCHANT_TITULAR CGI.escape(data[:card][:name])
443
509
  else
444
510
  xml.DS_MERCHANT_TITULAR data[:card][:name]
@@ -447,6 +513,9 @@ module ActiveMerchant #:nodoc:
447
513
  xml.DS_MERCHANT_EXPIRYDATE data[:card][:date]
448
514
  xml.DS_MERCHANT_CVV2 data[:card][:cvv]
449
515
  xml.DS_MERCHANT_IDENTIFIER 'REQUIRED' if data[:store_in_vault]
516
+
517
+ build_merchant_mpi_external(xml, data)
518
+
450
519
  elsif data[:credit_card_token]
451
520
  xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token]
452
521
  xml.DS_MERCHANT_DIRECTPAYMENT 'true'
@@ -457,9 +526,36 @@ module ActiveMerchant #:nodoc:
457
526
  xml.DS_MERCHANT_DIRECTPAYMENT 'moto' if options.dig(:moto) && options.dig(:metadata, :manual_entry)
458
527
 
459
528
  xml.DS_MERCHANT_EMV3DS data[:threeds].to_json if data[:threeds]
529
+
530
+ if options[:stored_credential]
531
+ xml.DS_MERCHANT_COF_INI data[:DS_MERCHANT_COF_INI]
532
+ xml.DS_MERCHANT_COF_TYPE data[:DS_MERCHANT_COF_TYPE]
533
+ xml.DS_MERCHANT_COF_TXNID data[:DS_MERCHANT_COF_TXNID] if data[:DS_MERCHANT_COF_TXNID]
534
+ end
460
535
  end
461
536
  end
462
537
 
538
+ def build_merchant_mpi_external(xml, data)
539
+ return unless data[:txid] || data[:threeDSServerTransID]
540
+
541
+ ds_merchant_mpi_external = {}
542
+ ds_merchant_mpi_external[:TXID] = data[:txid] if data[:txid]
543
+ ds_merchant_mpi_external[:CAVV] = data[:cavv] if data[:cavv]
544
+ ds_merchant_mpi_external[:ECI] = data[:eci_v1] if data[:eci_v1]
545
+
546
+ ds_merchant_mpi_external[:threeDSServerTransID] = data[:threeDSServerTransID] if data[:threeDSServerTransID]
547
+ ds_merchant_mpi_external[:dsTransID] = data[:dsTransID] if data[:dsTransID]
548
+ ds_merchant_mpi_external[:authenticacionValue] = data[:authenticacionValue] if data[:authenticacionValue]
549
+ ds_merchant_mpi_external[:protocolVersion] = data[:protocolVersion] if data[:protocolVersion]
550
+ ds_merchant_mpi_external[:Eci] = data[:eci_v2] if data[:eci_v2]
551
+ ds_merchant_mpi_external[:authenticacionMethod] = data[:authenticacionMethod] if data[:authenticacionMethod]
552
+ ds_merchant_mpi_external[:authenticacionType] = data[:authenticacionType] if data[:authenticacionType]
553
+ ds_merchant_mpi_external[:authenticacionFlow] = data[:authenticacionFlow] if data[:authenticacionFlow]
554
+
555
+ xml.DS_MERCHANT_MPIEXTERNAL ds_merchant_mpi_external.to_json unless ds_merchant_mpi_external.empty?
556
+ xml.target!
557
+ end
558
+
463
559
  def parse(data, action)
464
560
  params = {}
465
561
  success = false
@@ -542,7 +638,7 @@ module ActiveMerchant #:nodoc:
542
638
  def response_text(code)
543
639
  code = code.to_i
544
640
  code = 0 if code < 100
545
- RESPONSE_TEXTS[code] || 'Unkown code, please check in manual'
641
+ RESPONSE_TEXTS[code] || 'Unknown code, please check in manual'
546
642
  end
547
643
 
548
644
  def response_text_3ds(xml, params)
@@ -22,8 +22,19 @@ module ActiveMerchant #:nodoc:
22
22
 
23
23
  def purchase(money, payment, options = {})
24
24
  post = {}
25
- post[:sg_APIType] = 1 if options[:three_d_secure]
26
- trans_type = options[:three_d_secure] ? 'Sale3D' : 'Sale'
25
+
26
+ # Determine if 3DS is requested, or there is standard external MPI data
27
+ if options[:three_d_secure]
28
+ if options[:three_d_secure].is_a?(Hash)
29
+ add_external_mpi_data(post, options)
30
+ else
31
+ post[:sg_APIType] = 1
32
+ trans_type = 'Sale3D'
33
+ end
34
+ end
35
+
36
+ trans_type ||= 'Sale'
37
+
27
38
  add_transaction_data(trans_type, post, money, options)
28
39
  add_payment(post, payment, options)
29
40
  add_customer_details(post, payment, options)
@@ -33,6 +44,8 @@ module ActiveMerchant #:nodoc:
33
44
 
34
45
  def authorize(money, payment, options = {})
35
46
  post = {}
47
+
48
+ add_external_mpi_data(post, options) if options[:three_d_secure]&.is_a?(Hash)
36
49
  add_transaction_data('Auth', post, money, options)
37
50
  add_payment(post, payment, options)
38
51
  add_customer_details(post, payment, options)
@@ -69,8 +82,10 @@ module ActiveMerchant #:nodoc:
69
82
 
70
83
  def credit(money, payment, options = {})
71
84
  post = {}
85
+
72
86
  add_payment(post, payment, options)
73
87
  add_transaction_data('Credit', post, money, options)
88
+
74
89
  post[:sg_CreditType] = 1
75
90
 
76
91
  commit(post)
@@ -131,12 +146,20 @@ module ActiveMerchant #:nodoc:
131
146
  end
132
147
 
133
148
  def add_payment(post, payment, options = {})
134
- post[:sg_NameOnCard] = payment.name
135
- post[:sg_CardNumber] = payment.number
136
149
  post[:sg_ExpMonth] = format(payment.month, :two_digits)
137
150
  post[:sg_ExpYear] = format(payment.year, :two_digits)
138
- post[:sg_CVV2] = payment.verification_value
139
- post[:sg_StoredCredentialMode] = (options[:stored_credential_mode] == true ? 1 : 0)
151
+ post[:sg_CardNumber] = payment.number
152
+
153
+ if payment.is_a?(NetworkTokenizationCreditCard) && payment.source == :network_token
154
+ post[:sg_CAVV] = payment.payment_cryptogram
155
+ post[:sg_ECI] = options[:three_d_secure] && options[:three_d_secure][:eci] || '05'
156
+ post[:sg_IsExternalMPI] = 1
157
+ post[:sg_ExternalTokenProvider] = 5
158
+ else
159
+ post[:sg_CVV2] = payment.verification_value
160
+ post[:sg_NameOnCard] = payment.name
161
+ post[:sg_StoredCredentialMode] = (options[:stored_credential_mode] == true ? 1 : 0)
162
+ end
140
163
  end
141
164
 
142
165
  def add_customer_details(post, payment, options)
@@ -154,6 +177,16 @@ module ActiveMerchant #:nodoc:
154
177
  post[:sg_Email] = options[:email]
155
178
  end
156
179
 
180
+ def add_external_mpi_data(post, options)
181
+ post[:sg_ECI] = options[:three_d_secure][:eci] if options[:three_d_secure][:eci]
182
+ post[:sg_CAVV] = options[:three_d_secure][:cavv] if options[:three_d_secure][:cavv]
183
+ post[:sg_dsTransID] = options[:three_d_secure][:ds_transaction_id] if options[:three_d_secure][:ds_transaction_id]
184
+ post[:sg_threeDSProtocolVersion] = options[:three_d_secure][:ds_transaction_id] ? '2' : '1'
185
+ post[:sg_Xid] = options[:three_d_secure][:xid]
186
+ post[:sg_IsExternalMPI] = 1
187
+ post[:sg_EnablePartialApproval] = options[:is_partial_approval]
188
+ end
189
+
157
190
  def parse(xml)
158
191
  response = {}
159
192
 
@@ -28,7 +28,7 @@ module ActiveMerchant #:nodoc:
28
28
  self.supported_countries = %w(AT AU BE BG BR CA CH CY CZ DE DK EE ES FI FR GB GR HK IE IT JP LT LU LV MT MX NL NO NZ PL PT RO SE SG SI SK US)
29
29
  self.default_currency = 'USD'
30
30
  self.money_format = :cents
31
- self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro]
31
+ self.supported_cardtypes = %i[visa master american_express discover jcb diners_club maestro unionpay]
32
32
  self.currencies_without_fractions = %w(BIF CLP DJF GNF JPY KMF KRW MGA PYG RWF VND VUV XAF XOF XPF UGX)
33
33
 
34
34
  self.homepage_url = 'https://stripe.com/'
@@ -279,14 +279,14 @@ module ActiveMerchant #:nodoc:
279
279
  transcript.
280
280
  gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
281
281
  gsub(%r((&?three_d_secure\[cryptogram\]=)[\w=]*(&?)), '\1[FILTERED]\2').
282
- gsub(%r((card\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\2').
283
- gsub(%r((card\[cvc\]=)\d+), '\1[FILTERED]').
284
- gsub(%r((card\[emv_approval_data\]=)[^&]+(&?)), '\1[FILTERED]\2').
285
- gsub(%r((card\[emv_auth_data\]=)[^&]+(&?)), '\1[FILTERED]\2').
286
- gsub(%r((card\[encrypted_pin\]=)[^&]+(&?)), '\1[FILTERED]\2').
287
- gsub(%r((card\[encrypted_pin_key_id\]=)[\w=]+(&?)), '\1[FILTERED]\2').
288
- gsub(%r((card\[number\]=)\d+), '\1[FILTERED]').
289
- gsub(%r((card\[swipe_data\]=)[^&]+(&?)), '\1[FILTERED]\2')
282
+ gsub(%r(((\[card\]|card)\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\3').
283
+ gsub(%r(((\[card\]|card)\[cvc\]=)\d+), '\1[FILTERED]').
284
+ gsub(%r(((\[card\]|card)\[emv_approval_data\]=)[^&]+(&?)), '\1[FILTERED]\3').
285
+ gsub(%r(((\[card\]|card)\[emv_auth_data\]=)[^&]+(&?)), '\1[FILTERED]\3').
286
+ gsub(%r(((\[card\]|card)\[encrypted_pin\]=)[^&]+(&?)), '\1[FILTERED]\3').
287
+ gsub(%r(((\[card\]|card)\[encrypted_pin_key_id\]=)[\w=]+(&?)), '\1[FILTERED]\3').
288
+ gsub(%r(((\[card\]|card)\[number\]=)\d+), '\1[FILTERED]').
289
+ gsub(%r(((\[card\]|card)\[swipe_data\]=)[^&]+(&?)), '\1[FILTERED]\3')
290
290
  end
291
291
 
292
292
  def supports_network_tokenization?
@@ -23,12 +23,15 @@ module ActiveMerchant #:nodoc:
23
23
  payment_method = add_payment_method_token(post, payment_method, options)
24
24
  return payment_method if payment_method.is_a?(ActiveMerchant::Billing::Response)
25
25
 
26
+ add_external_three_d_secure_auth_data(post, options)
26
27
  add_metadata(post, options)
27
28
  add_return_url(post, options)
28
29
  add_connected_account(post, options)
29
30
  add_shipping_address(post, options)
30
31
  setup_future_usage(post, options)
31
32
  add_exemption(post, options)
33
+ add_stored_credentials(post, options)
34
+ add_error_on_requires_action(post, options)
32
35
  request_three_d_secure(post, options)
33
36
 
34
37
  CREATE_INTENT_ATTRIBUTES.each do |attribute|
@@ -54,16 +57,22 @@ module ActiveMerchant #:nodoc:
54
57
  end
55
58
 
56
59
  def create_payment_method(payment_method, options = {})
57
- post = {}
58
- post[:type] = 'card'
59
- post[:card] = {}
60
- post[:card][:number] = payment_method.number
61
- post[:card][:exp_month] = payment_method.month
62
- post[:card][:exp_year] = payment_method.year
63
- post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
64
- add_billing_address(post, options)
60
+ post_data = create_payment_method_data(payment_method, options)
61
+
65
62
  options = format_idempotency_key(options, 'pm')
66
- commit(:post, 'payment_methods', post, options)
63
+ commit(:post, 'payment_methods', post_data, options)
64
+ end
65
+
66
+ def create_payment_method_data(payment_method, options = {})
67
+ post_data = {}
68
+ post_data[:type] = 'card'
69
+ post_data[:card] = {}
70
+ post_data[:card][:number] = payment_method.number
71
+ post_data[:card][:exp_month] = payment_method.month
72
+ post_data[:card][:exp_year] = payment_method.year
73
+ post_data[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
74
+ add_billing_address(post_data, options)
75
+ post_data
67
76
  end
68
77
 
69
78
  def update_intent(money, intent_id, payment_method, options = {})
@@ -100,6 +109,17 @@ module ActiveMerchant #:nodoc:
100
109
  commit(:post, 'setup_intents', post, options)
101
110
  end
102
111
 
112
+ def retrieve_setup_intent(setup_intent_id)
113
+ # Retrieving a setup_intent passing 'expand[]=latest_attempt' allows the caller to
114
+ # check for a network_transaction_id and ds_transaction_id
115
+ # eg (latest_attempt -> payment_method_details -> card -> network_transaction_id)
116
+ #
117
+ # Being able to retrieve these fields enables payment flows that rely on MIT exemptions, e.g: off_session
118
+ commit(:post, "setup_intents/#{setup_intent_id}", {
119
+ 'expand[]': 'latest_attempt'
120
+ }, {})
121
+ end
122
+
103
123
  def authorize(money, payment_method, options = {})
104
124
  create_intent(money, payment_method, options.merge!(confirm: true, capture_method: 'manual'))
105
125
  end
@@ -163,7 +183,6 @@ module ActiveMerchant #:nodoc:
163
183
  if options[:customer]
164
184
  customer_id = options[:customer]
165
185
  else
166
- post[:validate] = options[:validate] unless options[:validate].nil?
167
186
  post[:description] = options[:description] if options[:description]
168
187
  post[:email] = options[:email] if options[:email]
169
188
  options = format_idempotency_key(options, 'customer')
@@ -171,7 +190,9 @@ module ActiveMerchant #:nodoc:
171
190
  customer_id = customer.params['id']
172
191
  end
173
192
  options = format_idempotency_key(options, 'attach')
174
- commit(:post, "payment_methods/#{params[:payment_method]}/attach", { customer: customer_id }, options)
193
+ attach_parameters = { customer: customer_id }
194
+ attach_parameters[:validate] = options[:validate] unless options[:validate].nil?
195
+ commit(:post, "payment_methods/#{params[:payment_method]}/attach", attach_parameters, options)
175
196
  else
176
197
  super(payment_method, options)
177
198
  end
@@ -192,6 +213,10 @@ module ActiveMerchant #:nodoc:
192
213
 
193
214
  private
194
215
 
216
+ def off_session_request?(options = {})
217
+ (options[:off_session] || options[:setup_future_usage]) && options[:confirm] == true
218
+ end
219
+
195
220
  def add_connected_account(post, options = {})
196
221
  super(post, options)
197
222
  post[:application_fee_amount] = options[:application_fee] if options[:application_fee]
@@ -199,25 +224,21 @@ module ActiveMerchant #:nodoc:
199
224
 
200
225
  def add_whitelisted_attribute(post, options, attribute)
201
226
  post[attribute] = options[attribute] if options[attribute]
202
- post
203
227
  end
204
228
 
205
229
  def add_capture_method(post, options)
206
230
  capture_method = options[:capture_method].to_s
207
231
  post[:capture_method] = capture_method if ALLOWED_METHOD_STATES.include?(capture_method)
208
- post
209
232
  end
210
233
 
211
234
  def add_confirmation_method(post, options)
212
235
  confirmation_method = options[:confirmation_method].to_s
213
236
  post[:confirmation_method] = confirmation_method if ALLOWED_METHOD_STATES.include?(confirmation_method)
214
- post
215
237
  end
216
238
 
217
239
  def add_customer(post, options)
218
240
  customer = options[:customer].to_s
219
241
  post[:customer] = customer if customer.start_with?('cus_')
220
- post
221
242
  end
222
243
 
223
244
  def add_return_url(post, options)
@@ -225,22 +246,27 @@ module ActiveMerchant #:nodoc:
225
246
 
226
247
  post[:confirm] = options[:confirm]
227
248
  post[:return_url] = options[:return_url] if options[:return_url]
228
- post
229
249
  end
230
250
 
231
251
  def add_payment_method_token(post, payment_method, options)
232
252
  return if payment_method.nil?
233
253
 
234
254
  if payment_method.is_a?(ActiveMerchant::Billing::CreditCard)
235
- p = create_payment_method(payment_method, options)
236
- return p unless p.success?
255
+ if off_session_request?(options)
256
+ post[:payment_method_data] = create_payment_method_data(payment_method, options)
257
+ return
258
+ else
259
+ p = create_payment_method(payment_method, options)
260
+ return p unless p.success?
237
261
 
238
- payment_method = p.params['id']
262
+ payment_method = p.params['id']
263
+ end
239
264
  end
240
265
 
241
- if payment_method.is_a?(StripePaymentToken)
266
+ case payment_method
267
+ when StripePaymentToken
242
268
  post[:payment_method] = payment_method.payment_data['id']
243
- elsif payment_method.is_a?(String)
269
+ when String
244
270
  if payment_method.include?('|')
245
271
  customer_id, payment_method_id = payment_method.split('|')
246
272
  token = payment_method_id
@@ -250,6 +276,8 @@ module ActiveMerchant #:nodoc:
250
276
  end
251
277
  post[:payment_method] = token
252
278
  end
279
+
280
+ post
253
281
  end
254
282
 
255
283
  def add_payment_method_types(post, options)
@@ -257,7 +285,6 @@ module ActiveMerchant #:nodoc:
257
285
  return if payment_method_types.nil?
258
286
 
259
287
  post[:payment_method_types] = Array(payment_method_types)
260
- post
261
288
  end
262
289
 
263
290
  def add_exemption(post, options = {})
@@ -268,6 +295,25 @@ module ActiveMerchant #:nodoc:
268
295
  post[:payment_method_options][:card][:moto] = true if options[:moto]
269
296
  end
270
297
 
298
+ def add_stored_credentials(post, options = {})
299
+ return unless options[:stored_credential] && !options[:stored_credential].values.all?(&:nil?)
300
+
301
+ stored_credential = options[:stored_credential]
302
+ post[:payment_method_options] ||= {}
303
+ post[:payment_method_options][:card] ||= {}
304
+ post[:payment_method_options][:card][:mit_exemption] = {}
305
+
306
+ # Stripe PI accepts network_transaction_id and ds_transaction_id via mit field under card.
307
+ post[:payment_method_options][:card][:mit_exemption][:network_transaction_id] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
308
+ post[:payment_method_options][:card][:mit_exemption][:ds_transaction_id] = stored_credential[:ds_transaction_id] if stored_credential[:ds_transaction_id]
309
+ end
310
+
311
+ def add_error_on_requires_action(post, options = {})
312
+ return unless options[:confirm]
313
+
314
+ post[:error_on_requires_action] = true if options[:error_on_requires_action]
315
+ end
316
+
271
317
  def request_three_d_secure(post, options = {})
272
318
  return unless options[:request_three_d_secure] && %w(any automatic).include?(options[:request_three_d_secure])
273
319
 
@@ -276,9 +322,22 @@ module ActiveMerchant #:nodoc:
276
322
  post[:payment_method_options][:card][:request_three_d_secure] = options[:request_three_d_secure]
277
323
  end
278
324
 
325
+ def add_external_three_d_secure_auth_data(post, options = {})
326
+ return unless options[:three_d_secure]&.is_a?(Hash)
327
+
328
+ three_d_secure = options[:three_d_secure]
329
+ post[:payment_method_options] ||= {}
330
+ post[:payment_method_options][:card] ||= {}
331
+ post[:payment_method_options][:card][:three_d_secure] ||= {}
332
+ post[:payment_method_options][:card][:three_d_secure][:version] = three_d_secure[:version] || (three_d_secure[:ds_transaction_id] ? '2.2.0' : '1.0.2')
333
+ post[:payment_method_options][:card][:three_d_secure][:electronic_commerce_indicator] = three_d_secure[:eci] if three_d_secure[:eci]
334
+ post[:payment_method_options][:card][:three_d_secure][:cryptogram] = three_d_secure[:cavv] if three_d_secure[:cavv]
335
+ post[:payment_method_options][:card][:three_d_secure][:transaction_id] = three_d_secure[:ds_transaction_id] || three_d_secure[:xid]
336
+ end
337
+
279
338
  def setup_future_usage(post, options = {})
280
339
  post[:setup_future_usage] = options[:setup_future_usage] if %w(on_session off_session).include?(options[:setup_future_usage])
281
- post[:off_session] = options[:off_session] if options[:off_session] && options[:confirm] == true
340
+ post[:off_session] = options[:off_session] if off_session_request?(options)
282
341
  post
283
342
  end
284
343
 
@@ -296,7 +355,6 @@ module ActiveMerchant #:nodoc:
296
355
  post[:billing_details][:email] = billing[:email] if billing[:email]
297
356
  post[:billing_details][:name] = billing[:name] if billing[:name]
298
357
  post[:billing_details][:phone] = billing[:phone] if billing[:phone]
299
- post
300
358
  end
301
359
 
302
360
  def add_shipping_address(post, options = {})
@@ -315,7 +373,6 @@ module ActiveMerchant #:nodoc:
315
373
  post[:shipping][:carrier] = shipping[:carrier] if shipping[:carrier]
316
374
  post[:shipping][:phone] = shipping[:phone] if shipping[:phone]
317
375
  post[:shipping][:tracking_number] = shipping[:tracking_number] if shipping[:tracking_number]
318
- post
319
376
  end
320
377
 
321
378
  def format_idempotency_key(options, suffix)