From 15f6e9a10de2781cfb39a6c150a07e1cd8043b51 Mon Sep 17 00:00:00 2001 From: Javier Pedroza Date: Wed, 17 Jul 2024 11:13:19 -0500 Subject: [PATCH] Nuvei: Base Gateway Layout Description ------------------------- [SER-1351](https://spreedly.atlassian.net/browse/SER-1351) This commit add a the basic structure for the new Nuvei Gateway Unit test ------------------------- Finished in 0.124334 seconds. 1 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 8.04 tests/s, 16.09 assertions/s Remote test ------------------------- Finished in 0.602895 seconds. 1 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 1.66 tests/s, 3.32 assertions/s Rubocop ------------------------- 801 files inspected, no offenses detected --- lib/active_merchant/billing/gateways/nuvei.rb | 137 ++++++++++++++++++ test/fixtures.yml | 5 + test/remote/gateways/remote_nuvei_test.rb | 21 +++ test/unit/gateways/nuvei_test.rb | 28 ++++ 4 files changed, 191 insertions(+) create mode 100644 lib/active_merchant/billing/gateways/nuvei.rb create mode 100644 test/remote/gateways/remote_nuvei_test.rb create mode 100644 test/unit/gateways/nuvei_test.rb diff --git a/lib/active_merchant/billing/gateways/nuvei.rb b/lib/active_merchant/billing/gateways/nuvei.rb new file mode 100644 index 00000000000..75518f493d1 --- /dev/null +++ b/lib/active_merchant/billing/gateways/nuvei.rb @@ -0,0 +1,137 @@ +module ActiveMerchant + module Billing + class NuveiGateway < Gateway + self.test_url = 'https://ppp-test.nuvei.com/ppp/api/v1' + self.live_url = 'https://secure.safecharge.com/ppp/api/v1' + + self.supported_countries = %w[US CA IN NZ GB AU US] + self.default_currency = 'USD' + self.money_format = :cents + self.supported_cardtypes = %i[visa master american_express discover union_pay] + self.currencies_without_fractions = %w[CLP KRW JPY ISK MMK PYG UGX VND XAF XOF] + self.homepage_url = 'https://www.nuvei.com/' + self.display_name = 'Nuvei' + + ENDPOINTS_MAPPING = { + authenticate: '/getSessionToken', + purchase: '/payment', # /authorize with transactionType: "Auth" + capture: '/settleTransaction', + refund: '/refundTransaction', + void: '/voidTransaction', + general_credit: '/payout' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :merchant_site_id, :secret_key) + super + end + + def authorize(money, payment, options = {}); end + + def purchase(money, payment, options = {}); end + + def capture(money, authorization, options = {}); end + + def refund(money, authorization, options = {}); end + + def void(authorization, options = {}); end + + def credit(money, payment, options = {}); end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Bearer )[a-zA-Z0-9._-]+)i, '\1[FILTERED]'). + gsub(%r((\"cardNumber\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"cardCvv\\\":\\\")\d+), '\1[FILTERED]'). + gsub(%r((\"cardHolderName\\\":\\\")\w+), '\1[FILTERED]') + end + + private + + def add_payment(post, payment); end + + def commit(action, post, authorization = nil, method = :post) + MultiResponse.run do |r| + r.process { fetch_session_token } unless session_token_valid? + r.process do + api_request(action, post, authorization, method).tap do |response| + response.params.merge!(@options.slice(:session_token, :token_expires)) if @options[:new_credentials] + end + end + end + end + + def session_token_valid? + @options[:session_token] && @options[:token_expires] && Time.now <= @options[:token_expires] + end + + def fetch_session_token + post = { + 'merchantId' => @options[:merchant_id], + 'merchantSiteId' => @options[:merchant_site_id], + 'merchantKey' => @options[:secret_key], + 'clientRequestId' => SecureRandom.uuid, + 'timeStamp' => Time.now.utc.strftime('%Y%m%d%H%M%S') + } + + post.tap { |p| p['checksum'] = calculate_checksum(p) } + + response = JSON.parse(ssl_post(url(:authenticate), post.to_json, headers)).with_indifferent_access + current_time = Time.now + expiration_time = current_time + (15 * 60) # 15 minutes in seconds + + @options[:session_token] = response.dig('sessionToken') + @options[:token_expires] = expiration_time + + Response.new( + response[:sessionToken].present?, + message_from(response), + response, + test: test?, + error_code: response[:errCode] + ) + rescue ResponseError => e + raise OAuthResponseError.new(e) + end + + def calculate_checksum(post) + Digest::SHA256.hexdigest("#{post['merchantId']}#{post['merchantSiteId']}#{post['clientRequestId']}#{post['timeStamp']}#{@options[:secret_key]}") + end + + def url(action, id = nil) + "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" + end + + def api_request(action, post, authorization, method = :post) + response = parse ssl_request(method, url(action, authorization), post.to_json, headers) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(action, response, post), + test: test?, + error_code: error_code_from(action, response) + ) + end + + def headers + { 'Content-Type' => 'application/json' }.tap do |headers| + headers['Authorization'] = "Bearer #{@options[:access_token]}" if @options[:access_token] + end + end + + def success_from(response) + response[:status] == 'SUCCESS' && response[:transactionStatus] == 'APPROVED' + end + + def authorization_from(action, response, post) + response.dig(:orderId) + end + + def message_from(response) + response[:status] + end + end + end +end diff --git a/test/fixtures.yml b/test/fixtures.yml index c4bb3de97ab..fcbfbfebfbb 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -716,6 +716,11 @@ nmi: nmi_secure: security_key: '6457Thfj624V5r7WUwc5v6a68Zsd6YEm' +nuvei: + merchant_id: 'merchantId' + merchant_site_id: 'siteId' + secret_key: 'secretKey' + ogone: login: LOGIN user: USER diff --git a/test/remote/gateways/remote_nuvei_test.rb b/test/remote/gateways/remote_nuvei_test.rb new file mode 100644 index 00000000000..35ee4fefe89 --- /dev/null +++ b/test/remote/gateways/remote_nuvei_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' + +class RemoteNuveiTest < Test::Unit::TestCase + def setup + @gateway = NuveiGateway.new(fixtures(:nuvei)) + + @amount = 100 + @credit_card_cit = credit_card('4111111111111111', verification_value: '999', first_name: 'Cure', last_name: 'Tester') + @credit_card_mit = credit_card('4000002760003184') + @declined_card = credit_card('4000300011112220') + + @options = { + } + end + + def test_successful_session_token_generation + response = @gateway.send(:fetch_session_token) + assert_success response + assert_not_nil response.params[:sessionToken] + end +end diff --git a/test/unit/gateways/nuvei_test.rb b/test/unit/gateways/nuvei_test.rb new file mode 100644 index 00000000000..6034fb8e7ae --- /dev/null +++ b/test/unit/gateways/nuvei_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class NuveiTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = NuveiGateway.new( + merchant_id: 'SOMECREDENTIAL', + merchant_site_id: 'SOMECREDENTIAL', + secret_key: 'SOMECREDENTIAL' + ) + @credit_card = credit_card + @amount = 100 + end + + def supported_card_types + assert_equal %i(visa master american_express discover union_pay), NuveiGateway.supported_cardtypes + end + + def test_supported_countries + assert_equal %w(US CA IN NZ GB AU US), NuveiGateway.supported_countries + end + + def build_request_authenticate_url + action = :authenticate + assert_equal @gateway.send(:url, action), "#{@gateway.test_url}/getSessionToken" + end +end