Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cybersource: Update NT flow #5106

Merged
merged 1 commit into from May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -157,6 +157,7 @@
* Braintree: Add additional data to response [aenand] #5084
* CheckoutV2: Retain and refresh OAuth access token [sinourain] #5098
* Worldpay: Remove default ECI value [aenand] #5103
* CyberSource: Update NT flow [almalee24] #5106


== Version 1.135.0 (August 24, 2023)
Expand Down
98 changes: 74 additions & 24 deletions lib/active_merchant/billing/gateways/cyber_source.rb
Expand Up @@ -141,11 +141,16 @@ class CyberSourceGateway < Gateway
r703: 'Export hostname_country/ip_country match'
}

@@payment_solution = {
@@wallet_payment_solution = {
apple_pay: '001',
google_pay: '012'
}

NT_PAYMENT_SOLUTION = {
'master' => '014',
'visa' => '015'
}

# These are the options that can be used when creating a new CyberSource
# Gateway object.
#
Expand All @@ -170,19 +175,31 @@ def initialize(options = {})
super
end

def authorize(money, creditcard_or_reference, options = {})
setup_address_hash(options)
commit(build_auth_request(money, creditcard_or_reference, options), :authorize, money, options)
def authorize(money, payment_method, options = {})
if valid_payment_method?(payment_method)
setup_address_hash(options)
commit(build_auth_request(money, payment_method, options), :authorize, money, options)
else
# this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource
payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '')
Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html")
end
end

def capture(money, authorization, options = {})
setup_address_hash(options)
commit(build_capture_request(money, authorization, options), :capture, money, options)
end

def purchase(money, payment_method_or_reference, options = {})
setup_address_hash(options)
commit(build_purchase_request(money, payment_method_or_reference, options), :purchase, money, options)
def purchase(money, payment_method, options = {})
if valid_payment_method?(payment_method)
setup_address_hash(options)
commit(build_purchase_request(money, payment_method, options), :purchase, money, options)
else
# this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource
payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '')
Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html")
end
end

def void(identification, options = {})
Expand Down Expand Up @@ -215,8 +232,14 @@ def credit(money, creditcard_or_reference, options = {})
# To charge the card while creating a profile, pass
# options[:setup_fee] => money
def store(payment_method, options = {})
setup_address_hash(options)
commit(build_create_subscription_request(payment_method, options), :store, nil, options)
if valid_payment_method?(payment_method)
setup_address_hash(options)
commit(build_create_subscription_request(payment_method, options), :store, nil, options)
else
# this is for NetworkToken, ApplePay or GooglePay brands that aren't supported at CyberSource
payment_type = payment_method.source.to_s.gsub('_', ' ').titleize.gsub(' ', '')
Response.new(false, "#{card_brand(payment_method).capitalize} is not supported by #{payment_type} at CyberSource, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html")
end
end

# Updates a customer subscription/profile
Expand Down Expand Up @@ -281,6 +304,8 @@ def scrub(transcript)
gsub(%r((<cvNumber>)[^<]*(</cvNumber>))i, '\1[FILTERED]\2').
gsub(%r((<cavv>)[^<]*(</cavv>))i, '\1[FILTERED]\2').
gsub(%r((<xid>)[^<]*(</xid>))i, '\1[FILTERED]\2').
gsub(%r((<networkTokenCryptogram>)[^<]*(</networkTokenCryptogram>))i, '\1[FILTERED]\2').
gsub(%r((<requestorID>)[^<]*(</requestorID>))i, '\1[FILTERED]\2').
gsub(%r((<authenticationData>)[^<]*(</authenticationData>))i, '\1[FILTERED]\2')
end

Expand All @@ -295,6 +320,12 @@ def verify_credentials

private

def valid_payment_method?(payment_method)
return true unless payment_method.is_a?(NetworkTokenizationCreditCard)

%w(visa master american_express).include?(card_brand(payment_method))
end

# Create all required address hash key value pairs
# If a value of nil is received, that value will be passed on to the gateway and will not be replaced with a default value
# Billing address fields received without an override value or with an empty string value will be replaced with the default_address values
Expand Down Expand Up @@ -337,8 +368,8 @@ def build_auth_request(money, creditcard_or_reference, options)
add_business_rules_data(xml, creditcard_or_reference, options)
add_airline_data(xml, options)
add_sales_slip_number(xml, options)
add_payment_network_token(xml) if network_tokenization?(creditcard_or_reference)
add_payment_solution(xml, creditcard_or_reference.source) if network_tokenization?(creditcard_or_reference)
add_payment_network_token(xml, creditcard_or_reference, options)
add_payment_solution(xml, creditcard_or_reference)
add_tax_management_indicator(xml, options)
add_stored_credential_subsequent_auth(xml, options)
add_issuer_additional_data(xml, options)
Expand Down Expand Up @@ -410,8 +441,8 @@ def build_purchase_request(money, payment_method_or_reference, options)
add_business_rules_data(xml, payment_method_or_reference, options)
add_airline_data(xml, options)
add_sales_slip_number(xml, options)
add_payment_network_token(xml) if network_tokenization?(payment_method_or_reference)
add_payment_solution(xml, payment_method_or_reference.source) if network_tokenization?(payment_method_or_reference)
add_payment_network_token(xml, payment_method_or_reference, options)
add_payment_solution(xml, payment_method_or_reference)
add_tax_management_indicator(xml, options)
add_stored_credential_subsequent_auth(xml, options)
add_issuer_additional_data(xml, options)
Expand Down Expand Up @@ -499,7 +530,7 @@ def build_create_subscription_request(payment_method, options)
add_check_service(xml)
else
add_purchase_service(xml, payment_method, options)
add_payment_network_token(xml) if network_tokenization?(payment_method)
add_payment_network_token(xml, payment_method, options)
end
end
add_subscription_create_service(xml, options)
Expand Down Expand Up @@ -689,10 +720,16 @@ def add_decision_manager_fields(xml, options)
end
end

def add_payment_solution(xml, source)
return unless (payment_solution = @@payment_solution[source])
def add_payment_solution(xml, payment_method)
return unless network_tokenization?(payment_method)

xml.tag! 'paymentSolution', payment_solution
case payment_method.source
when :network_token
payment_solution = NT_PAYMENT_SOLUTION[payment_method.brand]
xml.tag! 'paymentSolution', payment_solution if payment_solution
when :apple_pay, :google_pay
xml.tag! 'paymentSolution', @@wallet_payment_solution[payment_method.source]
end
end

def add_issuer_additional_data(xml, options)
Expand Down Expand Up @@ -743,7 +780,11 @@ def add_tax_service(xml)

def add_auth_service(xml, payment_method, options)
if network_tokenization?(payment_method)
add_auth_network_tokenization(xml, payment_method, options)
if payment_method.source == :network_token
add_auth_network_tokenization(xml, payment_method, options)
else
add_auth_wallet(xml, payment_method, options)
end
else
xml.tag! 'ccAuthService', { 'run' => 'true' } do
if options[:three_d_secure]
Expand Down Expand Up @@ -835,14 +876,20 @@ def network_tokenization?(payment_method)

def subsequent_nt_apple_pay_auth(source, options)
return unless options[:stored_credential] || options[:stored_credential_overrides]
return unless @@payment_solution[source]
return unless @@wallet_payment_solution[source]

options.dig(:stored_credential_overrides, :subsequent_auth) || options.dig(:stored_credential, :initiator) == 'merchant'
end

def add_auth_network_tokenization(xml, payment_method, options)
return unless network_tokenization?(payment_method)
xml.tag! 'ccAuthService', { 'run' => 'true' } do
xml.tag!('networkTokenCryptogram', payment_method.payment_cryptogram)
xml.tag!('commerceIndicator', 'internet')
xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id]
end
end

def add_auth_wallet(xml, payment_method, options)
commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options)

brand = card_brand(payment_method).to_sym
Expand All @@ -868,13 +915,12 @@ def add_auth_network_tokenization(xml, payment_method, options)
xml.tag!('xid', Base64.encode64(cryptogram[20...40])) if cryptogram.bytes.count > 20
xml.tag!('reconciliationID', options[:reconciliation_id]) if options[:reconciliation_id]
end
else
raise ArgumentError.new("Payment method #{brand} is not supported, check https://developer.cybersource.com/docs/cybs/en-us/payments/developer/all/rest/payments/CreatingOnlineAuth/CreatingAuthReqPNT.html")
end
end

def add_mastercard_network_tokenization_ucaf_data(xml, payment_method, options)
return unless network_tokenization?(payment_method) && card_brand(payment_method).to_sym == :master
return if payment_method.source == :network_token

commerce_indicator = 'internet' if subsequent_nt_apple_pay_auth(payment_method.source, options)

Expand All @@ -884,9 +930,13 @@ def add_mastercard_network_tokenization_ucaf_data(xml, payment_method, options)
end
end

def add_payment_network_token(xml)
def add_payment_network_token(xml, payment_method, options)
return unless network_tokenization?(payment_method)

transaction_type = payment_method.source == :network_token ? '3' : '1'
xml.tag! 'paymentNetworkToken' do
xml.tag!('transactionType', '1')
xml.tag!('requestorID', options[:trid]) if transaction_type == '3' && options[:trid]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the transaction_type == '3' guard in case an AP/GP has a trid?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, just to be extra cautious but I guess they wouldn't have a trid to begin with

xml.tag!('transactionType', transaction_type)
end
end

Expand Down
22 changes: 20 additions & 2 deletions test/remote/gateways/remote_cyber_source_test.rb
Expand Up @@ -57,13 +57,23 @@ def setup
'4111111111111111',
brand: 'visa',
eci: '05',
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk='
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=',
source: :network_token
)
@amex_network_token = network_tokenization_credit_card(
'378282246310005',
brand: 'american_express',
eci: '05',
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk='
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=',
source: :network_token
)

@mastercard_network_token = network_tokenization_credit_card(
'5555555555554444',
brand: 'master',
eci: '05',
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=',
source: :network_token
)

@amount = 100
Expand Down Expand Up @@ -747,6 +757,14 @@ def test_network_tokenization_with_amex_cc_and_basic_cryptogram
assert_successful_response(capture)
end

def test_network_tokenization_with_mastercard
assert auth = @gateway.authorize(@amount, @mastercard_network_token, @options)
assert_successful_response(auth)

assert capture = @gateway.capture(@amount, auth.authorization)
assert_successful_response(capture)
end

def test_network_tokenization_with_amex_cc_longer_cryptogram
# Generate a random 40 bytes binary amex cryptogram => Base64.encode64(Random.bytes(40))
long_cryptogram = "NZwc40C4eTDWHVDXPekFaKkNYGk26w+GYDZmU50cATbjqOpNxR/eYA==\n"
Expand Down