From 0faace62c8b4a8f0449def8d1eb2ae0b93237fa1 Mon Sep 17 00:00:00 2001 From: aenand <89794007+aenand@users.noreply.github.com> Date: Fri, 3 May 2024 13:22:08 -0400 Subject: [PATCH] Cybersource Rest: Support NT (#5107) * Cybersource Rest: Support NT COMP-75 Adds support for Network Tokens on the Cybersource Rest gateway. Test Summary Local: 5848 tests, 79228 assertions, 0 failures, 23 errors, 0 pendings, 0 omissions, 0 notifications 99.6067% passed Errors unrelated to this gateway Unit: 32 tests, 160 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Remote: 50 tests, 157 assertions, 7 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 86% passed Errors also on master * PR feedback * pr feedback * scrub cryptogram * changelog --- CHANGELOG | 1 + .../billing/gateways/cyber_source_rest.rb | 33 ++++--- .../gateways/remote_cyber_source_rest_test.rb | 47 ++++++++++ test/unit/gateways/cyber_source_rest_test.rb | 86 +++++++++++++++++++ 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a47a7fc062b..11ba101b03f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -160,6 +160,7 @@ * CyberSource: Update NT flow [almalee24] #5106 * Litle: Update enhanced data fields to pass integers [yunnydang] #5113 * Litle: Update commodity code and line item total fields [yunnydang] #5115 +* Cybersource Rest: Add support for network tokens [aenand] #5107 == Version 1.135.0 (August 24, 2023) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index 9e936e0b1e0..0a5e65711c8 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -31,11 +31,16 @@ class CyberSourceRestGateway < Gateway visa: '001' } - PAYMENT_SOLUTION = { + WALLET_PAYMENT_SOLUTION = { apple_pay: '001', google_pay: '012' } + NT_PAYMENT_SOLUTION = { + 'master' => '014', + 'visa' => '015' + } + def initialize(options = {}) requires!(options, :merchant_id, :public_key, :private_key) super @@ -93,6 +98,7 @@ def scrub(transcript) gsub(/(\\?"number\\?":\\?")\d+/, '\1[FILTERED]'). gsub(/(\\?"routingNumber\\?":\\?")\d+/, '\1[FILTERED]'). gsub(/(\\?"securityCode\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(\\?"cryptogram\\?":\\?")[^<]+/, '\1[FILTERED]'). gsub(/(signature=")[^"]*/, '\1[FILTERED]'). gsub(/(keyid=")[^"]*/, '\1[FILTERED]'). gsub(/(Digest: SHA-256=)[\w\/\+=]*/, '\1[FILTERED]') @@ -216,25 +222,30 @@ def add_payment(post, payment, options) end def add_network_tokenization_card(post, payment, options) - post[:processingInformation][:paymentSolution] = PAYMENT_SOLUTION[payment.source] - post[:processingInformation][:commerceIndicator] = 'internet' unless card_brand(payment) == 'jcb' + post[:processingInformation][:commerceIndicator] = 'internet' unless options[:stored_credential] || card_brand(payment) == 'jcb' post[:paymentInformation][:tokenizedCard] = { number: payment.number, expirationMonth: payment.month, expirationYear: payment.year, cryptogram: payment.payment_cryptogram, - transactionType: '1', - type: CREDIT_CARD_CODES[card_brand(payment).to_sym] + type: CREDIT_CARD_CODES[card_brand(payment).to_sym], + transactionType: payment.source == :network_token ? '3' : '1' } - if card_brand(payment) == 'master' - post[:consumerAuthenticationInformation] = { - ucafAuthenticationData: payment.payment_cryptogram, - ucafCollectionIndicator: '2' - } + if payment.source == :network_token && NT_PAYMENT_SOLUTION[payment.brand] + post[:processingInformation][:paymentSolution] = NT_PAYMENT_SOLUTION[payment.brand] else - post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram } + # Apple Pay / Google Pay + post[:processingInformation][:paymentSolution] = WALLET_PAYMENT_SOLUTION[payment.source] + if card_brand(payment) == 'master' + post[:consumerAuthenticationInformation] = { + ucafAuthenticationData: payment.payment_cryptogram, + ucafCollectionIndicator: '2' + } + else + post[:consumerAuthenticationInformation] = { cavv: payment.payment_cryptogram } + end end end diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index 6819eb44589..a08307d376a 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -13,6 +13,29 @@ def setup @master_card = credit_card('2222420000001113', brand: 'master') @discover_card = credit_card('6011111111111117', brand: 'discover') + @visa_network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + @amex_network_token = network_tokenization_credit_card( + '378282246310005', + brand: 'american_express', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @mastercard_network_token = network_tokenization_credit_card( + '5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + @apple_pay = network_tokenization_credit_card( '4111111111111111', payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', @@ -301,6 +324,30 @@ def test_failure_verify assert_equal 'INVALID_ACCOUNT', response.error_code end + def test_successful_authorize_with_visa_network_token + response = @gateway.authorize(@amount, @visa_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_mastercard_network_token + response = @gateway.authorize(@amount, @mastercard_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_amex_network_token + response = @gateway.authorize(@amount, @amex_network_token, @options) + + assert_success response + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + def test_successful_authorize_with_apple_pay response = @gateway.authorize(@amount, @apple_pay, @options) diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb index 5e2b434c3b5..8105b78c8ec 100644 --- a/test/unit/gateways/cyber_source_rest_test.rb +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -17,6 +17,22 @@ def setup year: 2031 ) @master_card = credit_card('2222420000001113', brand: 'master') + + @visa_network_token = network_tokenization_credit_card( + '4111111111111111', + brand: 'visa', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) + + @mastercard_network_token = network_tokenization_credit_card( + '5555555555554444', + brand: 'master', + eci: '05', + payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', + source: :network_token + ) @apple_pay = network_tokenization_credit_card( '4111111111111111', payment_cryptogram: 'AceY+igABPs3jdwNaDg3MAACAAA=', @@ -188,6 +204,34 @@ def test_add_shipping_address assert_equal '4158880000', address[:phoneNumber] end + def test_authorize_network_token_visa + stub_comms do + @gateway.authorize(100, @visa_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '001', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '015', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + + def test_authorize_network_token_mastercard + stub_comms do + @gateway.authorize(100, @mastercard_network_token, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + assert_equal '002', request['paymentInformation']['tokenizedCard']['type'] + assert_equal '3', request['paymentInformation']['tokenizedCard']['transactionType'] + assert_equal 'EHuWW9PiBkWvqE5juRwDzAUFBAk=', request['paymentInformation']['tokenizedCard']['cryptogram'] + assert_nil request['paymentInformation']['tokenizedCard']['requestorId'] + assert_equal '014', request['processingInformation']['paymentSolution'] + assert_equal 'internet', request['processingInformation']['commerceIndicator'] + end.respond_with(successful_purchase_response) + end + def test_authorize_apple_pay_visa stub_comms do @gateway.authorize(100, @apple_pay, @options) @@ -495,6 +539,48 @@ def post_scrubbed POST end + def pre_scrubbed_nt + <<-PRE + <- "POST /pts/v2/payments/ HTTP/1.1\r\nContent-Type: application/json;charset=utf-8\r\nAccept: application/hal+json;charset=utf-8\r\nV-C-Merchant-Id: testrest\r\nDate: Sun, 29 Jan 2023 17:13:30 GMT\r\nHost: apitest.cybersource.com\r\nSignature: keyid=\"08c94330-f618-42a3-b09d-e1e43be5efda\", algorithm=\"HmacSHA256\", headers=\"host date (request-target) digest v-c-merchant-id\", signature=\"DJHeHWceVrsJydd8BCbGowr9dzQ/ry5cGN1FocLakEw=\"\r\nDigest: SHA-256=wuV1cxGzs6KpuUKJmlD7pKV6MZ/5G1wQVoYbf8cRChM=\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nContent-Length: 584\r\n\r\n" + <- "{\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"paymentInformation\":{\"tokenizedCard\":{\"number\":\"4111111111111111\",\"expirationMonth\":9,\"expirationYear\":2025,\"cryptogram\":\"EHuWW9PiBkWvqE5juRwDzAUFBAk=\",\"type\":\"001\",\"transactionType\":\"3\"}},\"orderInformation\":{\"amountDetails\":{\"totalAmount\":\"102.21\",\"currency\":\"USD\"},\"billTo\":{\"firstName\":\"John\",\"lastName\":\"Doe\",\"address1\":\"1 Market St\",\"locality\":\"san francisco\",\"administrativeArea\":\"CA\",\"postalCode\":\"94105\",\"country\":\"US\",\"email\":\"test@cybs.com\",\"phoneNumber\":\"4158880000\"}},\"processingInformation\":{\"commerceIndicator\":\"internet\",\"paymentSolution\":\"015\",\"authorizationOptions\":{}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: -1\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "Content-Type: application/hal+json\r\n" + -> "Content-Length: 905\r\n" + -> "x-response-time: 291ms\r\n" + -> "X-OPNET-Transaction-Trace: 0b1f2bd7-9545-4939-9478-4b76cf7199b6\r\n" + -> "Connection: close\r\n" + -> "v-c-correlation-id: 42969bf5-a77d-4035-9d09-58d4ca070e8c\r\n" + -> "\r\n" + reading 905 bytes... + -> "{\"_links\":{\"authReversal\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/7145981349676498704951\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/captures\"}},\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"id\":\"7145981349676498704951\",\"issuerInformation\":{\"responseRaw\":\"0110322000000E10000200000000000001022105012115353420253130383141564D334B5953323833313030303030000159008000223134573031363135303730333830323039344730363400103232415050524F56414C00065649435243200034544B54523031313132313231323132313231544C3030323636504E30303431313131\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"requestorId\":\"12121212121\",\"assuranceLevel\":\"66\",\"type\":\"001\"},\"card\":{\"suffix\":\"1111\",\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"01234567\"},\"processorInformation\":{\"merchantNumber\":\"000123456789012\",\"approvalCode\":\"831000\",\"networkTransactionId\":\"016150703802094\",\"transactionId\":\"016150703802094\",\"responseCode\":\"00\",\"avs\":{\"code\":\"Y\",\"codeRaw\":\"Y\"}},\"reconciliationId\":\"1081AVM3KYS2\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2024-05-01T21:15:35Z\"}" + PRE + end + + def post_scrubbed_nt + <<-POST + <- "POST /pts/v2/payments/ HTTP/1.1\r\nContent-Type: application/json;charset=utf-8\r\nAccept: application/hal+json;charset=utf-8\r\nV-C-Merchant-Id: testrest\r\nDate: Sun, 29 Jan 2023 17:13:30 GMT\r\nHost: apitest.cybersource.com\r\nSignature: keyid=\"[FILTERED]\", algorithm=\"HmacSHA256\", headers=\"host date (request-target) digest v-c-merchant-id\", signature=\"[FILTERED]\"\r\nDigest: SHA-256=[FILTERED]\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nUser-Agent: Ruby\r\nContent-Length: 584\r\n\r\n" + <- "{\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"paymentInformation\":{\"tokenizedCard\":{\"number\":\"[FILTERED]\",\"expirationMonth\":9,\"expirationYear\":2025,\"cryptogram\":\"[FILTERED]\",\"type\":\"001\",\"transactionType\":\"3\"}},\"orderInformation\":{\"amountDetails\":{\"totalAmount\":\"102.21\",\"currency\":\"USD\"},\"billTo\":{\"firstName\":\"John\",\"lastName\":\"Doe\",\"address1\":\"1 Market St\",\"locality\":\"san francisco\",\"administrativeArea\":\"CA\",\"postalCode\":\"94105\",\"country\":\"US\",\"email\":\"test@cybs.com\",\"phoneNumber\":\"4158880000\"}},\"processingInformation\":{\"commerceIndicator\":\"internet\",\"paymentSolution\":\"015\",\"authorizationOptions\":{}}}" + -> "HTTP/1.1 201 Created\r\n" + -> "Cache-Control: no-cache, no-store, must-revalidate\r\n" + -> "Pragma: no-cache\r\n" + -> "Expires: -1\r\n" + -> "Strict-Transport-Security: max-age=31536000\r\n" + -> "Content-Type: application/hal+json\r\n" + -> "Content-Length: 905\r\n" + -> "x-response-time: 291ms\r\n" + -> "X-OPNET-Transaction-Trace: 0b1f2bd7-9545-4939-9478-4b76cf7199b6\r\n" + -> "Connection: close\r\n" + -> "v-c-correlation-id: 42969bf5-a77d-4035-9d09-58d4ca070e8c\r\n" + -> "\r\n" + reading 905 bytes... + -> "{\"_links\":{\"authReversal\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/7145981349676498704951\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/7145981349676498704951/captures\"}},\"clientReferenceInformation\":{\"code\":\"ba20ae354e25edd1a5ab27158c0a2955\"},\"id\":\"7145981349676498704951\",\"issuerInformation\":{\"responseRaw\":\"0110322000000E10000200000000000001022105012115353420253130383141564D334B5953323833313030303030000159008000223134573031363135303730333830323039344730363400103232415050524F56414C00065649435243200034544B54523031313132313231323132313231544C3030323636504E30303431313131\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"requestorId\":\"12121212121\",\"assuranceLevel\":\"66\",\"type\":\"001\"},\"card\":{\"suffix\":\"1111\",\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"01234567\"},\"processorInformation\":{\"merchantNumber\":\"000123456789012\",\"approvalCode\":\"831000\",\"networkTransactionId\":\"016150703802094\",\"transactionId\":\"016150703802094\",\"responseCode\":\"00\",\"avs\":{\"code\":\"Y\",\"codeRaw\":\"Y\"}},\"reconciliationId\":\"1081AVM3KYS2\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2024-05-01T21:15:35Z\"}" + POST + end + def successful_purchase_response <<-RESPONSE {