Skip to content

Commit

Permalink
Ogone: Enable 3ds Global for Ogone Gateway
Browse files Browse the repository at this point in the history
Description
-------------------------------------------

This commit introduces the support for 3DS Global payments using the Ogone Direct API through GlobalCollect.
As Ogone and GlobalCollect share the same underlying payment service provider (PSP), Worldline, we can leverage
the new attribute 'ogone_direct' to use the appropriate credentials and endpoint to connect with the Ogone Direct API.

[SER-562](https://spreedly.atlassian.net/browse/SER-562)

UNIT TEST
-------------------------------------------

Finished in 0.253826 seconds.

44 tests, 225 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed

173.35 tests/s, 886.43 assertions/s

REMOTE TEST
-------------------------------------------

Finished in 71.318909 seconds.

43 tests, 115 assertions, 1 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
97.6744% passed

0.60 tests/s, 1.61 assertions/s

Note:
During testing, a single failure related to installment processing was identified with GlobalCollect.
The error message "NO_ACQUIRER_CONFIGURED_FOR_INSTALLMENTS" I think that the issue may be related to
GlobalCollect's account configuration, which is outside the scope of this update.

RUBOCOP
-------------------------------------------

760 files inspected, no offenses detected
  • Loading branch information
Javier Pedroza authored and naashton committed May 25, 2023
1 parent 87f20dd commit 09156cb
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 29 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
== HEAD
* Payu Latam - Update error code method to surface network code [yunnydang] #4773
* CyberSource: Handling Canadian bank accounts [heavyblade] #4764
* CommerceHub: Enabling multi-use public key encryption [jherreraa] #4771
* CyberSource Rest: Fixing currency detection [heavyblade] #4777
* CyberSource: Allow business rules for requests with network tokens [aenand] #4764
* Adyen: Update Mastercard error messaging [kylene-spreedly] #4770
* Authorize.net: Update mapping for billing address phone number [jcreiff] #4778
* Braintree: Update mapping for billing address phone number [jcreiff] #4779
* CommerceHub: Enabling multi-use public key encryption [jherreraa] #4771
* Ogone: Enable 3ds Global for Ogone Gateway [javierpedrozaing] #4776

== Version 1.129.0 (May 3rd, 2023)
* Adyen: Update selectedBrand mapping for Google Pay [jcreiff] #4763
Expand Down
60 changes: 41 additions & 19 deletions lib/active_merchant/billing/gateways/global_collect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class GlobalCollectGateway < Gateway
class_attribute :preproduction_url
class_attribute :ogone_direct_test
class_attribute :ogone_direct_live

self.display_name = 'GlobalCollect'
self.homepage_url = 'http://www.globalcollect.com/'

self.test_url = 'https://eu.sandbox.api-ingenico.com'
self.preproduction_url = 'https://world.preprod.api-ingenico.com'
self.live_url = 'https://world.api-ingenico.com'
self.ogone_direct_test = 'https://payment.preprod.direct.worldline-solutions.com'
self.ogone_direct_live = 'https://payment.direct.worldline-solutions.com'

self.supported_countries = %w[AD AE AG AI AL AM AO AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BW BY BZ CA CC CD CF CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HN HR HT HU ID IE IL IM IN IS IT JM JO JP KE KG KH KI KM KN KR KW KY KZ LA LB LC LI LK LR LS LT LU LV MA MC MD ME MF MG MH MK MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PL PN PS PT PW QA RE RO RS RU RW SA SB SC SE SG SH SI SJ SK SL SM SN SR ST SV SZ TC TD TG TH TJ TL TM TN TO TR TT TV TW TZ UA UG US UY UZ VC VE VG VI VN WF WS ZA ZM ZW]
self.default_currency = 'USD'
Expand Down Expand Up @@ -114,7 +118,7 @@ def add_order(post, money, options, capture: false)
post['order']['references']['invoiceData'] = {
'invoiceNumber' => options[:invoice]
}
add_airline_data(post, options)
add_airline_data(post, options) unless ogone_direct?
add_lodging_data(post, options)
add_number_of_installments(post, options) if options[:number_of_installments]
end
Expand Down Expand Up @@ -248,9 +252,10 @@ def add_creator_info(post, options)
end

def add_amount(post, money, options = {})
currency_ogone = 'EUR' if ogone_direct?
post['amountOfMoney'] = {
'amount' => amount(money),
'currencyCode' => options[:currency] || currency(money)
'currencyCode' => options[:currency] || currency_ogone || currency(money)
}
end

Expand All @@ -262,7 +267,7 @@ def add_payment(post, payment, options)
product_id = options[:payment_product_id] || BRAND_MAP[payment.brand]
specifics_inputs = {
'paymentProductId' => product_id,
'skipAuthentication' => 'true', # refers to 3DSecure
'skipAuthentication' => options[:skip_authentication] || 'true', # refers to 3DSecure
'skipFraudService' => 'true',
'authorizationMode' => pre_authorization
}
Expand Down Expand Up @@ -377,18 +382,24 @@ def add_fraud_fields(post, options)
def add_external_cardholder_authentication_data(post, options)
return unless threeds_2_options = options[:three_d_secure]

authentication_data = {}
authentication_data[:acsTransactionId] = threeds_2_options[:acs_transaction_id] if threeds_2_options[:acs_transaction_id]
authentication_data[:cavv] = threeds_2_options[:cavv] if threeds_2_options[:cavv]
authentication_data[:cavvAlgorithm] = threeds_2_options[:cavv_algorithm] if threeds_2_options[:cavv_algorithm]
authentication_data[:directoryServerTransactionId] = threeds_2_options[:ds_transaction_id] if threeds_2_options[:ds_transaction_id]
authentication_data[:eci] = threeds_2_options[:eci] if threeds_2_options[:eci]
authentication_data[:threeDSecureVersion] = threeds_2_options[:version] if threeds_2_options[:version]
authentication_data[:validationResult] = threeds_2_options[:authentication_response_status] if threeds_2_options[:authentication_response_status]
authentication_data[:xid] = threeds_2_options[:xid] if threeds_2_options[:xid]
authentication_data = {
priorThreeDSecureData: { acsTransactionId: threeds_2_options[:acs_transaction_id] }.compact,
cavv: threeds_2_options[:cavv],
cavvAlgorithm: threeds_2_options[:cavv_algorithm],
directoryServerTransactionId: threeds_2_options[:ds_transaction_id],
eci: threeds_2_options[:eci],
threeDSecureVersion: threeds_2_options[:version] || options[:three_ds_version],
validationResult: threeds_2_options[:authentication_response_status],
xid: threeds_2_options[:xid],
acsTransactionId: threeds_2_options[:acs_transaction_id],
flow: threeds_2_options[:flow]
}.compact

post['cardPaymentMethodSpecificInput'] ||= {}
post['cardPaymentMethodSpecificInput']['threeDSecure'] ||= {}
post['cardPaymentMethodSpecificInput']['threeDSecure']['merchantFraudRate'] = threeds_2_options[:merchant_fraud_rate]
post['cardPaymentMethodSpecificInput']['threeDSecure']['exemptionRequest'] = threeds_2_options[:exemption_request]
post['cardPaymentMethodSpecificInput']['threeDSecure']['secureCorporatePayment'] = threeds_2_options[:secure_corporate_payment]
post['cardPaymentMethodSpecificInput']['threeDSecure']['externalCardholderAuthenticationData'] = authentication_data unless authentication_data.empty?
end

Expand All @@ -402,17 +413,28 @@ def parse(body)

def url(action, authorization)
return preproduction_url + uri(action, authorization) if @options[:url_override].to_s == 'preproduction'
return ogone_direct_url(action, authorization) if ogone_direct?

(test? ? test_url : live_url) + uri(action, authorization)
end

def ogone_direct_url(action, authorization)
(test? ? ogone_direct_test : ogone_direct_live) + uri(action, authorization)
end

def ogone_direct?
@options[:url_override].to_s == 'ogone_direct'
end

def uri(action, authorization)
uri = "/v1/#{@options[:merchant_id]}/"
version = ogone_direct? ? 'v2' : 'v1'
uri = "/#{version}/#{@options[:merchant_id]}/"
case action
when :authorize
uri + 'payments'
when :capture
uri + "payments/#{authorization}/approve"
capture_name = ogone_direct? ? 'capture' : 'approve'
uri + "payments/#{authorization}/#{capture_name}"
when :refund
uri + "payments/#{authorization}/refund"
when :void
Expand All @@ -423,7 +445,7 @@ def uri(action, authorization)
end

def idempotency_key_for_signature(options)
"x-gcs-idempotence-key:#{options[:idempotency_key]}" if options[:idempotency_key]
"x-gcs-idempotence-key:#{options[:idempotency_key]}" if options[:idempotency_key] && !ogone_direct?
end

def commit(method, action, post, authorization: nil, options: {})
Expand Down Expand Up @@ -461,7 +483,7 @@ def headers(method, action, post, authorization = nil, options = {})
'Date' => date
}

headers['X-GCS-Idempotence-Key'] = options[:idempotency_key] if options[:idempotency_key]
headers['X-GCS-Idempotence-Key'] = options[:idempotency_key] if options[:idempotency_key] && !ogone_direct?
headers
end

Expand All @@ -474,13 +496,13 @@ def auth_digest(method, action, post, authorization = nil, options = {})
#{uri(action, authorization)}
REQUEST
data = data.each_line.reject { |line| line.strip == '' }.join
digest = OpenSSL::Digest.new('sha256')
digest = OpenSSL::Digest.new('SHA256')
key = @options[:secret_api_key]
"GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data))}"
"GCS v1HMAC:#{@options[:api_key_id]}:#{Base64.strict_encode64(OpenSSL::HMAC.digest(digest, key, data)).strip}"
end

def date
@date ||= Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S %Z') # Must be same in digest and HTTP header
@date ||= Time.now.gmtime.strftime('%a, %d %b %Y %H:%M:%S GMT')
end

def content_type
Expand Down
9 changes: 7 additions & 2 deletions test/fixtures.yml
Original file line number Diff line number Diff line change
Expand Up @@ -439,8 +439,13 @@ garanti:

global_collect:
merchant_id: 2196
api_key_id: c91d6752cbbf9cf1
secret_api_key: xHjQr5gL9Wcihkqoj4w/UQugdSCNXM2oUQHG5C82jy4=
api_key_id: b2311c2c832dd238
secret_api_key: Av5wKihoVlLN8SnGm6669hBHyG4Y4aS4KwaZUCvEIbY=

global_collect_direct:
merchant_id: "NamastayTest"
api_key_id: "CF4CDF3F45F13C5CCBD0"
secret_api_key: "mvcEXR7Rem+KJE/atKsQ3Luqv37VEvTe2VOH5/Ibqd90VDzQ71Ht41RBVVyJuebzGnFu30dYpptgdrCcNvAu5A=="

global_transport:
global_user_name: "USERNAME"
Expand Down
117 changes: 112 additions & 5 deletions test/remote/gateways/remote_global_collect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ def setup
@gateway_preprod = GlobalCollectGateway.new(fixtures(:global_collect_preprod))
@gateway_preprod.options[:url_override] = 'preproduction'

@gateway_direct = GlobalCollectGateway.new(fixtures(:global_collect_direct))
@gateway_direct.options[:url_override] = 'ogone_direct'

@amount = 100
@credit_card = credit_card('4567350000427977')
@credit_card_challenge_3ds2 = credit_card('4874970686672022')
@naranja_card = credit_card('5895620033330020', brand: 'naranja')
@cabal_card = credit_card('6271701225979642', brand: 'cabal')
@declined_card = credit_card('5424180279791732')
Expand Down Expand Up @@ -63,6 +67,14 @@ def test_successful_purchase
assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status']
end

def test_successful_purchase_ogone_direct
options = @preprod_options.merge(requires_approval: false, currency: 'EUR')
response = @gateway_direct.purchase(@accepted_amount, @credit_card, options)
assert_success response
assert_equal 'Succeeded', response.message
assert_equal 'PENDING_CAPTURE', response.params['payment']['status']
end

def test_successful_purchase_with_naranja
options = @preprod_options.merge(requires_approval: false, currency: 'ARS')
response = @gateway_preprod.purchase(1000, @naranja_card, options)
Expand Down Expand Up @@ -145,8 +157,8 @@ def test_successful_purchase_with_more_options
end

def test_successful_purchase_with_installments
options = @options.merge(number_of_installments: 2)
response = @gateway.purchase(@amount, @credit_card, options)
options = @preprod_options.merge(number_of_installments: 2, currency: 'EUR')
response = @gateway_direct.purchase(@amount, @credit_card, options)
assert_success response
assert_equal 'Succeeded', response.message
end
Expand All @@ -159,14 +171,19 @@ def test_successful_purchase_with_requires_approval_true
response = @gateway.purchase(@amount, @credit_card, options)
assert_success response
assert_equal 'Succeeded', response.message
assert_equal 'CAPTURE_REQUESTED', response.params['payment']['status']
end

# When requires_approval is false, `purchase` will only make an `auth` call
# to request capture (and no subsequent `capture` call).
def test_successful_purchase_with_requires_approval_false
def test_successful_purchase_with_requires_approval_false_ogone_direct
options = @options.merge(requires_approval: false)
response = @gateway_direct.purchase(@amount, @credit_card, options)
assert_success response
assert_equal 'Succeeded', response.message
end

def test_successful_purchase_with_requires_approval_false
options = @options.merge(requires_approval: false)
response = @gateway.purchase(@amount, @credit_card, options)
assert_success response
assert_equal 'Succeeded', response.message
Expand All @@ -193,6 +210,56 @@ def test_successful_authorize_via_normalized_3ds2_fields
assert_equal 'Succeeded', response.message
end

def test_successful_authorize_via_3ds2_fields_direct_api
options = @options.merge(
currency: 'EUR',
phone: '5555555555',
three_d_secure: {
version: '2.1.0',
eci: '05',
cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=',
xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=',
ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC',
acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9',
cavv_algorithm: 1,
authentication_response_status: 'Y',
challenge_indicator: 'no-challenge-requested',
flow: 'frictionless'
}
)

response = @gateway_direct.authorize(@amount, @credit_card, options)
assert_success response
assert_match 'PENDING_CAPTURE', response.params['payment']['status']
assert_match 'jJ81HADVRtXfCBATEp01CJUAAAA=', response.params['payment']['paymentOutput']['cardPaymentMethodSpecificOutput']['threeDSecureResults']['cavv']
assert_equal 'Succeeded', response.message
end

def test_successful_purchase_via_3ds2_fields_direct_api_challenge
options = @options.merge(
currency: 'EUR',
phone: '5555555555',
is_recurring: true,
skip_authentication: false,
three_d_secure: {
version: '2.1.0',
eci: '05',
cavv: 'jJ81HADVRtXfCBATEp01CJUAAAA=',
xid: 'BwABBJQ1AgAAAAAgJDUCAAAAAAA=',
ds_transaction_id: '97267598-FAE6-48F2-8083-C23433990FBC',
acs_transaction_id: '13c701a3-5a88-4c45-89e9-ef65e50a8bf9',
cavv_algorithm: 1,
authentication_response_status: 'Y',
challenge_indicator: 'challenge-required'
}
)

response = @gateway_direct.purchase(@amount, @credit_card_challenge_3ds2, options)
assert_success response
assert_match 'CAPTURE_REQUESTED', response.params['status']
assert_equal 'Succeeded', response.message
end

def test_successful_purchase_with_airline_data
options = @options.merge(
airline_data: {
Expand Down Expand Up @@ -321,6 +388,14 @@ def test_successful_purchase_with_blank_name
assert_equal 'Succeeded', response.message
end

def test_unsuccessful_purchase_with_blank_name_ogone_direct
credit_card = credit_card('4567350000427977', { first_name: nil, last_name: nil })

response = @gateway_direct.purchase(@amount, credit_card, @options)
assert_failure response
assert_equal 'PARAMETER_NOT_FOUND_IN_REQUEST', response.message
end

def test_successful_purchase_with_pre_authorization_flag
response = @gateway.purchase(@accepted_amount, @credit_card, @options.merge(pre_authorization: true))
assert_success response
Expand All @@ -347,6 +422,12 @@ def test_failed_purchase
assert_equal 'Not authorised', response.message
end

def test_failed_purchase_ogone_direct
response = @gateway_direct.purchase(@rejected_amount, @declined_card, @options)
assert_failure response
assert_equal 'cardPaymentMethodSpecificInput.card.cardNumber does not match with cardPaymentMethodSpecificInput.paymentProductId.', response.message
end

def test_successful_authorize_and_capture
auth = @gateway.authorize(@amount, @credit_card, @options)
assert_success auth
Expand All @@ -368,13 +449,18 @@ def test_failed_authorize
assert_equal 'Not authorised', response.message
end

def test_failed_authorize_ogone_direct
response = @gateway_direct.authorize(@amount, @declined_card, @options)
assert_failure response
assert_equal 'cardPaymentMethodSpecificInput.card.cardNumber does not match with cardPaymentMethodSpecificInput.paymentProductId.', response.message
end

def test_partial_capture
auth = @gateway.authorize(@amount, @credit_card, @options)
assert_success auth

assert capture = @gateway.capture(@amount - 1, auth.authorization)
assert_success capture
assert_equal 99, capture.params['payment']['paymentOutput']['amountOfMoney']['amount']
end

def test_failed_capture
Expand Down Expand Up @@ -416,6 +502,15 @@ def test_successful_void
assert_equal 'Succeeded', void.message
end

def test_successful_void_ogone_direct
auth = @gateway_direct.authorize(@amount, @credit_card, @options)
assert_success auth

assert void = @gateway_direct.void(auth.authorization)
assert_success void
assert_equal 'Succeeded', void.message
end

def test_failed_void
response = @gateway.void('123')
assert_failure response
Expand Down Expand Up @@ -450,12 +545,24 @@ def test_successful_verify
assert_equal 'Succeeded', response.message
end

def test_successful_verify_ogone_direct
response = @gateway_direct.verify(@credit_card, @options)
assert_success response
assert_equal 'Succeeded', response.message
end

def test_failed_verify
response = @gateway.verify(@declined_card, @options)
assert_failure response
assert_equal 'Not authorised', response.message
end

def test_failed_verify_ogone_direct
response = @gateway_direct.verify(@declined_card, @options)
assert_failure response
assert_equal false, response.params['paymentResult']['payment']['statusOutput']['isAuthorized']
end

def test_invalid_login
gateway = GlobalCollectGateway.new(merchant_id: '', api_key_id: '', secret_api_key: '')
response = gateway.purchase(@amount, @credit_card, @options)
Expand Down
Loading

0 comments on commit 09156cb

Please sign in to comment.