activemerchant 1.125.0 → 1.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +75 -0
  3. data/lib/active_merchant/billing/credit_card_methods.rb +12 -0
  4. data/lib/active_merchant/billing/gateway.rb +2 -1
  5. data/lib/active_merchant/billing/gateways/adyen.rb +7 -4
  6. data/lib/active_merchant/billing/gateways/airwallex.rb +341 -0
  7. data/lib/active_merchant/billing/gateways/barclaycard_smartpay.rb +2 -1
  8. data/lib/active_merchant/billing/gateways/blue_pay.rb +1 -1
  9. data/lib/active_merchant/billing/gateways/blue_snap.rb +31 -21
  10. data/lib/active_merchant/billing/gateways/braintree/braintree_common.rb +6 -1
  11. data/lib/active_merchant/billing/gateways/braintree/token_nonce.rb +113 -0
  12. data/lib/active_merchant/billing/gateways/braintree_blue.rb +87 -15
  13. data/lib/active_merchant/billing/gateways/card_connect.rb +1 -1
  14. data/lib/active_merchant/billing/gateways/checkout_v2.rb +1 -1
  15. data/lib/active_merchant/billing/gateways/credorax.rb +10 -0
  16. data/lib/active_merchant/billing/gateways/cyber_source.rb +13 -33
  17. data/lib/active_merchant/billing/gateways/d_local.rb +49 -0
  18. data/lib/active_merchant/billing/gateways/decidir.rb +17 -1
  19. data/lib/active_merchant/billing/gateways/decidir_plus.rb +185 -14
  20. data/lib/active_merchant/billing/gateways/ebanx.rb +3 -2
  21. data/lib/active_merchant/billing/gateways/global_collect.rb +26 -16
  22. data/lib/active_merchant/billing/gateways/ipg.rb +1 -2
  23. data/lib/active_merchant/billing/gateways/litle.rb +93 -1
  24. data/lib/active_merchant/billing/gateways/moneris.rb +35 -8
  25. data/lib/active_merchant/billing/gateways/nmi.rb +12 -7
  26. data/lib/active_merchant/billing/gateways/orbital.rb +349 -327
  27. data/lib/active_merchant/billing/gateways/payflow.rb +62 -0
  28. data/lib/active_merchant/billing/gateways/paymentez.rb +26 -7
  29. data/lib/active_merchant/billing/gateways/paysafe.rb +15 -15
  30. data/lib/active_merchant/billing/gateways/payu_latam.rb +25 -15
  31. data/lib/active_merchant/billing/gateways/priority.rb +158 -136
  32. data/lib/active_merchant/billing/gateways/rapyd.rb +258 -0
  33. data/lib/active_merchant/billing/gateways/safe_charge.rb +1 -4
  34. data/lib/active_merchant/billing/gateways/simetrik.rb +362 -0
  35. data/lib/active_merchant/billing/gateways/stripe.rb +4 -2
  36. data/lib/active_merchant/billing/gateways/stripe_payment_intents.rb +93 -48
  37. data/lib/active_merchant/billing/gateways/visanet_peru.rb +6 -2
  38. data/lib/active_merchant/version.rb +1 -1
  39. metadata +6 -2
@@ -117,11 +117,11 @@ module ActiveMerchant #:nodoc:
117
117
  post[:establishment_name] = options[:establishment_name] if options[:establishment_name]
118
118
  post[:fraud_detection] = add_fraud_detection(options[:fraud_detection]) if options[:fraud_detection].present?
119
119
  post[:site_id] = options[:site_id] if options[:site_id]
120
- post[:sub_payments] = []
121
120
 
122
121
  add_invoice(post, money, options)
123
122
  add_payment(post, credit_card, options)
124
123
  add_aggregate_data(post, options) if options[:aggregate_data]
124
+ add_sub_payments(post, options)
125
125
  end
126
126
 
127
127
  def add_payment_method_id(credit_card, options)
@@ -210,6 +210,22 @@ module ActiveMerchant #:nodoc:
210
210
  post[:aggregate_data] = aggregate_data
211
211
  end
212
212
 
213
+ def add_sub_payments(post, options)
214
+ # sub_payments field is required for purchase transactions, even if empty
215
+ post[:sub_payments] = []
216
+
217
+ return unless sub_payments = options[:sub_payments]
218
+
219
+ sub_payments.each do |sub_payment|
220
+ sub_payment_hash = {
221
+ site_id: sub_payment[:site_id],
222
+ installments: sub_payment[:installments].to_i,
223
+ amount: sub_payment[:amount].to_i
224
+ }
225
+ post[:sub_payments] << sub_payment_hash
226
+ end
227
+ end
228
+
213
229
  def add_fraud_detection(options = {})
214
230
  {}.tap do |hsh|
215
231
  hsh[:send_to_cs] = options[:send_to_cs] if valid_fraud_detection_option?(options[:send_to_cs]) # true/false
@@ -6,7 +6,7 @@ module ActiveMerchant #:nodoc:
6
6
 
7
7
  self.supported_countries = ['AR']
8
8
  self.default_currency = 'ARS'
9
- self.supported_cardtypes = %i[visa master american_express discover]
9
+ self.supported_cardtypes = %i[visa master american_express discover diners_club naranja cabal]
10
10
 
11
11
  self.homepage_url = 'http://decidir.com.ar/home'
12
12
  self.display_name = 'Decidir Plus'
@@ -18,20 +18,44 @@ module ActiveMerchant #:nodoc:
18
18
 
19
19
  def purchase(money, payment, options = {})
20
20
  post = {}
21
+ build_purchase_authorize_request(post, money, payment, options)
21
22
 
22
- add_payment(post, payment, options)
23
- add_purchase_data(post, money, payment, options)
23
+ commit(:post, 'payments', post)
24
+ end
25
+
26
+ def authorize(money, payment, options = {})
27
+ post = {}
28
+ build_purchase_authorize_request(post, money, payment, options)
24
29
 
25
30
  commit(:post, 'payments', post)
26
31
  end
27
32
 
33
+ def capture(money, authorization, options = {})
34
+ post = {}
35
+ post[:amount] = money
36
+
37
+ commit(:put, "payments/#{add_reference(authorization)}", post)
38
+ end
39
+
28
40
  def refund(money, authorization, options = {})
29
41
  post = {}
30
42
  post[:amount] = money
31
43
 
44
+ commit(:post, "payments/#{add_reference(authorization)}/refunds", post)
45
+ end
46
+
47
+ def void(authorization, options = {})
32
48
  commit(:post, "payments/#{add_reference(authorization)}/refunds")
33
49
  end
34
50
 
51
+ def verify(credit_card, options = {})
52
+ MultiResponse.run(:use_first_response) do |r|
53
+ r.process { store(credit_card, options) }
54
+ r.process { authorize(100, r.authorization, options) }
55
+ r.process(:ignore_result) { void(r.authorization, options) }
56
+ end
57
+ end
58
+
35
59
  def store(payment, options = {})
36
60
  post = {}
37
61
  add_payment(post, payment, options)
@@ -39,6 +63,10 @@ module ActiveMerchant #:nodoc:
39
63
  commit(:post, 'tokens', post)
40
64
  end
41
65
 
66
+ def unstore(customer_token)
67
+ commit(:delete, "cardtokens/#{customer_token}")
68
+ end
69
+
42
70
  def supports_scrubbing?
43
71
  true
44
72
  end
@@ -52,6 +80,13 @@ module ActiveMerchant #:nodoc:
52
80
 
53
81
  private
54
82
 
83
+ def build_purchase_authorize_request(post, money, payment, options)
84
+ add_customer_data(post, options)
85
+ add_payment(post, payment, options)
86
+ add_purchase_data(post, money, payment, options)
87
+ add_fraud_detection(post, options)
88
+ end
89
+
55
90
  def add_reference(authorization)
56
91
  return unless authorization
57
92
 
@@ -65,26 +100,64 @@ module ActiveMerchant #:nodoc:
65
100
  post[:bin] = bin
66
101
  else
67
102
  post[:card_number] = payment.number
68
- post[:card_expiration_month] = payment.month.to_s.rjust(2, '0')
69
- post[:card_expiration_year] = payment.year.to_s[-2..-1]
103
+ post[:card_expiration_month] = format(payment.month, :two_digits)
104
+ post[:card_expiration_year] = format(payment.year, :two_digits)
70
105
  post[:security_code] = payment.verification_value.to_s
71
- post[:card_holder_name] = payment.name
106
+ post[:card_holder_name] = payment.name.empty? ? options[:name_override] : payment.name
72
107
  post[:card_holder_identification] = {}
73
- post[:card_holder_identification][:type] = options[:dni]
74
- post[:card_holder_identification][:number] = options[:card_holder_identification_number]
108
+ post[:card_holder_identification][:type] = options[:card_holder_identification_type] if options[:card_holder_identification_type]
109
+ post[:card_holder_identification][:number] = options[:card_holder_identification_number] if options[:card_holder_identification_number]
110
+
111
+ # additional data used for Visa transactions
112
+ post[:card_holder_door_number] = options[:card_holder_door_number].to_i if options[:card_holder_door_number]
113
+ post[:card_holder_birthday] = options[:card_holder_birthday] if options[:card_holder_birthday]
75
114
  end
76
115
  end
77
116
 
117
+ def add_customer_data(post, options = {})
118
+ return unless customer = options[:customer]
119
+
120
+ post[:customer] = {}
121
+ post[:customer][:id] = customer[:id] if customer[:id]
122
+ post[:customer][:email] = customer[:email] if customer[:email]
123
+ end
124
+
78
125
  def add_purchase_data(post, money, payment, options = {})
79
126
  post[:site_transaction_id] = options[:site_transaction_id] || SecureRandom.hex
80
- post[:payment_method_id] = 1
127
+ post[:payment_method_id] = add_payment_method_id(options)
81
128
  post[:amount] = money
82
129
  post[:currency] = options[:currency] || self.default_currency
83
130
  post[:installments] = options[:installments] || 1
84
131
  post[:payment_type] = options[:payment_type] || 'single'
132
+ post[:establishment_name] = options[:establishment_name] if options[:establishment_name]
133
+
134
+ add_aggregate_data(post, options) if options[:aggregate_data]
85
135
  add_sub_payments(post, options)
86
136
  end
87
137
 
138
+ def add_aggregate_data(post, options)
139
+ aggregate_data = {}
140
+ data = options[:aggregate_data]
141
+ aggregate_data[:indicator] = data[:indicator] if data[:indicator]
142
+ aggregate_data[:identification_number] = data[:identification_number] if data[:identification_number]
143
+ aggregate_data[:bill_to_pay] = data[:bill_to_pay] if data[:bill_to_pay]
144
+ aggregate_data[:bill_to_refund] = data[:bill_to_refund] if data[:bill_to_refund]
145
+ aggregate_data[:merchant_name] = data[:merchant_name] if data[:merchant_name]
146
+ aggregate_data[:street] = data[:street] if data[:street]
147
+ aggregate_data[:number] = data[:number] if data[:number]
148
+ aggregate_data[:postal_code] = data[:postal_code] if data[:postal_code]
149
+ aggregate_data[:category] = data[:category] if data[:category]
150
+ aggregate_data[:channel] = data[:channel] if data[:channel]
151
+ aggregate_data[:geographic_code] = data[:geographic_code] if data[:geographic_code]
152
+ aggregate_data[:city] = data[:city] if data[:city]
153
+ aggregate_data[:merchant_id] = data[:merchant_id] if data[:merchant_id]
154
+ aggregate_data[:province] = data[:province] if data[:province]
155
+ aggregate_data[:country] = data[:country] if data[:country]
156
+ aggregate_data[:merchant_email] = data[:merchant_email] if data[:merchant_email]
157
+ aggregate_data[:merchant_phone] = data[:merchant_phone] if data[:merchant_phone]
158
+ post[:aggregate_data] = aggregate_data
159
+ end
160
+
88
161
  def add_sub_payments(post, options)
89
162
  # sub_payments field is required for purchase transactions, even if empty
90
163
  post[:sub_payments] = []
@@ -94,14 +167,78 @@ module ActiveMerchant #:nodoc:
94
167
  sub_payments.each do |sub_payment|
95
168
  sub_payment_hash = {
96
169
  site_id: sub_payment[:site_id],
97
- installments: sub_payment[:installments],
98
- amount: sub_payment[:amount]
170
+ installments: sub_payment[:installments].to_i,
171
+ amount: sub_payment[:amount].to_i
99
172
  }
100
173
  post[:sub_payments] << sub_payment_hash
101
174
  end
102
175
  end
103
176
 
177
+ def add_payment_method_id(options)
178
+ return options[:payment_method_id].to_i if options[:payment_method_id]
179
+
180
+ if options[:debit]
181
+ case options[:card_brand]
182
+ when 'visa'
183
+ 31
184
+ when 'master'
185
+ 105
186
+ when 'maestro'
187
+ 106
188
+ when 'cabal'
189
+ 108
190
+ else
191
+ 31
192
+ end
193
+ else
194
+ case options[:card_brand]
195
+ when 'visa'
196
+ 1
197
+ when 'master'
198
+ 104
199
+ when 'american_express'
200
+ 65
201
+ when 'american_express_prisma'
202
+ 111
203
+ when 'cabal'
204
+ 63
205
+ when 'diners_club'
206
+ 8
207
+ else
208
+ 1
209
+ end
210
+ end
211
+ end
212
+
213
+ def add_fraud_detection(post, options)
214
+ return unless fraud_detection = options[:fraud_detection]
215
+
216
+ {}.tap do |hsh|
217
+ hsh[:send_to_cs] = fraud_detection[:send_to_cs] == 'true' # true/false
218
+ hsh[:channel] = fraud_detection[:channel] if fraud_detection[:channel]
219
+ hsh[:dispatch_method] = fraud_detection[:dispatch_method] if fraud_detection[:dispatch_method]
220
+ add_csmdds(hsh, fraud_detection)
221
+
222
+ post[:fraud_detection] = hsh
223
+ end
224
+ end
225
+
226
+ def add_csmdds(hsh, fraud_detection)
227
+ return unless fraud_detection[:csmdds]
228
+
229
+ csmdds_arr = []
230
+ fraud_detection[:csmdds].each do |csmdds|
231
+ csmdds_hsh = {}
232
+ csmdds_hsh[:code] = csmdds[:code].to_i
233
+ csmdds_hsh[:description] = csmdds[:description]
234
+ csmdds_arr.append(csmdds_hsh)
235
+ end
236
+ hsh[:csmdds] = csmdds_arr unless csmdds_arr.empty?
237
+ end
238
+
104
239
  def parse(body)
240
+ return {} if body.nil?
241
+
105
242
  JSON.parse(body)
106
243
  end
107
244
 
@@ -140,11 +277,13 @@ module ActiveMerchant #:nodoc:
140
277
  end
141
278
 
142
279
  def success_from(response)
143
- response.dig('status') == 'approved' || response.dig('status') == 'active'
280
+ response.dig('status') == 'approved' || response.dig('status') == 'active' || response.dig('status') == 'pre_approved' || response.empty?
144
281
  end
145
282
 
146
283
  def message_from(response)
147
- response.dig('status') || error_message(response) || response.dig('message')
284
+ return '' if response.empty?
285
+
286
+ rejected?(response) ? message_from_status_details(response) : response.dig('status') || error_message(response) || response.dig('message')
148
287
  end
149
288
 
150
289
  def authorization_from(response)
@@ -158,7 +297,24 @@ module ActiveMerchant #:nodoc:
158
297
  end
159
298
 
160
299
  def error_code_from(response)
161
- response.dig('error_type') unless success_from(response)
300
+ return if success_from(response)
301
+
302
+ error_code = nil
303
+ if error = response.dig('status_details', 'error')
304
+ error_code = error.dig('reason', 'id') || error['type']
305
+ elsif response['error_type']
306
+ error_code = response['error_type']
307
+ elsif response.dig('error', 'validation_errors')
308
+ error = response.dig('error')
309
+ validation_errors = error.dig('validation_errors', 0)
310
+ code = validation_errors['code'] if validation_errors && validation_errors['code']
311
+ param = validation_errors['param'] if validation_errors && validation_errors['param']
312
+ error_code = "#{error['error_type']} | #{code} | #{param}" if error['error_type']
313
+ elsif error = response.dig('error')
314
+ error_code = error.dig('reason', 'id')
315
+ end
316
+
317
+ error_code
162
318
  end
163
319
 
164
320
  def error_message(response)
@@ -168,6 +324,21 @@ module ActiveMerchant #:nodoc:
168
324
 
169
325
  "#{validation_errors.dig('code')}: #{validation_errors.dig('param')}"
170
326
  end
327
+
328
+ def rejected?(response)
329
+ return response.dig('status') == 'rejected'
330
+ end
331
+
332
+ def message_from_status_details(response)
333
+ return unless error = response.dig('status_details', 'error')
334
+ return message_from_fraud_detection(response) if error.dig('type') == 'cybersource_error'
335
+
336
+ "#{error.dig('type')}: #{error.dig('reason', 'description')}"
337
+ end
338
+
339
+ def message_from_fraud_detection(response)
340
+ return error_message(response.dig('fraud_detection', 'status', 'details'))
341
+ end
171
342
  end
172
343
  end
173
344
  end
@@ -42,8 +42,8 @@ module ActiveMerchant #:nodoc:
42
42
  'ar' => 100,
43
43
  'co' => 100,
44
44
  'pe' => 300,
45
- 'mx' => 300,
46
- 'cl' => 5000
45
+ 'mx' => 2000,
46
+ 'cl' => 80000
47
47
  }
48
48
 
49
49
  def initialize(options = {})
@@ -183,6 +183,7 @@ module ActiveMerchant #:nodoc:
183
183
  post[:payment][:currency_code] = (options[:currency] || currency(money))
184
184
  post[:payment][:merchant_payment_code] = Digest::MD5.hexdigest(options[:order_id])
185
185
  post[:payment][:instalments] = options[:instalments] || 1
186
+ post[:payment][:order_number] = options[:order_id][0..39] if options[:order_id]
186
187
  end
187
188
 
188
189
  def add_card_or_token(post, payment)
@@ -87,7 +87,9 @@ module ActiveMerchant #:nodoc:
87
87
  'master' => '3',
88
88
  'discover' => '128',
89
89
  'jcb' => '125',
90
- 'diners_club' => '132'
90
+ 'diners_club' => '132',
91
+ 'cabal' => '135',
92
+ 'naranja' => '136'
91
93
  }
92
94
 
93
95
  def add_order(post, money, options, capture: false)
@@ -248,7 +250,6 @@ module ActiveMerchant #:nodoc:
248
250
  month = format(payment.month, :two_digits)
249
251
  expirydate = "#{month}#{year}"
250
252
  pre_authorization = options[:pre_authorization] ? 'PRE_AUTHORIZATION' : 'FINAL_AUTHORIZATION'
251
-
252
253
  post['cardPaymentMethodSpecificInput'] = {
253
254
  'paymentProductId' => BRAND_MAP[payment.brand],
254
255
  'skipAuthentication' => 'true', # refers to 3DSecure
@@ -386,12 +387,12 @@ module ActiveMerchant #:nodoc:
386
387
  response = json_error(raw_response)
387
388
  end
388
389
 
389
- succeeded = success_from(response)
390
+ succeeded = success_from(action, response)
390
391
  Response.new(
391
392
  succeeded,
392
393
  message_from(succeeded, response),
393
394
  response,
394
- authorization: authorization_from(succeeded, response),
395
+ authorization: authorization_from(response),
395
396
  error_code: error_code_from(succeeded, response),
396
397
  test: test?
397
398
  )
@@ -400,8 +401,7 @@ module ActiveMerchant #:nodoc:
400
401
  def json_error(raw_response)
401
402
  {
402
403
  'error_message' => 'Invalid response received from the Ingenico ePayments (formerly GlobalCollect) API. Please contact Ingenico ePayments if you continue to receive this message.' \
403
- " (The raw response returned by the API was #{raw_response.inspect})",
404
- 'status' => 'REJECTED'
404
+ " (The raw response returned by the API was #{raw_response.inspect})"
405
405
  }
406
406
  end
407
407
 
@@ -438,8 +438,24 @@ module ActiveMerchant #:nodoc:
438
438
  'application/json'
439
439
  end
440
440
 
441
- def success_from(response)
442
- !response['errorId'] && response['status'] != 'REJECTED'
441
+ def success_from(action, response)
442
+ return false if response['errorId'] || response['error_message']
443
+
444
+ case action
445
+ when :authorize
446
+ response.dig('payment', 'statusOutput', 'isAuthorized')
447
+ when :capture
448
+ capture_status = response.dig('status') || response.dig('payment', 'status')
449
+ %w(CAPTURED CAPTURE_REQUESTED).include?(capture_status)
450
+ when :void
451
+ void_response_id = response.dig('cardPaymentMethodSpecificOutput', 'voidResponseId') || response.dig('mobilePaymentMethodSpecificOutput', 'voidResponseId')
452
+ %w(00 0 8 11).include?(void_response_id) || response.dig('payment', 'status') == 'CANCELLED'
453
+ when :refund
454
+ refund_status = response.dig('status') || response.dig('payment', 'status')
455
+ %w(REFUNDED REFUND_REQUESTED).include?(refund_status)
456
+ else
457
+ response['status'] != 'REJECTED'
458
+ end
443
459
  end
444
460
 
445
461
  def message_from(succeeded, response)
@@ -456,14 +472,8 @@ module ActiveMerchant #:nodoc:
456
472
  end
457
473
  end
458
474
 
459
- def authorization_from(succeeded, response)
460
- if succeeded
461
- response['id'] || response['payment']['id'] || response['paymentResult']['payment']['id']
462
- elsif response['errorId']
463
- response['errorId']
464
- else
465
- 'GATEWAY ERROR'
466
- end
475
+ def authorization_from(response)
476
+ response.dig('id') || response.dig('payment', 'id') || response.dig('paymentResult', 'payment', 'id')
467
477
  end
468
478
 
469
479
  def error_code_from(succeeded, response)
@@ -4,7 +4,7 @@ module ActiveMerchant #:nodoc:
4
4
  self.test_url = 'https://test.ipg-online.com/ipgapi/services'
5
5
  self.live_url = 'https://www5.ipg-online.com'
6
6
 
7
- self.supported_countries = %w(UY AR)
7
+ self.supported_countries = %w(AR)
8
8
  self.default_currency = 'ARS'
9
9
  self.supported_cardtypes = %i[visa master american_express discover]
10
10
 
@@ -12,7 +12,6 @@ module ActiveMerchant #:nodoc:
12
12
  self.display_name = 'IPG'
13
13
 
14
14
  CURRENCY_CODES = {
15
- 'UYU' => '858',
16
15
  'ARS' => '032'
17
16
  }
18
17
 
@@ -39,6 +39,96 @@ module ActiveMerchant #:nodoc:
39
39
  check?(payment_method) ? commit(:echeckSales, request, money) : commit(:sale, request, money)
40
40
  end
41
41
 
42
+ def add_level_two_data(doc, payment_method, options = {})
43
+ level_2_data = options[:level_2_data]
44
+ if level_2_data
45
+ doc.enhancedData do
46
+ case payment_method.brand
47
+ when 'visa'
48
+ doc.salesTax(level_2_data[:sales_tax]) if level_2_data[:sales_tax]
49
+ when 'master'
50
+ doc.customerReference(level_2_data[:customer_code]) if level_2_data[:customer_code]
51
+ doc.salesTax(level_2_data[:total_tax_amount]) if level_2_data[:total_tax_amount]
52
+ doc.detailTax do
53
+ doc.taxIncludedInTotal(level_2_data[:tax_included_in_total]) if level_2_data[:tax_included_in_total]
54
+ doc.taxAmount(level_2_data[:tax_amount]) if level_2_data[:tax_amount]
55
+ doc.cardAcceptorTaxId(level_2_data[:card_acceptor_tax_id]) if level_2_data[:card_acceptor_tax_id]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ def add_level_three_data(doc, payment_method, options = {})
63
+ level_3_data = options[:level_3_data]
64
+ if level_3_data
65
+ doc.enhancedData do
66
+ case payment_method.brand
67
+ when 'visa'
68
+ add_level_three_information_tags_visa(doc, payment_method, level_3_data)
69
+ when 'master'
70
+ add_level_three_information_tags_master(doc, payment_method, level_3_data)
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def add_level_three_information_tags_visa(doc, payment_method, level_3_data)
77
+ doc.discountAmount(level_3_data[:discount_amount]) if level_3_data[:discount_amount]
78
+ doc.shippingAmount(level_3_data[:shipping_amount]) if level_3_data[:shipping_amount]
79
+ doc.dutyAmount(level_3_data[:duty_amount]) if level_3_data[:duty_amount]
80
+ doc.detailTax do
81
+ doc.taxIncludedInTotal(level_3_data[:tax_included_in_total]) if level_3_data[:tax_included_in_total]
82
+ doc.taxAmount(level_3_data[:tax_amount]) if level_3_data[:tax_amount]
83
+ doc.taxRate(level_3_data[:tax_rate]) if level_3_data[:tax_rate]
84
+ doc.taxTypeIdentifier(level_3_data[:tax_type_identifier]) if level_3_data[:tax_type_identifier]
85
+ doc.cardAcceptorTaxId(level_3_data[:card_acceptor_tax_id]) if level_3_data[:card_acceptor_tax_id]
86
+ end
87
+ add_line_item_information_for_level_three_visa(doc, payment_method, level_3_data)
88
+ end
89
+
90
+ def add_level_three_information_tags_master(doc, payment_method, level_3_data)
91
+ doc.customerReference :customerReference, level_3_data[:customer_code] if level_3_data[:customer_code]
92
+ doc.salesTax(level_3_data[:total_tax_amount]) if level_3_data[:total_tax_amount]
93
+ doc.detailTax do
94
+ doc.taxIncludedInTotal(level_3_data[:tax_included_in_total]) if level_3_data[:tax_included_in_total]
95
+ doc.taxAmount(level_3_data[:tax_amount]) if level_3_data[:tax_amount]
96
+ doc.cardAcceptorTaxId :cardAcceptorTaxId, level_3_data[:card_acceptor_tax_id] if level_3_data[:card_acceptor_tax_id]
97
+ end
98
+ doc.lineItemData do
99
+ level_3_data[:line_items].each do |line_item|
100
+ doc.itemDescription(line_item[:item_description]) if line_item[:item_description]
101
+ doc.productCode(line_item[:product_code]) if line_item[:product_code]
102
+ doc.quantity(line_item[:quantity]) if line_item[:quantity]
103
+ doc.unitOfMeasure(line_item[:unit_of_measure]) if line_item[:unit_of_measure]
104
+ doc.lineItemTotal(line_item[:line_item_total]) if line_item[:line_item_total]
105
+ end
106
+ end
107
+ end
108
+
109
+ def add_line_item_information_for_level_three_visa(doc, payment_method, level_3_data)
110
+ doc.lineItemData do
111
+ level_3_data[:line_items].each do |line_item|
112
+ doc.itemSequenceNumber(line_item[:item_sequence_number]) if line_item[:item_sequence_number]
113
+ doc.commodityCode(line_item[:commodity_code]) if line_item[:commodity_code]
114
+ doc.itemDescription(line_item[:item_description]) if line_item[:item_description]
115
+ doc.productCode(line_item[:product_code]) if line_item[:product_code]
116
+ doc.quantity(line_item[:quantity]) if line_item[:quantity]
117
+ doc.unitOfMeasure(line_item[:unit_of_measure]) if line_item[:unit_of_measure]
118
+ doc.taxAmount(line_item[:tax_amount]) if line_item[:tax_amount]
119
+ doc.itemDiscountAmount(line_item[:discount_per_line_item]) unless line_item[:discount_per_line_item] < 0
120
+ doc.unitCost(line_item[:unit_cost]) unless line_item[:unit_cost] < 0
121
+ doc.detailTax do
122
+ doc.taxIncludedInTotal(line_item[:tax_included_in_total]) if line_item[:tax_included_in_total]
123
+ doc.taxAmount(line_item[:tax_amount]) if line_item[:tax_amount]
124
+ doc.taxRate(line_item[:tax_rate]) if line_item[:tax_rate]
125
+ doc.taxTypeIdentifier(line_item[:tax_type_identifier]) if line_item[:tax_type_identifier]
126
+ doc.cardAcceptorTaxId(line_item[:card_acceptor_tax_id]) if line_item[:card_acceptor_tax_id]
127
+ end
128
+ end
129
+ end
130
+ end
131
+
42
132
  def authorize(money, payment_method, options = {})
43
133
  request = build_xml_request do |doc|
44
134
  add_authentication(doc)
@@ -225,6 +315,8 @@ module ActiveMerchant #:nodoc:
225
315
  add_payment_method(doc, payment_method, options)
226
316
  add_pos(doc, payment_method)
227
317
  add_descriptor(doc, options)
318
+ add_level_two_data(doc, payment_method, options)
319
+ add_level_three_data(doc, payment_method, options)
228
320
  add_merchant_data(doc, options)
229
321
  add_debt_repayment(doc, options)
230
322
  add_stored_credential_params(doc, options)
@@ -386,7 +478,7 @@ module ActiveMerchant #:nodoc:
386
478
  doc.orderSource(order_source)
387
479
  elsif payment_method.is_a?(NetworkTokenizationCreditCard) && payment_method.source == :apple_pay
388
480
  doc.orderSource('applepay')
389
- elsif payment_method.is_a?(NetworkTokenizationCreditCard) && payment_method.source == :android_pay
481
+ elsif payment_method.is_a?(NetworkTokenizationCreditCard) && %i[google_pay android_pay].include?(payment_method.source)
390
482
  doc.orderSource('androidpay')
391
483
  elsif payment_method.respond_to?(:track_data) && payment_method.track_data.present?
392
484
  doc.orderSource('retail')
@@ -50,15 +50,16 @@ module ActiveMerchant #:nodoc:
50
50
  post[:order_id] = options[:order_id]
51
51
  post[:address] = options[:billing_address] || options[:address]
52
52
  post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
53
+ add_external_mpi_fields(post, options)
53
54
  add_stored_credential(post, options)
54
- action = if post[:cavv]
55
+ action = if post[:cavv] || options[:three_d_secure]
55
56
  'cavv_preauth'
56
57
  elsif post[:data_key].blank?
57
58
  'preauth'
58
59
  else
59
60
  'res_preauth_cc'
60
61
  end
61
- commit(action, post)
62
+ commit(action, post, options)
62
63
  end
63
64
 
64
65
  # This action verifies funding on a customer's card and readies them for
@@ -73,15 +74,16 @@ module ActiveMerchant #:nodoc:
73
74
  post[:order_id] = options[:order_id]
74
75
  post[:address] = options[:billing_address] || options[:address]
75
76
  post[:crypt_type] = options[:crypt_type] || @options[:crypt_type]
77
+ add_external_mpi_fields(post, options)
76
78
  add_stored_credential(post, options)
77
- action = if post[:cavv]
79
+ action = if post[:cavv] || options[:three_d_secure]
78
80
  'cavv_purchase'
79
81
  elsif post[:data_key].blank?
80
82
  'purchase'
81
83
  else
82
84
  'res_purchase_cc'
83
85
  end
84
- commit(action, post)
86
+ commit(action, post, options)
85
87
  end
86
88
 
87
89
  # This method retrieves locked funds from a customer's account (from a
@@ -203,6 +205,21 @@ module ActiveMerchant #:nodoc:
203
205
  sprintf('%.4i', creditcard.year)[-2..-1] + sprintf('%.2i', creditcard.month)
204
206
  end
205
207
 
208
+ def add_external_mpi_fields(post, options)
209
+ # See these pages:
210
+ # https://developer.moneris.com/livedemo/3ds2/cavv_purchase/tool/php
211
+ # https://developer.moneris.com/livedemo/3ds2/cavv_preauth/guide/php
212
+ return unless options[:three_d_secure]
213
+
214
+ three_d_secure_options = options[:three_d_secure]
215
+
216
+ post[:threeds_version] = three_d_secure_options[:version]
217
+ post[:crypt_type] = three_d_secure_options[:eci]
218
+ post[:cavv] = three_d_secure_options[:cavv]
219
+ post[:threeds_server_trans_id] = three_d_secure_options[:three_ds_server_trans_id]
220
+ post[:ds_trans_id] = three_d_secure_options[:ds_transaction_id]
221
+ end
222
+
206
223
  def add_payment_source(post, payment_method, options)
207
224
  if payment_method.is_a?(String)
208
225
  post[:data_key] = payment_method
@@ -291,14 +308,16 @@ module ActiveMerchant #:nodoc:
291
308
  end
292
309
  end
293
310
 
294
- def commit(action, parameters = {})
311
+ def commit(action, parameters = {}, options = {})
312
+ threed_ds_transaction = options[:three_d_secure].present?
313
+
295
314
  data = post_data(action, parameters)
296
315
  url = test? ? self.test_url : self.live_url
297
316
  raw = ssl_post(url, data)
298
317
  response = parse(raw)
299
318
 
300
319
  Response.new(
301
- successful?(response),
320
+ successful?(action, response, threed_ds_transaction),
302
321
  message_from(response[:message]),
303
322
  response,
304
323
  test: test?,
@@ -314,8 +333,16 @@ module ActiveMerchant #:nodoc:
314
333
  end
315
334
 
316
335
  # Tests for a successful response from Moneris' servers
317
- def successful?(response)
318
- response[:response_code] &&
336
+ def successful?(action, response, threed_ds_transaction = false)
337
+ # See 9.4 CAVV Result Codes in https://developer.moneris.com/livedemo/3ds2/reference/guide/php
338
+ cavv_accepted = if threed_ds_transaction
339
+ response[:cavv_result_code] && response[:cavv_result_code] == '2'
340
+ else
341
+ true
342
+ end
343
+
344
+ cavv_accepted &&
345
+ response[:response_code] &&
319
346
  response[:complete] &&
320
347
  (0..49).cover?(response[:response_code].to_i)
321
348
  end