-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
2eb14a1
commit 729026a
Showing
5 changed files
with
600 additions
and
0 deletions.
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
lib/active_merchant/billing/gateways/cyber_source/cyber_source_common.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
218
lib/active_merchant/billing/gateways/cyber_source_rest.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.