diff --git a/lib/active_merchant/billing/gateways/nuvei.rb b/lib/active_merchant/billing/gateways/nuvei.rb index 4410c5b443a..f2c6c1df87f 100644 --- a/lib/active_merchant/billing/gateways/nuvei.rb +++ b/lib/active_merchant/billing/gateways/nuvei.rb @@ -27,10 +27,10 @@ def initialize(options = {}) fetch_session_token unless session_token_valid? end - def authorize(money, payment, options = {}) - post = { transactionType: 'Auth' } + def authorize(money, payment, options = {}, transaction_type = 'Auth') + post = { transactionType: transaction_type } - build_post_data(post, :authorize) + build_post_data(post) add_amount(post, money, options) add_payment_method(post, payment) add_address(post, payment, options) @@ -39,22 +39,53 @@ def authorize(money, payment, options = {}) commit(:purchase, post) end - def purchase(money, payment, options = {}); end + def purchase(money, payment, options = {}) + authorize(money, payment, options, 'Sale') + end def capture(money, authorization, options = {}) post = { relatedTransactionId: authorization } - build_post_data(post, :capture) + build_post_data(post) add_amount(post, money, options) commit(:capture, post) end - def refund(money, authorization, options = {}); end + def refund(money, authorization, options = {}) + post = { relatedTransactionId: authorization } + + build_post_data(post) + add_amount(post, money, options) + + commit(:refund, post) + end - def void(authorization, options = {}); end + def void(authorization, options = {}) + post = { relatedTransactionId: authorization } + build_post_data(post) - def credit(money, payment, options = {}); end + commit(:void, post) + end + + def verify(credit_card, options = {}) + MultiResponse.run(:use_first_response) do |r| + r.process { authorize(0, credit_card, options) } + r.process(:ignore_result) { void(r.authorization, options) } + end + end + + def credit(money, payment, options = {}) + post = { userTokenId: options[:user_token_id] } + + build_post_data(post) + add_amount(post, money, options) + add_payment_method(post, payment, :cardData) + add_address(post, payment, options) + add_customer_ip(post, options) + + commit(:general_credit, post.compact) + end def supports_scrubbing? true @@ -93,12 +124,9 @@ def credit_card_hash(payment) } end - def add_payment_method(post, payment) - if payment.is_a?(CreditCard) - post[:paymentOption] = { card: credit_card_hash(payment) } - else - post[:paymentOption] = { card: { cardToken: payment } } - end + def add_payment_method(post, payment, key = :paymentOption) + payment_data = payment.is_a?(CreditCard) ? credit_card_hash(payment) : { cardToken: payment } + post[key] = key == :cardData ? payment_data : { card: payment_data } end def add_customer_names(full_name, payment_method) @@ -126,7 +154,7 @@ def current_timestamp Time.now.utc.strftime('%Y%m%d%H%M%S') end - def build_post_data(post, action) + def build_post_data(post) post[:merchantId] = @options[:merchant_id] post[:merchantSiteId] = @options[:merchant_site_id] post[:timeStamp] = current_timestamp.to_i @@ -139,7 +167,7 @@ def calculate_checksum(post, action) keys = case action when :authenticate [:timeStamp] - when :capture + when :capture, :refund, :void %i[clientUniqueId amount currency relatedTransactionId timeStamp] else %i[amount currency timeStamp] @@ -161,12 +189,12 @@ def send_session_request(post) message_from(response), response, test: test?, - error_code: response[:errCode] + error_code: error_code_from(response) ) end def fetch_session_token(post = {}) - build_post_data(post, :authenticate) + build_post_data(post) send_session_request(post) end @@ -188,7 +216,7 @@ def commit(action, post, authorization = nil, method = :post) response, authorization: authorization_from(action, response, post), test: test?, - error_code: error_code_from(action, response) + error_code: error_code_from(response) ) rescue ResponseError => e response = parse(e.response.body) @@ -201,8 +229,8 @@ def url(action, id = nil) "#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" end - def error_code_from(action, response) - (response[:statusName] || response[:status]) unless success_from(response) + def error_code_from(response) + response[:errCode] == 0 ? response[:gwErrorCode] : response[:errCode] end def headers @@ -231,7 +259,8 @@ def authorization_from(action, response, post) end def message_from(response) - response[:status] + reason = response[:reason]&.present? ? response[:reason] : nil + response[:gwErrorReason] || reason || response[:transactionStatus] end end end diff --git a/test/remote/gateways/remote_nuvei_test.rb b/test/remote/gateways/remote_nuvei_test.rb index 5aa64a28f08..51f1ceae5cb 100644 --- a/test/remote/gateways/remote_nuvei_test.rb +++ b/test/remote/gateways/remote_nuvei_test.rb @@ -12,36 +12,8 @@ def setup @options = { email: 'test@gmail.com', billing_address: address.merge(name: 'Cure Tester'), - ip_address: '127.0.0.1' + ip: '127.0.0.1' } - - @post = { - merchantId: 'test_merchant_id', - merchantSiteId: 'test_merchant_site_id', - clientRequestId: 'test_client_request_id', - amount: 'test_amount', - currency: 'test_currency', - timeStamp: 'test_time_stamp' - } - end - - def test_calculate_checksum - expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_amounttest_currencytest_time_stamp#{@gateway.options[:secret_key]}") - assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :purchase) - end - - def test_calculate_checksum_authenticate - expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_time_stamp#{@gateway.options[:secret_key]}") - @post.delete(:amount) - @post.delete(:currency) - assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :authenticate) - end - - def test_calculate_checksum_capture - expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_client_idtest_amounttest_currencytest_transaction_idtest_time_stamp#{@gateway.options[:secret_key]}") - @post[:clientUniqueId] = 'test_client_id' - @post[:relatedTransactionId] = 'test_transaction_id' - assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :capture) end def test_transcript_scrubbing @@ -62,16 +34,14 @@ def test_failed_session_token_generation @gateway.options[:merchant_site_id] = 123 response = @gateway.send(:fetch_session_token, {}) assert_failure response - assert_match 'ERROR', response.message - assert_match 'Invalid merchant site id', response.params['reason'] + assert_match 'Invalid merchant site id', response.message end def test_successful_authorize response = @gateway.authorize(@amount, @credit_card, @options) assert_success response assert_not_nil response.params[:transactionId] - assert_match 'SUCCESS', response.message - assert_match 'APPROVED', response.params['transactionStatus'] + assert_match 'APPROVED', response.message end def test_failed_authorize @@ -87,14 +57,89 @@ def test_successful_authorize_and_capture capture_response = @gateway.capture(@amount, response.authorization) assert_success capture_response - assert_match 'SUCCESS', capture_response.message - assert_match 'APPROVED', capture_response.params['transactionStatus'] + assert_match 'APPROVED', capture_response.message end def test_successful_zero_auth response = @gateway.authorize(0, @credit_card, @options) assert_success response - assert_match 'SUCCESS', response.message - assert_match 'APPROVED', response.params['transactionStatus'] + assert_match 'APPROVED', response.message + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + assert_not_nil response.params[:transactionId] + assert_match 'APPROVED', response.message + assert_match 'SUCCESS', response.params['status'] + end + + def test_failed_purchase + response = @gateway.purchase(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_failed_purchase_with_invalid_cvv + @credit_card.verification_value = nil + response = @gateway.purchase(@amount, @credit_card, @options) + assert_failure response + assert_match 'ERROR', response.params['transactionStatus'] + assert_match 'Invalid CVV2', response.message + end + + def test_failed_capture_invalid_transaction_id + response = @gateway.capture(@amount, '123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_void + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + void_response = @gateway.void(response.authorization) + assert_success void_response + assert_match 'SUCCESS', void_response.params['status'] + assert_match 'APPROVED', void_response.message + end + + def test_failed_void_invalid_transaction_id + response = @gateway.void('123') + assert_failure response + assert_match 'ERROR', response.params['status'] + assert_match 'Invalid relatedTransactionId', response.message + end + + def test_successful_refund + response = @gateway.purchase(@amount, @credit_card, @options) + assert_success response + + refund_response = @gateway.refund(@amount, response.authorization) + assert_success refund_response + assert_match 'SUCCESS', refund_response.params['status'] + assert_match 'APPROVED', refund_response.message + end + + def test_successful_verify + response = @gateway.verify(@credit_card, @options) + assert_success response + assert_match 'SUCCESS', response.params['status'] + assert_match 'APPROVED', response.message + end + + def test_successful_general_credit + credit_response = @gateway.credit(@amount, @credit_card, @options.merge!(user_token_id: '123')) + assert_success credit_response + assert_match 'SUCCESS', credit_response.params['status'] + assert_match 'APPROVED', credit_response.message + end + + def test_failed_general_credit + credit_response = @gateway.credit(@amount, @declined_card, @options) + assert_failure credit_response + assert_match 'ERROR', credit_response.params['status'] + assert_match 'Invalid user token', credit_response.message end end diff --git a/test/unit/gateways/nuvei_test.rb b/test/unit/gateways/nuvei_test.rb index c3a07c68327..d19d19981ef 100644 --- a/test/unit/gateways/nuvei_test.rb +++ b/test/unit/gateways/nuvei_test.rb @@ -77,6 +77,44 @@ def test_successful_authorize end.respond_with(successful_authorize_response) end + def test_successful_purchase + stub_comms(@gateway, :ssl_request) do + @gateway.purchase(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + if /payment/.match?(endpoint) + json_data = JSON.parse(data) + assert_match(/#{@amount}/, json_data['amount']) + assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber']) + assert_match(/#{@credit_card.verification_value}/, json_data['paymentOption']['card']['CVV']) + assert_match(%r(/payment), endpoint) + end + end.respond_with(successful_purchase_response) + end + + def test_successful_refund + stub_comms(@gateway, :ssl_request) do + @gateway.refund(@amount, '123456', @options) + end.check_request(skip_response: true) do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /refundTransaction/.match?(endpoint) + assert_match(/123456/, json_data['relatedTransactionId']) + assert_match(/#{@amount}/, json_data['amount']) + end + end + end + + def test_successful_credit + stub_comms(@gateway, :ssl_request) do + @gateway.credit(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payout/.match?(endpoint) + assert_match(/#{@amount}/, json_data['amount']) + assert_match(/#{@credit_card.number}/, json_data['cardData']['cardNumber']) + end + end.respond_with(successful_purchase_response) + end + private def pre_scrubbed @@ -140,4 +178,10 @@ def successful_authorize_response {"internalRequestId":1171104468,"status":"SUCCESS","errCode":0,"reason":"","merchantId":"3755516963854600967","merchantSiteId":"255388","version":"1.0","clientRequestId":"02ba666c-e3e5-4ec9-ae30-3f8500b18c96","sessionToken":"29226538-82c7-4a3c-b363-cb6829b8c32a","clientUniqueId":"c00ed73a7d682bf478295d57bdae3028","orderId":"471361708","paymentOption":{"userPaymentOptionId":"","card":{"ccCardNumber":"4****1390","bin":"476134","last4Digits":"1390","ccExpMonth":"09","ccExpYear":"25","acquirerId":"19","cvv2Reply":"","avsCode":"","cardType":"Debit","cardBrand":"VISA","issuerBankName":"INTL HDQTRS-CENTER OWNED","issuerCountry":"SG","isPrepaid":"false","threeD":{},"processedBrand":"VISA"},"paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"},"transactionStatus":"APPROVED","gwErrorCode":0,"gwExtendedErrorCode":0,"issuerDeclineCode":"","issuerDeclineReason":"","transactionType":"Auth","transactionId":"7110000000001908486","externalTransactionId":"","authCode":"111397","customData":"","fraudDetails":{"finalDecision":"Accept","score":"0"},"externalSchemeTransactionId":"","merchantAdviceCode":""} RESPONSE end + + def successful_purchase_response + <<~RESPONSE + {"internalRequestId":1172848838, "status":"SUCCESS", "errCode":0, "reason":"", "merchantId":"3755516963854600967", "merchantSiteId":"255388", "version":"1.0", "clientRequestId":"a114381a-0f88-46d0-920c-7b5614f29e5b", "sessionToken":"d3424c9c-dd6d-40dc-85da-a2b92107cbe3", "clientUniqueId":"3ba2a81c46d78837ea819d9f3fe644e7", "orderId":"471833818", "paymentOption":{"userPaymentOptionId":"", "card":{"ccCardNumber":"4****1390", "bin":"476134", "last4Digits":"1390", "ccExpMonth":"09", "ccExpYear":"25", "acquirerId":"19", "cvv2Reply":"", "avsCode":"", "cardType":"Debit", "cardBrand":"VISA", "issuerBankName":"INTL HDQTRS-CENTER OWNED", "issuerCountry":"SG", "isPrepaid":"false", "threeD":{}, "processedBrand":"VISA"}, "paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"}, "transactionStatus":"APPROVED", "gwErrorCode":0, "gwExtendedErrorCode":0, "issuerDeclineCode":"", "issuerDeclineReason":"", "transactionType":"Sale", "transactionId":"7110000000001990927", "externalTransactionId":"", "authCode":"111711", "customData":"", "fraudDetails":{"finalDecision":"Accept", "score":"0"}, "externalSchemeTransactionId":"", "merchantAdviceCode":""} + RESPONSE + end end