diff --git a/lib/active_merchant/billing/gateways/nuvei.rb b/lib/active_merchant/billing/gateways/nuvei.rb new file mode 100644 index 00000000000..74ab65a5c2a --- /dev/null +++ b/lib/active_merchant/billing/gateways/nuvei.rb @@ -0,0 +1,243 @@ +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 + fetch_session_token unless session_token_valid? + end + + def authorize(money, payment, options = {}) + post = {} + post[:transactionType] = 'Auth' + build_post_data(post, :authorize) + add_invoice(post, money, options) + add_payment(post, payment) + add_address(post, payment, options) + add_customer_ip(post, options) + + commit(:purchase, post) + end + + def purchase(money, payment, options = {}); end + + def capture(money, authorization, options = {}) + post = {} + post[:relatedTransactionId] = authorization + build_post_data(post, :capture) + add_invoice(post, money, options) + + commit(:capture, post) + end + + def refund(money, authorization, options = {}); end + + def void(authorization, options = {}); end + + def credit(money, payment, options = {}); end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). + gsub(%r(("cardNumber\\?":\\?")[^"\\]*)i, '\1[FILTERED]'). + gsub(%r(("cardCvv\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantSiteId\\?":\\?")\d+), '\1[FILTERED]'). + gsub(%r(("merchantKey\\?":\\?")\d+), '\1[FILTERED]') + end + + private + + def add_customer_ip(post, options) + return unless options[:ip_address] + + post[:deviceDetails] = { + ipAddress: options[:ip_address] + } + end + + def add_invoice(post, money, options) + post[:amount] = amount(money) + post[:currency] = (options[:currency] || currency(money)) + end + + def credit_card_hash(payment) + { + cardNumber: payment.number, + cardHolderName: payment.name, + expirationMonth: format(payment.month, :two_digits), + expirationYear: format(payment.year, :four_digits), + CVV: payment.verification_value + } + end + + def add_payment(post, payment) + if payment.is_a?(CreditCard) + post[:paymentOption] = { card: credit_card_hash(payment) } + else + post[:paymentOption] = { card: { cardToken: payment } } + end + end + + def add_customer_names(full_name, payment_method) + split_names(full_name).tap do |names| + names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) + names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) + end + end + + def add_address(post, payment, options) + return unless address = options[:billing_address] || options[:address] + + first_name, last_name = add_customer_names(address[:name], payment) + + post[:billingAddress] = { + email: options[:email], + country: address[:country], + phone: options[:phone] || address[:phone], + firstName: first_name, + lastName: last_name + }.compact + end + + def current_timestamp + Time.now.utc.strftime('%Y%m%d%H%M%S') + end + + def build_post_data(post, action) + post[:merchantId] = @options[:merchant_id] + post[:merchantSiteId] = @options[:merchant_site_id] + post[:timeStamp] = current_timestamp.to_i + post[:clientRequestId] = SecureRandom.uuid + post[:clientUniqueId] = SecureRandom.hex(16) + end + + def calculate_checksum(post, action) + case action + when :authenticate + Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:timeStamp]}#{@options[:secret_key]}") + when :capture + Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:clientUniqueId]}#{post[:amount]}#{post[:currency]}#{post[:relatedTransactionId]}#{post[:timeStamp]}#{@options[:secret_key]}") + else + Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:amount]}#{post[:currency]}#{post[:timeStamp]}#{@options[:secret_key]}") + end + end + + def send_session_request(post) + post[:checksum] = calculate_checksum(post, 'authenticate') + response = parse(ssl_post(url(:authenticate), post.to_json, headers)).with_indifferent_access + expiration_time = post[:timeStamp] + @options[:session_token] = response.dig('sessionToken') + @options[:token_expires] = expiration_time + @options[:valid_credentials] = true + + Response.new( + response[:sessionToken].present?, + message_from(response), + response, + test: test?, + error_code: response[:errCode] + ) + end + + def fetch_session_token(post = {}) + build_post_data(post, :authenticate) + send_session_request(post) + end + + def session_token_valid? + return false unless @options[:session_token] && @options[:token_expires] + + token_time = @options[:token_expires].to_i + current_time = Time.now.utc.to_i + (current_time - token_time) < 15 * 60 + end + + def commit(action, post, authorization = nil, method = :post) + @options[:checksum] = calculate_checksum(post, action) + post[:sessionToken] = @options[:session_token] unless action == :capture + post[:checksum] = @options[:checksum] + + 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) + ) + rescue ResponseError => e + response = parse(e.response.body) + # if current access_token is invalid then clean it + if e.response.code == '401' + @options[:session_token] = '' + @options[:new_credentials] = true + end + Response.new(false, message_from(response), response, test: test?) + end + + 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) + end + + def headers + { 'Content-Type' => 'application/json' }.tap do |headers| + headers['Authorization'] = "Bearer #{@options[:session_token]}" if @options[:session_token] + end + end + + def parse(body) + body = '{}' if body.blank? + + JSON.parse(body).with_indifferent_access + rescue JSON::ParserError + { + errors: body, + status: 'Unable to parse JSON response' + }.with_indifferent_access + end + + def success_from(response) + response[:status] == 'SUCCESS' && response[:transactionStatus] == 'APPROVED' + end + + def authorization_from(action, response, post) + response.dig(:transactionId) + 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..5aa64a28f08 --- /dev/null +++ b/test/remote/gateways/remote_nuvei_test.rb @@ -0,0 +1,100 @@ +require 'test_helper' +require 'timecop' + +class RemoteNuveiTest < Test::Unit::TestCase + def setup + @gateway = NuveiGateway.new(fixtures(:nuvei)) + + @amount = 100 + @credit_card = credit_card('4761344136141390', verification_value: '999', first_name: 'Cure', last_name: 'Tester') + @declined_card = credit_card('4000128449498204') + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip_address: '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 + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @credit_card, @options) + end + + @gateway.scrub(transcript) + end + + def test_successful_session_token_generation + response = @gateway.send(:fetch_session_token, @options) + assert_success response + assert_not_nil response.params[:sessionToken] + end + + 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'] + 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'] + end + + def test_failed_authorize + response = @gateway.authorize(@amount, @declined_card, @options) + assert_failure response + assert_match 'DECLINED', response.params['transactionStatus'] + end + + def test_successful_authorize_and_capture + response = @gateway.authorize(@amount, @credit_card, @options) + assert_success response + + capture_response = @gateway.capture(@amount, response.authorization) + + assert_success capture_response + assert_match 'SUCCESS', capture_response.message + assert_match 'APPROVED', capture_response.params['transactionStatus'] + 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'] + end +end diff --git a/test/unit/gateways/nuvei_test.rb b/test/unit/gateways/nuvei_test.rb new file mode 100644 index 00000000000..918275aa1b1 --- /dev/null +++ b/test/unit/gateways/nuvei_test.rb @@ -0,0 +1,115 @@ +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 + + @options = { + email: 'test@gmail.com', + billing_address: address.merge(name: 'Cure Tester'), + ip_address: '127.0.0.1' + } + 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 + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_successful_authorize + stub_comms(@gateway, :ssl_request) do + @gateway.authorize(@amount, @credit_card, @options) + end.check_request do |_method, endpoint, data, _headers| + json_data = JSON.parse(data) + if /payment/.match?(endpoint) + assert_match(%r(/payment), endpoint) + assert_match(/Auth/, json_data['transactionType']) + end + end.respond_with(successful_authorize_response) + end + + private + + def pre_scrubbed + <<-PRE_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"3755516963854600967","merchantSiteId":"255388","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"4761344136141390","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"4761344136141390\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"3755516963854600967\",\"merchantSiteId\":\"255388\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"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\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + PRE_SCRUBBED + end + + def post_scrubbed + <<-POST_SCRUBBED + opening connection to ppp-test.nuvei.com:443... + opened + starting SSL for ppp-test.nuvei.com:443... + SSL established, protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384 + I, [2024-07-22T12:21:29.506576 #65153] INFO -- : [ActiveMerchant::Billing::NuveiGateway] connection_ssl_version=TLSv1.3 connection_ssl_cipher=TLS_AES_256_GCM_SHA384 + D, [2024-07-22T12:21:29.506622 #65153] DEBUG -- : {"transactionType":"Auth","merchantId":"[FILTERED]","merchantSiteId":"[FILTERED]","timeStamp":"20240722172128","clientRequestId":"8fdaf176-67e7-4fee-86f7-efa3bfb2df60","clientUniqueId":"e1c3cb6c583be8f475dff7e25a894f81","amount":"100","currency":"USD","paymentOption":{"card":{"cardNumber":"[FILTERED]","cardHolderName":"Cure Tester","expirationMonth":"09","expirationYear":"2025","CVV":"999"}},"billingAddress":{"email":"test@gmail.com","country":"CA","firstName":"Cure","lastName":"Tester","phone":"(555)555-5555"},"deviceDetails":{"ipAddress":"127.0.0.1"},"sessionToken":"fdda0126-674f-4f8c-ad24-31ac846654ab","checksum":"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741"} + <- "POST /ppp/api/v1/payment HTTP/1.1\r\nContent-Type: application/json\r\nAuthorization: Bearer fdda0126-674f-4f8c-ad24-31ac846654ab\r\nConnection: close\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: ppp-test.nuvei.com\r\nContent-Length: 702\r\n\r\n" + <- "{\"transactionType\":\"Auth\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"timeStamp\":\"20240722172128\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"amount\":\"100\",\"currency\":\"USD\",\"paymentOption\":{\"card\":{\"cardNumber\":\"[FILTERED]\",\"cardHolderName\":\"Cure Tester\",\"expirationMonth\":\"09\",\"expirationYear\":\"2025\",\"CVV\":\"999\"}},\"billingAddress\":{\"email\":\"test@gmail.com\",\"country\":\"CA\",\"firstName\":\"Cure\",\"lastName\":\"Tester\",\"phone\":\"(555)555-5555\"},\"deviceDetails\":{\"ipAddress\":\"127.0.0.1\"},\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"checksum\":\"577658357a0b2c33e5f567dc52f40e984e50b6fa0344d55abb7849cca9a79741\"}" + -> "HTTP/1.1 200 OK\r\n" + -> "Content-Type: application/json\r\n" + -> "Server: nginx\r\n" + -> "Access-Control-Allow-Headers: content-type, X-PINGOTHER\r\n" + -> "Access-Control-Allow-Methods: GET, POST\r\n" + -> "P3P: CP=\"ALL ADM DEV PSAi COM NAV OUR OTR STP IND DEM\"\r\n" + -> "Content-Length: 1103\r\n" + -> "Date: Mon, 22 Jul 2024 17:21:31 GMT\r\n" + -> "Connection: close\r\n" + -> "Set-Cookie: JSESSIONID=b766cc7f4ed4fe63f992477fbe27; Path=/ppp; Secure; HttpOnly; SameSite=None\r\n" + -> "\r\n" + reading 1103 bytes... + -> "{\"internalRequestId\":1170828168,\"status\":\"SUCCESS\",\"errCode\":0,\"reason\":\"\",\"merchantId\":\"[FILTERED]\",\"merchantSiteId\":\"[FILTERED]\",\"version\":\"1.0\",\"clientRequestId\":\"8fdaf176-67e7-4fee-86f7-efa3bfb2df60\",\"sessionToken\":\"fdda0126-674f-4f8c-ad24-31ac846654ab\",\"clientUniqueId\":\"e1c3cb6c583be8f475dff7e25a894f81\",\"orderId\":\"471268418\",\"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\":\"7110000000001884667\",\"externalTransactionId\":\"\",\"authCode\":\"111144\",\"customData\":\"\",\"fraudDetails\":{\"finalDecision\":\"Accept\",\"score\":\"0\"},\"externalSchemeTransactionId\":\"\",\"merchantAdviceCode\":\"\"}" + read 1103 bytes + Conn close + POST_SCRUBBED + end + + def successful_authorize_response + <<~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 +end