-
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.
Description ------------------------- [SER-1351](https://spreedly.atlassian.net/browse/SER-1351) This commit add a the basic structure for the new Nuvei Gateway Unit test ------------------------- Finished in 0.010243 seconds. 3 tests, 9 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 292.88 tests/s, 878.65 assertions/s Remote test ------------------------- Finished in 20.996509 seconds. 10 tests, 21 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 0.48 tests/s, 1.00 assertions/s Rubocop ------------------------- 801 files inspected, no offenses detected
- Loading branch information
Javier Pedroza
committed
Aug 9, 2024
1 parent
dfaccf4
commit dbdaf38
Showing
4 changed files
with
463 additions
and
0 deletions.
There are no files selected for viewing
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,243 @@ | ||
module ActiveMerchant | ||
module Billing | ||
class NuveiGateway < Gateway | ||
self.test_url = 'https://ppp-test.nuvei.com/ppp/api/v1' | ||
self.live_url = 'https://secure.safecharge.com/ppp/api/v1' | ||
|
||
self.supported_countries = %w[US CA IN NZ GB AU US] | ||
self.default_currency = 'USD' | ||
self.money_format = :cents | ||
self.supported_cardtypes = %i[visa master american_express discover union_pay] | ||
self.currencies_without_fractions = %w[CLP KRW JPY ISK MMK PYG UGX VND XAF XOF] | ||
self.homepage_url = 'https://www.nuvei.com/' | ||
self.display_name = 'Nuvei' | ||
|
||
ENDPOINTS_MAPPING = { | ||
authenticate: '/getSessionToken', | ||
purchase: '/payment', # /authorize with transactionType: "Auth" | ||
capture: '/settleTransaction', | ||
refund: '/refundTransaction', | ||
void: '/voidTransaction', | ||
general_credit: '/payout' | ||
} | ||
|
||
def initialize(options = {}) | ||
requires!(options, :merchant_id, :merchant_site_id, :secret_key) | ||
super | ||
fetch_session_token unless session_token_valid? | ||
end | ||
|
||
def authorize(money, payment, options = {}) | ||
post = {} | ||
post[:transactionType] = 'Auth' | ||
build_post_data(post, :authorize) | ||
add_invoice(post, money, options) | ||
add_payment(post, payment) | ||
add_address(post, payment, options) | ||
add_customer_ip(post, options) | ||
|
||
commit(:purchase, post) | ||
end | ||
|
||
def purchase(money, payment, options = {}); end | ||
|
||
def capture(money, authorization, options = {}) | ||
post = {} | ||
post[:relatedTransactionId] = authorization | ||
build_post_data(post, :capture) | ||
add_invoice(post, money, options) | ||
|
||
commit(:capture, post) | ||
end | ||
|
||
def refund(money, authorization, options = {}); end | ||
|
||
def void(authorization, options = {}); end | ||
|
||
def credit(money, payment, options = {}); end | ||
|
||
def supports_scrubbing? | ||
true | ||
end | ||
|
||
def scrub(transcript) | ||
transcript. | ||
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]'). | ||
gsub(%r(("cardNumber\\?":\\?")[^"\\]*)i, '\1[FILTERED]'). | ||
gsub(%r(("cardCvv\\?":\\?")\d+), '\1[FILTERED]'). | ||
gsub(%r(("merchantId\\?":\\?")\d+), '\1[FILTERED]'). | ||
gsub(%r(("merchantSiteId\\?":\\?")\d+), '\1[FILTERED]'). | ||
gsub(%r(("merchantKey\\?":\\?")\d+), '\1[FILTERED]') | ||
end | ||
|
||
private | ||
|
||
def add_customer_ip(post, options) | ||
return unless options[:ip_address] | ||
|
||
post[:deviceDetails] = { | ||
ipAddress: options[:ip_address] | ||
} | ||
end | ||
|
||
def add_invoice(post, money, options) | ||
post[:amount] = amount(money) | ||
post[:currency] = (options[:currency] || currency(money)) | ||
end | ||
|
||
def credit_card_hash(payment) | ||
{ | ||
cardNumber: payment.number, | ||
cardHolderName: payment.name, | ||
expirationMonth: format(payment.month, :two_digits), | ||
expirationYear: format(payment.year, :four_digits), | ||
CVV: payment.verification_value | ||
} | ||
end | ||
|
||
def add_payment(post, payment) | ||
if payment.is_a?(CreditCard) | ||
post[:paymentOption] = { card: credit_card_hash(payment) } | ||
else | ||
post[:paymentOption] = { card: { cardToken: payment } } | ||
end | ||
end | ||
|
||
def add_customer_names(full_name, payment_method) | ||
split_names(full_name).tap do |names| | ||
names[0] = payment_method&.first_name unless names[0].present? || payment_method.is_a?(String) | ||
names[1] = payment_method&.last_name unless names[1].present? || payment_method.is_a?(String) | ||
end | ||
end | ||
|
||
def add_address(post, payment, options) | ||
return unless address = options[:billing_address] || options[:address] | ||
|
||
first_name, last_name = add_customer_names(address[:name], payment) | ||
|
||
post[:billingAddress] = { | ||
email: options[:email], | ||
country: address[:country], | ||
phone: options[:phone] || address[:phone], | ||
firstName: first_name, | ||
lastName: last_name | ||
}.compact | ||
end | ||
|
||
def current_timestamp | ||
Time.now.utc.strftime('%Y%m%d%H%M%S') | ||
end | ||
|
||
def build_post_data(post, action) | ||
post[:merchantId] = @options[:merchant_id] | ||
post[:merchantSiteId] = @options[:merchant_site_id] | ||
post[:timeStamp] = current_timestamp.to_i | ||
post[:clientRequestId] = SecureRandom.uuid | ||
post[:clientUniqueId] = SecureRandom.hex(16) | ||
end | ||
|
||
def calculate_checksum(post, action) | ||
case action | ||
when :authenticate | ||
Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:timeStamp]}#{@options[:secret_key]}") | ||
when :capture | ||
Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:clientUniqueId]}#{post[:amount]}#{post[:currency]}#{post[:relatedTransactionId]}#{post[:timeStamp]}#{@options[:secret_key]}") | ||
else | ||
Digest::SHA256.hexdigest("#{post[:merchantId]}#{post[:merchantSiteId]}#{post[:clientRequestId]}#{post[:amount]}#{post[:currency]}#{post[:timeStamp]}#{@options[:secret_key]}") | ||
end | ||
end | ||
|
||
def send_session_request(post) | ||
post[:checksum] = calculate_checksum(post, 'authenticate') | ||
response = parse(ssl_post(url(:authenticate), post.to_json, headers)).with_indifferent_access | ||
expiration_time = post[:timeStamp] | ||
@options[:session_token] = response.dig('sessionToken') | ||
@options[:token_expires] = expiration_time | ||
@options[:valid_credentials] = true | ||
|
||
Response.new( | ||
response[:sessionToken].present?, | ||
message_from(response), | ||
response, | ||
test: test?, | ||
error_code: response[:errCode] | ||
) | ||
end | ||
|
||
def fetch_session_token(post = {}) | ||
build_post_data(post, :authenticate) | ||
send_session_request(post) | ||
end | ||
|
||
def session_token_valid? | ||
return false unless @options[:session_token] && @options[:token_expires] | ||
|
||
token_time = @options[:token_expires].to_i | ||
current_time = Time.now.utc.to_i | ||
(current_time - token_time) < 15 * 60 | ||
end | ||
|
||
def commit(action, post, authorization = nil, method = :post) | ||
@options[:checksum] = calculate_checksum(post, action) | ||
post[:sessionToken] = @options[:session_token] unless action == :capture | ||
post[:checksum] = @options[:checksum] | ||
|
||
response = parse(ssl_request(method, url(action, authorization), post.to_json, headers)) | ||
|
||
Response.new( | ||
success_from(response), | ||
message_from(response), | ||
response, | ||
authorization: authorization_from(action, response, post), | ||
test: test?, | ||
error_code: error_code_from(action, response) | ||
) | ||
rescue ResponseError => e | ||
response = parse(e.response.body) | ||
# if current access_token is invalid then clean it | ||
if e.response.code == '401' | ||
@options[:session_token] = '' | ||
@options[:new_credentials] = true | ||
end | ||
Response.new(false, message_from(response), response, test: test?) | ||
end | ||
|
||
def url(action, id = nil) | ||
"#{test? ? test_url : live_url}#{ENDPOINTS_MAPPING[action] % id}" | ||
end | ||
|
||
def error_code_from(action, response) | ||
(response[:statusName] || response[:status]) unless success_from(response) | ||
end | ||
|
||
def headers | ||
{ 'Content-Type' => 'application/json' }.tap do |headers| | ||
headers['Authorization'] = "Bearer #{@options[:session_token]}" if @options[:session_token] | ||
end | ||
end | ||
|
||
def parse(body) | ||
body = '{}' if body.blank? | ||
|
||
JSON.parse(body).with_indifferent_access | ||
rescue JSON::ParserError | ||
{ | ||
errors: body, | ||
status: 'Unable to parse JSON response' | ||
}.with_indifferent_access | ||
end | ||
|
||
def success_from(response) | ||
response[:status] == 'SUCCESS' && response[:transactionStatus] == 'APPROVED' | ||
end | ||
|
||
def authorization_from(action, response, post) | ||
response.dig(:transactionId) | ||
end | ||
|
||
def message_from(response) | ||
response[:status] | ||
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,100 @@ | ||
require 'test_helper' | ||
require 'timecop' | ||
|
||
class RemoteNuveiTest < Test::Unit::TestCase | ||
def setup | ||
@gateway = NuveiGateway.new(fixtures(:nuvei)) | ||
|
||
@amount = 100 | ||
@credit_card = credit_card('4761344136141390', verification_value: '999', first_name: 'Cure', last_name: 'Tester') | ||
@declined_card = credit_card('4000128449498204') | ||
|
||
@options = { | ||
email: 'test@gmail.com', | ||
billing_address: address.merge(name: 'Cure Tester'), | ||
ip_address: '127.0.0.1' | ||
} | ||
|
||
@post = { | ||
merchantId: 'test_merchant_id', | ||
merchantSiteId: 'test_merchant_site_id', | ||
clientRequestId: 'test_client_request_id', | ||
amount: 'test_amount', | ||
currency: 'test_currency', | ||
timeStamp: 'test_time_stamp' | ||
} | ||
end | ||
|
||
def test_calculate_checksum | ||
expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_amounttest_currencytest_time_stamp#{@gateway.options[:secret_key]}") | ||
assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :purchase) | ||
end | ||
|
||
def test_calculate_checksum_authenticate | ||
expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_time_stamp#{@gateway.options[:secret_key]}") | ||
@post.delete(:amount) | ||
@post.delete(:currency) | ||
assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :authenticate) | ||
end | ||
|
||
def test_calculate_checksum_capture | ||
expected_checksum = Digest::SHA256.hexdigest("test_merchant_idtest_merchant_site_idtest_client_request_idtest_client_idtest_amounttest_currencytest_transaction_idtest_time_stamp#{@gateway.options[:secret_key]}") | ||
@post[:clientUniqueId] = 'test_client_id' | ||
@post[:relatedTransactionId] = 'test_transaction_id' | ||
assert_equal expected_checksum, @gateway.send(:calculate_checksum, @post, :capture) | ||
end | ||
|
||
def test_transcript_scrubbing | ||
transcript = capture_transcript(@gateway) do | ||
@gateway.authorize(@amount, @credit_card, @options) | ||
end | ||
|
||
@gateway.scrub(transcript) | ||
end | ||
|
||
def test_successful_session_token_generation | ||
response = @gateway.send(:fetch_session_token, @options) | ||
assert_success response | ||
assert_not_nil response.params[:sessionToken] | ||
end | ||
|
||
def test_failed_session_token_generation | ||
@gateway.options[:merchant_site_id] = 123 | ||
response = @gateway.send(:fetch_session_token, {}) | ||
assert_failure response | ||
assert_match 'ERROR', response.message | ||
assert_match 'Invalid merchant site id', response.params['reason'] | ||
end | ||
|
||
def test_successful_authorize | ||
response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success response | ||
assert_not_nil response.params[:transactionId] | ||
assert_match 'SUCCESS', response.message | ||
assert_match 'APPROVED', response.params['transactionStatus'] | ||
end | ||
|
||
def test_failed_authorize | ||
response = @gateway.authorize(@amount, @declined_card, @options) | ||
assert_failure response | ||
assert_match 'DECLINED', response.params['transactionStatus'] | ||
end | ||
|
||
def test_successful_authorize_and_capture | ||
response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success response | ||
|
||
capture_response = @gateway.capture(@amount, response.authorization) | ||
|
||
assert_success capture_response | ||
assert_match 'SUCCESS', capture_response.message | ||
assert_match 'APPROVED', capture_response.params['transactionStatus'] | ||
end | ||
|
||
def test_successful_zero_auth | ||
response = @gateway.authorize(0, @credit_card, @options) | ||
assert_success response | ||
assert_match 'SUCCESS', response.message | ||
assert_match 'APPROVED', response.params['transactionStatus'] | ||
end | ||
end |
Oops, something went wrong.