From 72ba52fcc80e7e3b9dfd79a957b4653a172af37f Mon Sep 17 00:00:00 2001 From: cristian Date: Mon, 30 Jan 2023 15:22:16 -0500 Subject: [PATCH 1/7] CybersourceRest: basic setup with authorize and purchase transactions CybersourceRest: adding gateway Summary: ------------------------------ Adding CybersourceRest gateway with authorize and purchase calls. GWI-474 Remote Test: ------------------------------ Finished in 3.6855 seconds. 6 tests, 17 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed Unit Tests: ------------------------------ Finished in 35.528692 seconds. 5441 tests, 77085 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: ------------------------------ 760 files inspected, no offenses detected --- .../cyber_source/cyber_source_common.rb | 32 +++ .../billing/gateways/cyber_source_rest.rb | 221 +++++++++++++++ test/fixtures.yml | 6 + .../gateways/remote_cyber_source_rest_test.rb | 82 ++++++ test/unit/gateways/cyber_source_rest_test.rb | 262 ++++++++++++++++++ 5 files changed, 603 insertions(+) create mode 100644 lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb create mode 100644 lib/active_merchant/billing/gateways/cyber_source_rest.rb create mode 100644 test/remote/gateways/remote_cyber_source_rest_test.rb create mode 100644 test/unit/gateways/cyber_source_rest_test.rb diff --git a/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb b/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb new file mode 100644 index 00000000000..4055a9197bc --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb @@ -0,0 +1,32 @@ +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + module CyberSourceCommon + def check_billing_field_value(default, submitted) + if submitted.nil? + nil + elsif submitted.blank? + default + else + submitted + end + end + + def address_names(address_name, payment_method) + names = split_names(address_name) + return names if names.any?(&:present?) + + [ + payment_method&.first_name, + payment_method&.last_name + ] + end + + def lookup_country_code(country_field) + return unless country_field.present? + + country_code = Country.find(country_field) + country_code&.code(:alpha2) + end + end + end +end diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb new file mode 100644 index 00000000000..13d51af9226 --- /dev/null +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -0,0 +1,221 @@ +require 'active_merchant/billing/gateways/cyber_source/cyber_source_common' + +module ActiveMerchant #:nodoc: + module Billing #:nodoc: + class CyberSourceRestGateway < Gateway + include ActiveMerchant::Billing::CyberSourceCommon + + self.test_url = 'https://apitest.cybersource.com' + self.live_url = 'https://api.cybersource.com' + + self.supported_countries = ActiveMerchant::Billing::CyberSourceGateway.supported_countries + self.default_currency = 'USD' + self.supported_cardtypes = %i[visa master american_express discover diners_club jcb maestro elo union_pay cartes_bancaires mada] + + self.homepage_url = 'http://www.cybersource.com' + self.display_name = 'CyberSourceRest' + + CREDIT_CARD_CODES = { + american_express: '003', + cartes_bancaires: '036', + dankort: '034', + diners_club: '005', + discover: '004', + elo: '054', + jcb: '007', + maestro: '042', + master: '002', + unionpay: '062', + visa: '001' + } + + def initialize(options = {}) + requires!(options, :merchant_id, :public_key, :private_key) + super + end + + def purchase(money, payment, options = {}) + authorize(money, payment, options) do |post| + post[:processingInformation] = { capture: true } + end + end + + def authorize(money, payment, options = {}) + post = build_auth_request(money, payment, options) + + yield post if block_given? + + commit('/pts/v2/payments/', post) + end + + def supports_scrubbing? + true + end + + def scrub(transcript) + transcript. + gsub(/(\\?"number\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(\\?"securityCode\\?":\\?")\d+/, '\1[FILTERED]'). + gsub(/(signature=")[^"]*/, '\1[FILTERED]'). + gsub(/(keyid=")[^"]*/, '\1[FILTERED]'). + gsub(/(Digest: SHA-256=)[\w\/\+=]*/, '\1[FILTERED]') + end + + private + + def build_auth_request(amount, payment, options) + { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| + add_customer_id(post, options) + add_code(post, options) + add_credit_card(post, payment) + add_amount(post, amount) + add_address(post, payment, options[:billing_address], options, :billTo) + add_address(post, payment, options[:shipping_address], options, :shipTo) + end.compact + end + + def add_code(post, options) + return unless options[:order_id].present? + + post[:clientReferenceInformation][:code] = options[:order_id] + end + + def add_customer_id(post, options) + return unless options[:customer_id].present? + + post[:paymentInformation][:customer] = { customerId: options[:customer_id] } + end + + def add_amount(post, amount) + currency = options[:currency] || currency(amount) + + post[:orderInformation][:amountDetails] = { + totalAmount: localized_amount(amount, currency), + currency: currency + } + end + + def add_credit_card(post, creditcard) + post[:paymentInformation][:card] = { + number: creditcard.number, + expirationMonth: format(creditcard.month, :two_digits), + expirationYear: format(creditcard.year, :four_digits), + securityCode: creditcard.verification_value, + type: CREDIT_CARD_CODES[card_brand(creditcard).to_sym] + } + end + + def add_address(post, payment_method, address, options, address_type) + return unless address.present? + + first_name, last_name = address_names(address[:name], payment_method) + + post[:orderInformation][address_type] = { + firstName: first_name, + lastName: last_name, + address1: address[:address1], + address2: address[:address2], + locality: address[:city], + administrativeArea: address[:state], + postalCode: address[:zip], + country: lookup_country_code(address[:country])&.value, + email: options[:email].presence || 'null@cybersource.com', + phoneNumber: address[:phone] + # merchantTaxID: ship_to ? options[:merchant_tax_id] : nil, + # company: address[:company], + # companyTaxID: address[:companyTaxID], + # ipAddress: options[:ip], + # driversLicenseNumber: options[:drivers_license_number], + # driversLicenseState: options[:drivers_license_state], + }.compact + end + + def url(action) + "#{(test? ? test_url : live_url)}#{action}" + end + + def host + URI.parse(url('')).host + end + + def parse(body) + JSON.parse(body) + end + + def commit(action, post) + response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post))) + + Response.new( + success_from(response), + message_from(response), + response, + authorization: authorization_from(response), + avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')), + # cvv_result: CVVResult.new(response['some_cvv_response_key']), + test: test?, + error_code: error_code_from(response) + ) + rescue ActiveMerchant::ResponseError => e + response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } } + Response.new(false, response.dig('response', 'rmsg'), response, test: test?) + end + + def success_from(response) + response['status'] == 'AUTHORIZED' + end + + def message_from(response) + return response['status'] if success_from(response) + + response['errorInformation']['message'] + end + + def authorization_from(response) + response['id'] + end + + def error_code_from(response) + response['errorInformation']['reason'] unless success_from(response) + end + + # This implementation follows the Cybersource guide on how create the request signature, see: + # https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/GenerateHeader/httpSignatureAuthentication.html + def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate) + string_to_sign = { + host: host, + date: gmtdatetime, + "(request-target)": "#{http_method} #{resource}", + digest: digest, + "v-c-merchant-id": @options[:merchant_id] + }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) + + { + keyid: @options[:public_key], + algorithm: 'HmacSHA256', + headers: "host date (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", + signature: sign_payload(string_to_sign) + }.map { |k, v| %{#{k}="#{v}"} }.join(', ') + end + + def sign_payload(payload) + decoded_key = Base64.decode64(@options[:private_key]) + Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) + end + + def auth_headers(action, post, http_method = 'post') + digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? + date = Time.now.httpdate + + { + 'Accept' => 'application/hal+json;charset=utf-8', + 'Content-Type' => 'application/json;charset=utf-8', + 'V-C-Merchant-Id' => @options[:merchant_id], + 'Date' => date, + 'Host' => host, + 'Signature' => get_http_signature(action, digest, http_method, date), + 'Digest' => digest + } + end + end + end +end diff --git a/test/fixtures.yml b/test/fixtures.yml index 66606cf5e6d..82237b04bf9 100644 --- a/test/fixtures.yml +++ b/test/fixtures.yml @@ -272,6 +272,12 @@ cyber_source_latam_pe: login: merchant_id password: soap_key +# Working credentials, no need to replace +cybersource_rest: + merchant_id: "testrest" + public_key: "08c94330-f618-42a3-b09d-e1e43be5efda" + private_key: "yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE=" + # Working credentials, no need to replace d_local: login: aeaf9bbfa1 diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb new file mode 100644 index 00000000000..40ac67e46bb --- /dev/null +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -0,0 +1,82 @@ +require 'test_helper' + +class RemoteCyberSourceRestTest < Test::Unit::TestCase + def setup + @gateway = CyberSourceRestGateway.new(fixtures(:cybersource_rest)) + @amount = 10221 + @card_with_out_funds = credit_card('42423482938483873') + @visa_card = credit_card('4111111111111111', + verification_value: '987', + month: 12, + year: 2031) + + @billing_address = { + name: 'John Doe', + address1: '1 Market St', + city: 'san francisco', + state: 'CA', + zip: '94105', + country: 'US', + phone: '4158880000' + } + + @options = { + order_id: generate_unique_id, + currency: 'USD', + email: 'test@cybs.com' + } + end + + def test_handle_credentials_error + gateway = CyberSourceRestGateway.new({ merchant_id: 'abc123', public_key: 'abc456', private_key: 'def789' }) + response = gateway.authorize(@amount, @visa_card, @options) + + assert_equal('Authentication Failed', response.message) + end + + def test_successful_authorize + response = @gateway.authorize(@amount, @visa_card, @options) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_successful_authorize_with_billing_address + @options[:billing_address] = @billing_address + response = @gateway.authorize(@amount, @visa_card, @options) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + refute_empty response.params['_links']['capture'] + end + + def test_failure_authorize_with_declined_credit_card + response = @gateway.authorize(@amount, @card_with_out_funds, @options) + + assert_failure response + assert_match %r{Invalid account}, response.message + assert_equal 'INVALID_ACCOUNT', response.error_code + end + + def test_successful_purchase + response = @gateway.purchase(@amount, @visa_card, @options) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + + def test_transcript_scrubbing + transcript = capture_transcript(@gateway) do + @gateway.authorize(@amount, @visa_card, @options) + end + + transcript = @gateway.scrub(transcript) + assert_scrubbed(@visa_card.number, transcript) + assert_scrubbed(@visa_card.verification_value, transcript) + end +end diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb new file mode 100644 index 00000000000..70258beb9d2 --- /dev/null +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -0,0 +1,262 @@ +require 'test_helper' + +class CyberSourceRestTest < Test::Unit::TestCase + include CommStub + + def setup + @gateway = CyberSourceRestGateway.new( + merchant_id: 'abc123', + public_key: 'def345', + private_key: "NYlM1sgultLjvgaraWvDCXykdz1buqOW8yXE3pMlmxQ=\n" + ) + @credit_card = credit_card('4111111111111111', + verification_value: '987', + month: 12, + year: 2031) + @amount = 100 + @options = { + order_id: '1', + description: 'Store Purchase', + billing_address: { + name: 'John Doe', + address1: '1 Market St', + city: 'san francisco', + state: 'CA', + zip: '94105', + country: 'US', + phone: '4158880000' + }, + email: 'test@cybs.com' + } + + @gmt_time = Time.now.httpdate + @digest = 'SHA-256=gXWufV4Zc7VkN9Wkv9jh/JuAVclqDusx3vkyo3uJFWU=' + @resource = '/pts/v2/payments/' + end + + def test_required_merchant_id_and_secret + error = assert_raises(ArgumentError) { CyberSourceRestGateway.new } + assert_equal 'Missing required parameter: merchant_id', error.message + end + + def test_supported_card_types + assert_equal CyberSourceRestGateway.supported_cardtypes, %i[visa master american_express discover diners_club jcb maestro elo union_pay cartes_bancaires mada] + end + + def test_properly_format_on_zero_decilmal + stub_comms do + @gateway.authorize(1000, @credit_card, @options) + end.check_request do |_endpoint, data, _headers| + request = JSON.parse(data) + card = request['paymentInformation']['card'] + amount_details = request['orderInformation']['amountDetails'] + + assert_equal '1', request['clientReferenceInformation']['code'] + assert_equal '2031', card['expirationYear'] + assert_equal '12', card['expirationMonth'] + assert_equal '987', card['securityCode'] + assert_equal '001', card['type'] + assert_equal 'USD', amount_details['currency'] + assert_equal '10.00', amount_details['totalAmount'] + end.respond_with(successful_purchase_response) + end + + def test_should_create_an_http_signature_for_a_post + signature = @gateway.send :get_http_signature, @resource, @digest, 'post', @gmt_time + + parsed = parse_signature(signature) + + assert_equal 'def345', parsed['keyid'] + assert_equal 'HmacSHA256', parsed['algorithm'] + assert_equal 'host date (request-target) digest v-c-merchant-id', parsed['headers'] + assert_equal %w[algorithm headers keyid signature], signature.split(', ').map { |v| v.split('=').first }.sort + end + + def test_should_create_an_http_signature_for_a_get + signature = @gateway.send :get_http_signature, @resource, nil, 'get', @gmt_time + + parsed = parse_signature(signature) + assert_equal 'host date (request-target) v-c-merchant-id', parsed['headers'] + end + + def test_scrub + assert @gateway.supports_scrubbing? + assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed + end + + def test_including_customer_if_customer_id_present + post = { paymentInformation: {} } + + @gateway.send :add_customer_id, post, {} + assert_nil post[:paymentInformation][:customer] + + @gateway.send :add_customer_id, post, { customer_id: 10 } + assert_equal 10, post[:paymentInformation][:customer][:customerId] + end + + def test_add_ammount_and_currency + post = { orderInformation: {} } + + @gateway.send :add_amount, post, 10221 + + assert_equal '102.21', post.dig(:orderInformation, :amountDetails, :totalAmount) + assert_equal 'USD', post.dig(:orderInformation, :amountDetails, :currency) + end + + def test_add_credit_card_data + post = { paymentInformation: {} } + @gateway.send :add_credit_card, post, @credit_card + + card = post[:paymentInformation][:card] + assert_equal @credit_card.number, card[:number] + assert_equal '2031', card[:expirationYear] + assert_equal '12', card[:expirationMonth] + assert_equal '987', card[:securityCode] + assert_equal '001', card[:type] + end + + def test_add_billing_address + post = { orderInformation: {} } + + @gateway.send :add_address, post, @credit_card, @options[:billing_address], @options, :billTo + + address = post[:orderInformation][:billTo] + + assert_equal 'John', address[:firstName] + assert_equal 'Doe', address[:lastName] + assert_equal '1 Market St', address[:address1] + assert_equal 'san francisco', address[:locality] + assert_equal 'US', address[:country] + assert_equal 'test@cybs.com', address[:email] + assert_equal '4158880000', address[:phoneNumber] + end + + def test_add_shipping_address + post = { orderInformation: {} } + @options[:shipping_address] = @options.delete(:billing_address) + + @gateway.send :add_address, post, @credit_card, @options[:shipping_address], @options, :shipTo + + address = post[:orderInformation][:shipTo] + + assert_equal 'John', address[:firstName] + assert_equal 'Doe', address[:lastName] + assert_equal '1 Market St', address[:address1] + assert_equal 'san francisco', address[:locality] + assert_equal 'US', address[:country] + assert_equal 'test@cybs.com', address[:email] + assert_equal '4158880000', address[:phoneNumber] + end + + def test_url_building + assert_equal "#{@gateway.class.test_url}/action", @gateway.send(:url, '/action') + end + + private + + def parse_signature(signature) + signature.gsub(/=\"$/, '').delete('"').split(', ').map { |x| x.split('=') }.to_h + end + + def pre_scrubbed + <<-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\":\"b8779865d140125036016a0f85db907f\"},\"paymentInformation\":{\"card\":{\"number\":\"4111111111111111\",\"expirationMonth\":\"12\",\"expirationYear\":\"2031\",\"securityCode\":\"987\",\"type\":\"001\"}},\"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\"},\"shipTo\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\",\"email\":\"test@cybs.com\"}}}" + -> "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/6750124114786780104953/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/6750124114786780104953\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/captures\"}},\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"id\":\"6750124114786780104953\",\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"type\":\"001\"},\"card\":{\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"111111\"},\"processorInformation\":{\"approvalCode\":\"888888\",\"networkTransactionId\":\"123456789619999\",\"transactionId\":\"123456789619999\",\"responseCode\":\"100\",\"avs\":{\"code\":\"X\",\"codeRaw\":\"I1\"}},\"reconciliationId\":\"78243988SD9YL291\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2023-01-29T17:13:31Z\"}" + PRE + end + + def post_scrubbed + <<-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\":\"b8779865d140125036016a0f85db907f\"},\"paymentInformation\":{\"card\":{\"number\":\"[FILTERED]\",\"expirationMonth\":\"12\",\"expirationYear\":\"2031\",\"securityCode\":\"[FILTERED]\",\"type\":\"001\"}},\"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\"},\"shipTo\":{\"firstName\":\"Longbob\",\"lastName\":\"Longsen\",\"email\":\"test@cybs.com\"}}}" + -> "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/6750124114786780104953/reversals\"},\"self\":{\"method\":\"GET\",\"href\":\"/pts/v2/payments/6750124114786780104953\"},\"capture\":{\"method\":\"POST\",\"href\":\"/pts/v2/payments/6750124114786780104953/captures\"}},\"clientReferenceInformation\":{\"code\":\"b8779865d140125036016a0f85db907f\"},\"id\":\"6750124114786780104953\",\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"102.21\",\"currency\":\"USD\"}},\"paymentAccountInformation\":{\"card\":{\"type\":\"001\"}},\"paymentInformation\":{\"tokenizedCard\":{\"type\":\"001\"},\"card\":{\"type\":\"001\"}},\"pointOfSaleInformation\":{\"terminalId\":\"111111\"},\"processorInformation\":{\"approvalCode\":\"888888\",\"networkTransactionId\":\"123456789619999\",\"transactionId\":\"123456789619999\",\"responseCode\":\"100\",\"avs\":{\"code\":\"X\",\"codeRaw\":\"I1\"}},\"reconciliationId\":\"78243988SD9YL291\",\"status\":\"AUTHORIZED\",\"submitTimeUtc\":\"2023-01-29T17:13:31Z\"}" + POST + end + + def successful_purchase_response + <<-RESPONSE + { + "_links": { + "authReversal": { + "method": "POST", + "href": "/pts/v2/payments/6750124114786780104953/reversals" + }, + "self": { + "method": "GET", + "href": "/pts/v2/payments/6750124114786780104953" + }, + "capture": { + "method": "POST", + "href": "/pts/v2/payments/6750124114786780104953/captures" + } + }, + "clientReferenceInformation": { + "code": "b8779865d140125036016a0f85db907f" + }, + "id": "6750124114786780104953", + "orderInformation": { + "amountDetails": { + "authorizedAmount": "102.21", + "currency": "USD" + } + }, + "paymentAccountInformation": { + "card": { + "type": "001" + } + }, + "paymentInformation": { + "tokenizedCard": { + "type": "001" + }, + "card": { + "type": "001" + } + }, + "pointOfSaleInformation": { + "terminalId": "111111" + }, + "processorInformation": { + "approvalCode": "888888", + "networkTransactiDDDonId": "123456789619999", + "transactionId": "123456789619999", + "responseCode": "100", + "avs": { + "code": "X", + "codeRaw": "I1" + } + }, + "reconciliationId": "78243988SD9YL291", + "status": "AUTHORIZED", + "submitTimeUtc": "2023-01-29T17:13:31Z" + } + RESPONSE + end +end From 7063589bdff5d83fbdae38f718dbbe6c21c56b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Thu, 16 Feb 2023 09:40:27 -0500 Subject: [PATCH 2/7] create customer --- .../billing/gateways/cyber_source_rest.rb | 29 ++++++++++++++++++- .../gateways/remote_cyber_source_rest_test.rb | 5 ++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index 13d51af9226..a499be362b0 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -48,6 +48,10 @@ def authorize(money, payment, options = {}) commit('/pts/v2/payments/', post) end + def store(payment, options = {}) + build_store_request(payment, options) + end + def supports_scrubbing? true end @@ -63,6 +67,26 @@ def scrub(transcript) private + def create_instrument_identifier + commit('tms/v1/instrumentidentifiers') + end + + def create_customer(payment, options) + { buyerInformation: {}, clientReferenceInformation: {}, merchantDefinedInformation: [] }.tap do |post| + post[:buyerInformation][:merchantCustomerId] = options[:customer_id] + post[:buyerInformation][:email] = options[:email].presence || 'null@cybersource.com' + add_code(post, options) + post[:merchantDefinedInformation] = [ { name: 'data1', value: 'x' } ] + end.compact + end + + def build_store_request(payment, options) + customer = create_customer(payment, options) + require 'pry' + binding.pry + response = commit('/tms/v2/customers/', customer) + end + def build_auth_request(amount, payment, options) { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| add_customer_id(post, options) @@ -144,7 +168,6 @@ def parse(body) def commit(action, post) response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post))) - Response.new( success_from(response), message_from(response), @@ -198,11 +221,15 @@ def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Tim end def sign_payload(payload) + require 'pry' + binding.pry decoded_key = Base64.decode64(@options[:private_key]) Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) end def auth_headers(action, post, http_method = 'post') + require 'pry' + binding.pry digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? date = Time.now.httpdate diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index 40ac67e46bb..9a8345f2c08 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -70,6 +70,11 @@ def test_successful_purchase assert_nil response.params['_links']['capture'] end + def test_successful_store + response = @gateway.store(@visa_card, @options.merge(customer_id: '10')) + assert_success response + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.authorize(@amount, @visa_card, @options) From be9ac007c4ea2fd5cfb3f6f9e6aeced0a2c1605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Tue, 21 Feb 2023 16:39:52 -0500 Subject: [PATCH 3/7] Add store method with succesful remote test --- .../billing/gateways/cyber_source_rest.rb | 91 +++++++++++++------ .../gateways/remote_cyber_source_rest_test.rb | 11 +++ 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index a499be362b0..f0f561e834f 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -49,7 +49,18 @@ def authorize(money, payment, options = {}) end def store(payment, options = {}) - build_store_request(payment, options) + MultiResponse.run do |r| + customer = create_customer(payment, options) + customer_response = r.process { commit('/tms/v2/customers/', customer) } + r.process { create_instrument_identifier(payment, options) } + r.process { create_customer_payment_instrument(payment, options, customer_response, r.params['id']) } + end + end + + def unstore(options = {}) + customer_token_id = options[:customer_token_id] + payment_instrument_token_id = options[:payment_instrument_id] + commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_token_id}", {}, :delete) end def supports_scrubbing? @@ -67,24 +78,46 @@ def scrub(transcript) private - def create_instrument_identifier - commit('tms/v1/instrumentidentifiers') - end - def create_customer(payment, options) { buyerInformation: {}, clientReferenceInformation: {}, merchantDefinedInformation: [] }.tap do |post| post[:buyerInformation][:merchantCustomerId] = options[:customer_id] post[:buyerInformation][:email] = options[:email].presence || 'null@cybersource.com' add_code(post, options) - post[:merchantDefinedInformation] = [ { name: 'data1', value: 'x' } ] + post[:merchantDefinedInformation] = [] end.compact end - def build_store_request(payment, options) - customer = create_customer(payment, options) - require 'pry' - binding.pry - response = commit('/tms/v2/customers/', customer) + def create_instrument_identifier(payment, options) + instrument_identifier = { + card: { + number: payment.number + } + } + commit('/tms/v1/instrumentidentifiers', instrument_identifier) + end + + def create_customer_payment_instrument(payment, options, customer_token, instrument_identifier) + post = {} + post[:deafult] = 'true' + post[:card] = {} + post[:card][:type] = CREDIT_CARD_CODES[payment.brand.to_sym] + post[:card][:expirationMonth] = payment.month.to_s + post[:card][:expirationYear] = payment.year.to_s + post[:billTo] = { + firstName: options[:billing_address][:name].split.first, + lastName: options[:billing_address][:name].split.last, + company: options[:company], + address1: options[:billing_address][:address1], + locality: options[:billing_address][:city], + administrativeArea: options[:billing_address][:state], + postalCode: options[:billing_address][:zip], + country: options[:billing_address][:country], + email: options[:email], + phoneNumber: options[:billing_address][:phone] + } + post[:instrumentIdentifier] = {} + post[:instrumentIdentifier][:id] = instrument_identifier + commit("/tms/v2/customers/#{customer_token.params['id']}/payment-instruments", post) end def build_auth_request(amount, payment, options) @@ -166,29 +199,35 @@ def parse(body) JSON.parse(body) end - def commit(action, post) - response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post))) + def commit(action, post, http_method = 'post') + response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post, http_method))) + Response.new( - success_from(response), - message_from(response), + success_from(action, response), + message_from(action, response), response, authorization: authorization_from(response), avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')), # cvv_result: CVVResult.new(response['some_cvv_response_key']), test: test?, - error_code: error_code_from(response) + error_code: error_code_from(action, response) ) rescue ActiveMerchant::ResponseError => e response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } } Response.new(false, response.dig('response', 'rmsg'), response, test: test?) end - def success_from(response) - response['status'] == 'AUTHORIZED' + def success_from(action, response) + case action + when /payments/ + response['status'] == 'AUTHORIZED' + else + !response['id'].nil? + end end - def message_from(response) - return response['status'] if success_from(response) + def message_from(action, response) + return response['status'] if success_from(action, response) response['errorInformation']['message'] end @@ -197,8 +236,8 @@ def authorization_from(response) response['id'] end - def error_code_from(response) - response['errorInformation']['reason'] unless success_from(response) + def error_code_from(action, response) + response['errorInformation']['reason'] unless success_from(action, response) end # This implementation follows the Cybersource guide on how create the request signature, see: @@ -221,20 +260,16 @@ def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Tim end def sign_payload(payload) - require 'pry' - binding.pry decoded_key = Base64.decode64(@options[:private_key]) Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) end def auth_headers(action, post, http_method = 'post') - require 'pry' - binding.pry digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? date = Time.now.httpdate - + accept = /payments/.match?(action) ? 'application/hal+json;charset=utf-8' : 'application/json;charset=utf-8' { - 'Accept' => 'application/hal+json;charset=utf-8', + 'Accept' => accept, 'Content-Type' => 'application/json;charset=utf-8', 'V-C-Merchant-Id' => @options[:merchant_id], 'Date' => date, diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index 9a8345f2c08..97c09ca6303 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -71,10 +71,21 @@ def test_successful_purchase end def test_successful_store + @options[:billing_address] = @billing_address response = @gateway.store(@visa_card, @options.merge(customer_id: '10')) assert_success response end + def test_successful_unstore + @options[:billing_address] = @billing_address + store_transaction = @gateway.store(@visa_card, @options.merge(customer_id: '10')) + @options[:customer_token_id] = store_transaction.params['id'] + @options[:payment_instrument_id] = store_transaction.params['instrumentIdentifier']['id'] + unstore = @gateway.unstore(@options) + + assert_success unstore + end + def test_transcript_scrubbing transcript = capture_transcript(@gateway) do @gateway.authorize(@amount, @visa_card, @options) From ba97035cb7190fa79a48ceb65393c25fa1e878aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Thu, 23 Feb 2023 10:37:31 -0500 Subject: [PATCH 4/7] Debugging headers for bad request --- .../billing/gateways/cyber_source_rest.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index f0f561e834f..33707a53f5c 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -51,7 +51,7 @@ def authorize(money, payment, options = {}) def store(payment, options = {}) MultiResponse.run do |r| customer = create_customer(payment, options) - customer_response = r.process { commit('/tms/v2/customers/', customer) } + customer_response = r.process { commit('/tms/v2/customers', customer) } r.process { create_instrument_identifier(payment, options) } r.process { create_customer_payment_instrument(payment, options, customer_response, r.params['id']) } end @@ -60,7 +60,8 @@ def store(payment, options = {}) def unstore(options = {}) customer_token_id = options[:customer_token_id] payment_instrument_token_id = options[:payment_instrument_id] - commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_token_id}", {}, :delete) + #/tms/v2/customers/{customerTokenId}/payment-instruments/{paymentInstrumentTokenId} + commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_token_id}", nil, :delete) end def supports_scrubbing? @@ -199,8 +200,8 @@ def parse(body) JSON.parse(body) end - def commit(action, post, http_method = 'post') - response = parse(ssl_post(url(action), post.to_json, auth_headers(action, post, http_method))) + def commit(action, post, http_method = :post) + response = parse(ssl_request(http_method, url(action), post.to_json, auth_headers(action, post, http_method))) Response.new( success_from(action, response), @@ -242,7 +243,7 @@ def error_code_from(action, response) # This implementation follows the Cybersource guide on how create the request signature, see: # https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/GenerateHeader/httpSignatureAuthentication.html - def get_http_signature(resource, digest, http_method = 'post', gmtdatetime = Time.now.httpdate) + def get_http_signature(resource, digest, http_method = :post, gmtdatetime = Time.now.httpdate) string_to_sign = { host: host, date: gmtdatetime, @@ -264,11 +265,12 @@ def sign_payload(payload) Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) end - def auth_headers(action, post, http_method = 'post') + def auth_headers(action, post, http_method = :post) digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? + date = Time.now.httpdate accept = /payments/.match?(action) ? 'application/hal+json;charset=utf-8' : 'application/json;charset=utf-8' - { + headers = { 'Accept' => accept, 'Content-Type' => 'application/json;charset=utf-8', 'V-C-Merchant-Id' => @options[:merchant_id], @@ -277,6 +279,8 @@ def auth_headers(action, post, http_method = 'post') 'Signature' => get_http_signature(action, digest, http_method, date), 'Digest' => digest } + + headers end end end From 41f8f50afcd5cdcff3dd712d84be7e9fdda0e635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Fri, 24 Feb 2023 08:57:55 -0500 Subject: [PATCH 5/7] Create payment instrument --- .../billing/gateways/cyber_source_rest.rb | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index 33707a53f5c..1c1dad2743c 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -52,16 +52,16 @@ def store(payment, options = {}) MultiResponse.run do |r| customer = create_customer(payment, options) customer_response = r.process { commit('/tms/v2/customers', customer) } - r.process { create_instrument_identifier(payment, options) } - r.process { create_customer_payment_instrument(payment, options, customer_response, r.params['id']) } + instrument_identifier = r.process { create_instrument_identifier(payment, options) } + r.process { create_payment_instrument(payment, instrument_identifier, options) } + r.process { create_customer_payment_instrument(payment, options, customer_response, instrument_identifier) } end end def unstore(options = {}) customer_token_id = options[:customer_token_id] - payment_instrument_token_id = options[:payment_instrument_id] - #/tms/v2/customers/{customerTokenId}/payment-instruments/{paymentInstrumentTokenId} - commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_token_id}", nil, :delete) + payment_instrument_id = options[:payment_instrument_id] + commit("/tms/v2/customers/#{customer_token_id}/payment-instruments/#{payment_instrument_id}/", nil, :delete) end def supports_scrubbing? @@ -97,6 +97,32 @@ def create_instrument_identifier(payment, options) commit('/tms/v1/instrumentidentifiers', instrument_identifier) end + def create_payment_instrument(payment, instrument_identifier, options) + post = { + card: { + expirationMonth: payment.month.to_s, + expirationYear: payment.year.to_s, + type: payment.brand + }, + billTo: { + firstName: options[:billing_address][:name].split.first, + lastName: options[:billing_address][:name].split.last, + company: options[:company], + address1: options[:billing_address][:address1], + locality: options[:billing_address][:city], + administrativeArea: options[:billing_address][:state], + postalCode: options[:billing_address][:zip], + country: options[:billing_address][:country], + email: options[:email], + phoneNumber: options[:billing_address][:phone] + }, + instrumentIdentifier: { + id: instrument_identifier.params['id'] + } + } + commit('/tms/v1/paymentinstruments', post) + end + def create_customer_payment_instrument(payment, options, customer_token, instrument_identifier) post = {} post[:deafult] = 'true' @@ -117,7 +143,7 @@ def create_customer_payment_instrument(payment, options, customer_token, instrum phoneNumber: options[:billing_address][:phone] } post[:instrumentIdentifier] = {} - post[:instrumentIdentifier][:id] = instrument_identifier + post[:instrumentIdentifier][:id] = instrument_identifier.params['id'] commit("/tms/v2/customers/#{customer_token.params['id']}/payment-instruments", post) end @@ -202,7 +228,6 @@ def parse(body) def commit(action, post, http_method = :post) response = parse(ssl_request(http_method, url(action), post.to_json, auth_headers(action, post, http_method))) - Response.new( success_from(action, response), message_from(action, response), @@ -255,7 +280,7 @@ def get_http_signature(resource, digest, http_method = :post, gmtdatetime = Time { keyid: @options[:public_key], algorithm: 'HmacSHA256', - headers: "host date (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", + headers: "host#{http_method == :delete ? '' : ' date'} (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", signature: sign_payload(string_to_sign) }.map { |k, v| %{#{k}="#{v}"} }.join(', ') end @@ -274,12 +299,11 @@ def auth_headers(action, post, http_method = :post) 'Accept' => accept, 'Content-Type' => 'application/json;charset=utf-8', 'V-C-Merchant-Id' => @options[:merchant_id], - 'Date' => date, 'Host' => host, 'Signature' => get_http_signature(action, digest, http_method, date), 'Digest' => digest } - + headers.merge!(http_method == :delete ? { 'v-c-date' => date } : { 'date' => date }) headers end end From 55665583faf585ed2297c9ae92955c8af678c34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Fri, 24 Feb 2023 09:01:41 -0500 Subject: [PATCH 6/7] Update headers --- lib/active_merchant/billing/gateways/cyber_source_rest.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index d42c7239265..7d74f33dda4 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -241,7 +241,7 @@ def commit(action, post, http_method = :post) response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } } Response.new(false, response.dig('response', 'rmsg'), response, test: test?) end - + def success_from(action, response) case action when /payments/ From bd2409d2c96f862a28b49cb019cea4d51b97c6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Santiago=20Castillo=20Garz=C3=B3n?= Date: Mon, 27 Feb 2023 15:53:01 -0500 Subject: [PATCH 7/7] Successful unstore tests, fix delete headers --- .../billing/gateways/cyber_source_rest.rb | 58 ++++++++++++------- .../gateways/remote_cyber_source_rest_test.rb | 16 ++++- test/unit/gateways/cyber_source_rest_test.rb | 7 +++ 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/lib/active_merchant/billing/gateways/cyber_source_rest.rb b/lib/active_merchant/billing/gateways/cyber_source_rest.rb index 7d74f33dda4..5011aff764c 100644 --- a/lib/active_merchant/billing/gateways/cyber_source_rest.rb +++ b/lib/active_merchant/billing/gateways/cyber_source_rest.rb @@ -42,8 +42,8 @@ def purchase(money, payment, options = {}) def authorize(money, payment, options = {}, capture = false) post = build_auth_request(money, payment, options) - post[:processingInformation] = { capture: true } if capture - + post[:processingInformation] = { capture: true } if capture && !options[:third_party_token] + post[:paymentInformation][:customer] = { id: options[:third_party_token] } if options[:third_party_token] commit('/pts/v2/payments/', post) end @@ -83,7 +83,6 @@ def create_customer(payment, options) post[:buyerInformation][:merchantCustomerId] = options[:customer_id] post[:buyerInformation][:email] = options[:email].presence || 'null@cybersource.com' add_code(post, options) - post[:merchantDefinedInformation] = [] end.compact end @@ -148,12 +147,12 @@ def create_customer_payment_instrument(payment, options, customer_token, instrum def build_auth_request(amount, payment, options) { clientReferenceInformation: {}, paymentInformation: {}, orderInformation: {} }.tap do |post| - add_customer_id(post, options) + add_customer_id(post, options) unless options[:third_party_token] add_code(post, options) - add_credit_card(post, payment) + add_credit_card(post, payment) unless options[:third_party_token] add_amount(post, amount) - add_address(post, payment, options[:billing_address], options, :billTo) - add_address(post, payment, options[:shipping_address], options, :shipTo) + add_address(post, payment, options[:billing_address], options, :billTo) unless options[:third_party_token] + add_address(post, payment, options[:shipping_address], options, :shipTo) unless options[:third_party_token] end.compact end @@ -222,37 +221,40 @@ def host end def parse(body) - JSON.parse(body) + JSON.parse(body.nil? ? '{}' : body) end def commit(action, post, http_method = :post) - response = parse(ssl_request(http_method, url(action), post.to_json, auth_headers(action, post, http_method))) + headers = http_method == :delete ? auth_headers_delete(action, post, http_method) : auth_headers(action, post, http_method) + response = parse(ssl_request(http_method, url(action), post.nil? || post.empty? ? nil : post.to_json, headers)) Response.new( - success_from(action, response), - message_from(action, response), + success_from(action, response, http_method), + message_from(action, response, http_method), response, authorization: authorization_from(response), avs_result: AVSResult.new(code: response.dig('processorInformation', 'avs', 'code')), # cvv_result: CVVResult.new(response['some_cvv_response_key']), test: test?, - error_code: error_code_from(action, response) + error_code: error_code_from(action, response, http_method) ) rescue ActiveMerchant::ResponseError => e response = e.response.body.present? ? parse(e.response.body) : { 'response' => { 'rmsg' => e.response.msg } } Response.new(false, response.dig('response', 'rmsg'), response, test: test?) end - def success_from(action, response) + def success_from(action, response, http_method) case action when /payments/ response['status'] == 'AUTHORIZED' else - !response['id'].nil? + return response['id'].present? unless http_method == :delete + + return true end end - def message_from(action, response) - return response['status'] if success_from(action, response) + def message_from(action, response, http_method) + return response['status'] if success_from(action, response, http_method) response['errorInformation']['message'] end @@ -261,8 +263,8 @@ def authorization_from(response) response['id'] end - def error_code_from(action, response) - response['errorInformation']['reason'] unless success_from(action, response) + def error_code_from(action, response, http_method) + response['errorInformation']['reason'] unless success_from(action, response, http_method) end # This implementation follows the Cybersource guide on how create the request signature, see: @@ -275,12 +277,18 @@ def get_http_signature(resource, digest, http_method = :post, gmtdatetime = Time digest: digest, "v-c-merchant-id": @options[:merchant_id] }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) + delete_string_to_sign = { + host: host, + "v-c-date": gmtdatetime, + "(request-target)": "#{http_method} #{resource}", + "v-c-merchant-id": @options[:merchant_id] + }.map { |k, v| "#{k}: #{v}" }.join("\n").force_encoding(Encoding::UTF_8) { keyid: @options[:public_key], algorithm: 'HmacSHA256', - headers: "host#{http_method == :delete ? '' : ' date'} (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", - signature: sign_payload(string_to_sign) + headers: "host#{http_method == :delete ? ' v-c-date' : ' date'} (request-target)#{digest.present? ? ' digest' : ''} v-c-merchant-id", + signature: sign_payload(http_method == :delete ? delete_string_to_sign : string_to_sign) }.map { |k, v| %{#{k}="#{v}"} }.join(', ') end @@ -289,6 +297,16 @@ def sign_payload(payload) Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', decoded_key, payload)) end + def auth_headers_delete(action, post, http_method) + date = Time.now.httpdate + { + 'v-c-date' => date, + 'V-C-Merchant-Id' => @options[:merchant_id], + 'Host' => host, + 'Signature' => get_http_signature(action, nil, http_method, date) + } + end + def auth_headers(action, post, http_method = :post) digest = "SHA-256=#{Digest::SHA256.base64digest(post.to_json)}" if post.present? diff --git a/test/remote/gateways/remote_cyber_source_rest_test.rb b/test/remote/gateways/remote_cyber_source_rest_test.rb index 8d7d559b667..91ecec06e1e 100644 --- a/test/remote/gateways/remote_cyber_source_rest_test.rb +++ b/test/remote/gateways/remote_cyber_source_rest_test.rb @@ -76,11 +76,23 @@ def test_successful_store assert_success response end + def test_successful_purchase_with_stored_card + @options[:billing_address] = @billing_address + stored = @gateway.store(@visa_card, @options.merge(customer_id: '10')) + response = @gateway.purchase(@amount, @visa_card, @options.merge(third_party_token: stored.params['id'])) + + assert_success response + assert response.test? + assert_equal 'AUTHORIZED', response.message + assert_nil response.params['_links']['capture'] + end + def test_successful_unstore @options[:billing_address] = @billing_address store_transaction = @gateway.store(@visa_card, @options.merge(customer_id: '10')) - @options[:customer_token_id] = store_transaction.params['id'] - @options[:payment_instrument_id] = store_transaction.params['instrumentIdentifier']['id'] + @options[:customer_token_id] = store_transaction.responses[0].params['id'] + @options[:payment_instrument_id] = store_transaction.responses[2].params['id'] + unstore = @gateway.unstore(@options) assert_success unstore diff --git a/test/unit/gateways/cyber_source_rest_test.rb b/test/unit/gateways/cyber_source_rest_test.rb index 70258beb9d2..50379afbb4f 100644 --- a/test/unit/gateways/cyber_source_rest_test.rb +++ b/test/unit/gateways/cyber_source_rest_test.rb @@ -79,6 +79,13 @@ def test_should_create_an_http_signature_for_a_get assert_equal 'host date (request-target) v-c-merchant-id', parsed['headers'] end + def test_should_create_an_http_signature_for_a_delete + signature = @gateway.send :get_http_signature, @resource, nil, :delete, @gmt_time + + parsed = parse_signature(signature) + assert_equal 'host v-c-date (request-target) v-c-merchant-id', parsed['headers'] + end + def test_scrub assert @gateway.supports_scrubbing? assert_equal @gateway.scrub(pre_scrubbed), post_scrubbed