Skip to content

Commit

Permalink
Nuvei: Add 3DS GS
Browse files Browse the repository at this point in the history
Summary:
----------------
Include 3ds Gateway Specific fields to support this functionality

[SER-1353](https://spreedly.atlassian.net/browse/SER-1353)

Unit tests
----------------
Finished in 0.027336 seconds.
13 tests, 77 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Remote tests
----------------
Finished in 32.629558 seconds.
22 tests, 64 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

Rubocop
----------------
801 files, no offenses detected
  • Loading branch information
Gustavo Sanmartin authored and Gustavo Sanmartin committed Sep 10, 2024
1 parent 52f3401 commit 9813dc4
Show file tree
Hide file tree
Showing 3 changed files with 388 additions and 4 deletions.
70 changes: 66 additions & 4 deletions lib/active_merchant/billing/gateways/nuvei.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class NuveiGateway < Gateway
capture: '/settleTransaction',
refund: '/refundTransaction',
void: '/voidTransaction',
general_credit: '/payout'
general_credit: '/payout',
init_payment: '/initPayment'
}

def initialize(options = {})
Expand All @@ -29,14 +30,17 @@ def initialize(options = {})

def authorize(money, payment, options = {}, transaction_type = 'Auth')
post = { transactionType: transaction_type }

build_post_data(post)
add_amount(post, money, options)
add_payment_method(post, payment)
add_address(post, payment, options)
add_customer_ip(post, options)

commit(:purchase, post)
if options[:execute_threed]
execute_3ds_flow(post, money, payment, transaction_type, options)
else
commit(:purchase, post)
end
end

def purchase(money, payment, options = {})
Expand Down Expand Up @@ -150,6 +154,64 @@ def add_address(post, payment, options)
}.compact
end

def execute_3ds_flow(post, money, payment, transaction_type, options = {})
post_3ds = post.dup

MultiResponse.run do |r|
r.process { commit(:init_payment, post) }

r.process do
three_d_params = r.params.dig('paymentOption', 'card', 'threeD')
three_d_supported = three_d_params['v2supported'] != 'false'

next r.process { Response.new(false, '3D Secure is required but not supported') } if !three_d_supported && options[:force_3d_secure]

if three_d_supported
add_3ds_data(post_3ds, options.merge(version: three_d_params['version']))
post_3ds[:relatedTransactionId] = r.authorization
end

commit(:purchase, post_3ds)
end
end
end

def add_3ds_data(post, options = {})
three_d_secure = options[:three_ds_2]
# 01 => Challenge requested, 02 => Exemption requested, 03 or not sending parameter => No preference
challenge_preference = if options[:force_3d_secure] == true || options[:force_3d_secure] == 'true'
'01'
elsif options[:force_3d_secure] == false || options[:force_3d_secure] == 'false'
'02'
end
browser_info_3ds = three_d_secure[:browser_info]
payment_options = post[:paymentOption] ||= {}
card = payment_options[:card] ||= {}
card[:threeD] = {
v2AdditionalParams: {
challengeWindowSize: options[:browser_size],
challengePreference: challenge_preference
}.compact,
browserDetails: {
acceptHeader: browser_info_3ds[:accept_header],
ip: options[:ip],
javaEnabled: browser_info_3ds[:java],
javaScriptEnabled: browser_info_3ds[:javascript] || false,
language: browser_info_3ds[:language],
colorDepth: browser_info_3ds[:depth], # Possible values: 1, 4, 8, 15, 16, 24, 32, 48
screenHeight: browser_info_3ds[:height],
screenWidth: browser_info_3ds[:width],
timeZone: browser_info_3ds[:timezone],
userAgent: browser_info_3ds[:user_agent]
}.compact,
notificationURL: (options[:notification_url] || options[:callback_url]),
merchantURL: options[:merchant_url], # The URL of the merchant's fully qualified website.
version: options[:version], # returned from initPayment
methodCompletionInd: 'U', # to indicate "unavailable".
platformType: '02' # browser instead of app-based (app-based is only for SDK implementation)
}.compact
end

def current_timestamp
Time.now.utc.strftime('%Y%m%d%H%M%S')
end
Expand Down Expand Up @@ -251,7 +313,7 @@ def parse(body)
end

def success_from(response)
response[:status] == 'SUCCESS' && response[:transactionStatus] == 'APPROVED'
response[:status] == 'SUCCESS' && %w[APPROVED REDIRECT].include?(response[:transactionStatus])
end

def authorization_from(action, response, post)
Expand Down
61 changes: 61 additions & 0 deletions test/remote/gateways/remote_nuvei_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,35 @@ def setup
@amount = 100
@credit_card = credit_card('4761344136141390', verification_value: '999', first_name: 'Cure', last_name: 'Tester')
@declined_card = credit_card('4000128449498204')
@challenge_credit_card = credit_card('2221008123677736', first_name: 'CL-BRW2', last_name: '')
@three_ds_amount = 151 # for challenge = 151, for frictionless >= 150
@frictionless_credit_card = credit_card('4000020951595032', first_name: 'FL-BRW2', last_name: '')

@options = {
email: 'test@gmail.com',
billing_address: address.merge(name: 'Cure Tester'),
ip: '127.0.0.1'
}

@three_ds_options = {
execute_threed: true,
redirect_url: 'http://www.example.com/redirect',
callback_url: 'http://www.example.com/callback',
three_ds_2: {
browser_info: {
width: 390,
height: 400,
depth: 24,
timezone: 300,
user_agent: 'Spreedly Agent',
java: false,
javascript: true,
language: 'en-US',
browser_size: '05',
accept_header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
}
}
}
end

def test_transcript_scrubbing
Expand Down Expand Up @@ -74,6 +97,44 @@ def test_successful_purchase
assert_match 'SUCCESS', response.params['status']
end

def test_successful_purchase_with_3ds_frictionless
response = @gateway.purchase(@three_ds_amount, @frictionless_credit_card, @options.merge(@three_ds_options))
assert_success response
assert_not_nil response.params[:transactionId]
assert_match 'SUCCESS', response.params['status']
assert_match 'APPROVED', response.message
end

def test_successful_purchase_with_3ds_challenge
response = @gateway.purchase(@three_ds_amount, @challenge_credit_card, @options.merge(@three_ds_options))
assert_success response
assert_not_nil response.params[:transactionId]
assert_match 'SUCCESS', response.params['status']
assert_match 'REDIRECT', response.message
end

def test_successful_purchase_with_not_enrolled_card
response = @gateway.purchase(@three_ds_amount, @credit_card, @options.merge(@three_ds_options))
assert_success response
assert_not_nil response.params[:transactionId]
assert_match 'SUCCESS', response.params['status']
assert_match 'APPROVED', response.message
end

def test_successful_purchase_with_3ds_frictionless_and_forced_3ds
response = @gateway.purchase(@three_ds_amount, @frictionless_credit_card, @options.merge(@three_ds_options.merge({ force_3d_secure: true })))
assert_success response
assert_not_nil response.params[:transactionId]
assert_match 'SUCCESS', response.params['status']
assert_match 'APPROVED', response.message
end

def test_successful_purchase_with_not_enrolled_card_and_forced_3ds
response = @gateway.purchase(@three_ds_amount, @credit_card, @options.merge(@three_ds_options.merge({ force_3d_secure: true })))
assert_failure response
assert_equal response.message, '3D Secure is required but not supported'
end

def test_failed_purchase
response = @gateway.purchase(@amount, @declined_card, @options)
assert_failure response
Expand Down
Loading

0 comments on commit 9813dc4

Please sign in to comment.