Skip to content

Commit

Permalink
CybersourceRest: basic setup with authorize and purchase transactions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Heavyblade committed Feb 15, 2023
1 parent 2eb14a1 commit 729026a
Show file tree
Hide file tree
Showing 5 changed files with 600 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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
218 changes: 218 additions & 0 deletions lib/active_merchant/billing/gateways/cyber_source_rest.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
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 = 'Cybersource REST'

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, true)
end

def authorize(money, payment, options = {}, capture = false)
post = build_auth_request(money, payment, options)
post[:processingInformation] = { capture: true } if capture

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
6 changes: 6 additions & 0 deletions test/fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions test/remote/gateways/remote_cyber_source_rest_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
require 'test_helper'

class RemoteCyberSourceRestTest < Test::Unit::TestCase
def setup
@gateway = CyberSourceRestGateway.new(fixtures(:cybersource_rest))
@amount = 10221
@card_without_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_without_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
Loading

0 comments on commit 729026a

Please sign in to comment.