activemerchant 1.125.0 → 1.126.0

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