Skip to content

Commit

Permalink
Nuvei: Adding basic operations
Browse files Browse the repository at this point in the history
Nuvei: Base Gateway Layout

Description
-------------------------

[SER-1349](https://spreedly.atlassian.net/browse/SER-1349)
[SER-1350](https://spreedly.atlassian.net/browse/SER-1350)
[SER-1348](https://spreedly.atlassian.net/browse/SER-1348)

This commit add the basic operation for Nuvei such as Purchase, refund, void and general credit

Unit test
-------------------------
Finished in 68.576328 seconds.
6003 tests, 80235 assertions, 0 failures, 0 errors, 0 pendings,
0 omissions, 0 notifications
100% passed

Remote test
-------------------------
Finished in 37.911283 seconds.
17 tests, 46 assertions, 0 failures, 0 errors, 0 pendings,
0 omissions, 0 notifications
100% passed
0.45 tests/s, 1.21 assertions/s

Rubocop
-------------------------
801 files inspected, no offenses detected
  • Loading branch information
Heavyblade committed Aug 23, 2024
1 parent 6b59d76 commit c2122d0
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 55 deletions.
67 changes: 49 additions & 18 deletions lib/active_merchant/billing/gateways/nuvei.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ def initialize(options = {})
end

def authorize(money, payment, options = {})
post = { transactionType: 'Auth' }
post = { transactionType: options.delete(:transaction_type) || 'Auth' }

build_post_data(post, :authorize)
build_post_data(post)
add_amount(post, money, options)
add_payment_method(post, payment)
add_address(post, payment, options)
Expand All @@ -39,22 +39,54 @@ def authorize(money, payment, options = {})
commit(:purchase, post)
end

def purchase(money, payment, options = {}); end
def purchase(money, payment, options = {})
options[:transaction_type] = 'Sale'
authorize(money, payment, options)
end

def capture(money, authorization, options = {})
post = { relatedTransactionId: authorization }

build_post_data(post, :capture)
build_post_data(post)
add_amount(post, money, options)

commit(:capture, post)
end

def refund(money, authorization, options = {}); end
def refund(money, authorization, options = {})
post = { relatedTransactionId: authorization }

build_post_data(post)
add_amount(post, money, options)

commit(:refund, post)
end

def void(authorization, options = {}); end
def void(authorization, options = {})
post = { relatedTransactionId: authorization }
build_post_data(post)

def credit(money, payment, options = {}); end
commit(:void, post)
end

def verify(credit_card, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(0, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end

def credit(money, payment, options = {})
post = { userTokenId: options[:user_token_id] }

build_post_data(post)
add_amount(post, money, options)
add_payment_method(post, payment, :cardData)
add_address(post, payment, options)
add_customer_ip(post, options)

commit(:general_credit, post.compact)
end

def supports_scrubbing?
true
Expand All @@ -65,6 +97,7 @@ def scrub(transcript)
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r(("cardNumber\\?":\\?")[^"\\]*)i, '\1[FILTERED]').
gsub(%r(("cardCvv\\?":\\?")\d+), '\1[FILTERED]').
gsub(%r(("cardHolderName\\?":\\?")\d+), '\1[FILTERED]').
gsub(%r(("merchantId\\?":\\?")\d+), '\1[FILTERED]').
gsub(%r(("merchantSiteId\\?":\\?")\d+), '\1[FILTERED]').
gsub(%r(("merchantKey\\?":\\?")\d+), '\1[FILTERED]')
Expand Down Expand Up @@ -93,12 +126,9 @@ def credit_card_hash(payment)
}
end

def add_payment_method(post, payment)
if payment.is_a?(CreditCard)
post[:paymentOption] = { card: credit_card_hash(payment) }
else
post[:paymentOption] = { card: { cardToken: payment } }
end
def add_payment_method(post, payment, key = :paymentOption)
payment_data = payment.is_a?(CreditCard) ? credit_card_hash(payment) : { cardToken: payment }
post[key] = key == :cardData ? payment_data : { card: payment_data }
end

def add_customer_names(full_name, payment_method)
Expand Down Expand Up @@ -126,7 +156,7 @@ def current_timestamp
Time.now.utc.strftime('%Y%m%d%H%M%S')
end

def build_post_data(post, action)
def build_post_data(post)
post[:merchantId] = @options[:merchant_id]
post[:merchantSiteId] = @options[:merchant_site_id]
post[:timeStamp] = current_timestamp.to_i
Expand All @@ -139,7 +169,7 @@ def calculate_checksum(post, action)
keys = case action
when :authenticate
[:timeStamp]
when :capture
when :capture, :refund, :void
%i[clientUniqueId amount currency relatedTransactionId timeStamp]
else
%i[amount currency timeStamp]
Expand All @@ -161,12 +191,12 @@ def send_session_request(post)
message_from(response),
response,
test: test?,
error_code: response[:errCode]
error_code: response[:gwErrorCode]
)
end

def fetch_session_token(post = {})
build_post_data(post, :authenticate)
build_post_data(post)
send_session_request(post)
end

Expand Down Expand Up @@ -231,7 +261,8 @@ def authorization_from(action, response, post)
end

def message_from(response)
response[:status]
reason = response[:reason]&.present? ? response[:reason] : nil
response[:gwErrorReason] || reason || response[:transactionStatus]
end
end
end
Expand Down
119 changes: 82 additions & 37 deletions test/remote/gateways/remote_nuvei_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,8 @@ def setup
@options = {
email: 'test@gmail.com',
billing_address: address.merge(name: 'Cure Tester'),
ip_address: '127.0.0.1'
ip: '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
Expand All @@ -62,16 +34,14 @@ 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']
assert_match 'Invalid merchant site id', response.message
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']
assert_match 'APPROVED', response.message
end

def test_failed_authorize
Expand All @@ -87,14 +57,89 @@ def test_successful_authorize_and_capture
capture_response = @gateway.capture(@amount, response.authorization)

assert_success capture_response
assert_match 'SUCCESS', capture_response.message
assert_match 'APPROVED', capture_response.params['transactionStatus']
assert_match 'APPROVED', capture_response.message
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']
assert_match 'APPROVED', response.message
end

def test_successful_purchase
response = @gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_not_nil response.params[:transactionId]
assert_match 'APPROVED', response.message
assert_match 'SUCCESS', response.params['status']
end

def test_failed_purchase
response = @gateway.purchase(@amount, @declined_card, @options)
assert_failure response
assert_match 'DECLINED', response.params['transactionStatus']
end

def test_failed_purchase_with_invalid_cvv
@credit_card.verification_value = nil
response = @gateway.purchase(@amount, @credit_card, @options)
assert_failure response
assert_match 'ERROR', response.params['transactionStatus']
assert_match 'Invalid CVV2', response.message
end

def test_failed_capture_invalid_transaction_id
response = @gateway.capture(@amount, '123')
assert_failure response
assert_match 'ERROR', response.params['status']
assert_match 'Invalid relatedTransactionId', response.message
end

def test_successful_void
response = @gateway.authorize(@amount, @credit_card, @options)
assert_success response

void_response = @gateway.void(response.authorization)
assert_success void_response
assert_match 'SUCCESS', void_response.params['status']
assert_match 'APPROVED', void_response.message
end

def test_failed_void_invalid_transaction_id
response = @gateway.void('123')
assert_failure response
assert_match 'ERROR', response.params['status']
assert_match 'Invalid relatedTransactionId', response.message
end

def test_successful_refund
response = @gateway.purchase(@amount, @credit_card, @options)
assert_success response

refund_response = @gateway.refund(@amount, response.authorization)
assert_success refund_response
assert_match 'SUCCESS', refund_response.params['status']
assert_match 'APPROVED', refund_response.message
end

def test_successful_verify
response = @gateway.verify(@credit_card, @options)
assert_success response
assert_match 'SUCCESS', response.params['status']
assert_match 'APPROVED', response.message
end

def test_successful_general_credit
credit_response = @gateway.credit(@amount, @credit_card, @options.merge!(user_token_id: '123'))
assert_success credit_response
assert_match 'SUCCESS', credit_response.params['status']
assert_match 'APPROVED', credit_response.message
end

def test_failed_general_credit
credit_response = @gateway.credit(@amount, @declined_card, @options)
assert_failure credit_response
assert_match 'ERROR', credit_response.params['status']
assert_match 'Invalid user token', credit_response.message
end
end
44 changes: 44 additions & 0 deletions test/unit/gateways/nuvei_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,44 @@ def test_successful_authorize
end.respond_with(successful_authorize_response)
end

def test_successful_purchase
stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@amount, @credit_card, @options)
end.check_request do |_method, endpoint, data, _headers|
if /payment/.match?(endpoint)
json_data = JSON.parse(data)
assert_match(/#{@amount}/, json_data['amount'])
assert_match(/#{@credit_card.number}/, json_data['paymentOption']['card']['cardNumber'])
assert_match(/#{@credit_card.verification_value}/, json_data['paymentOption']['card']['CVV'])
assert_match(%r(/payment), endpoint)
end
end.respond_with(successful_purchase_response)
end

def test_successful_refund
stub_comms(@gateway, :ssl_request) do
@gateway.refund(@amount, '123456', @options)
end.check_request(skip_response: true) do |_method, endpoint, data, _headers|
json_data = JSON.parse(data)
if /refundTransaction/.match?(endpoint)
assert_match(/123456/, json_data['relatedTransactionId'])
assert_match(/#{@amount}/, json_data['amount'])
end
end
end

def test_successful_credit
stub_comms(@gateway, :ssl_request) do
@gateway.credit(@amount, @credit_card, @options)
end.check_request do |_method, endpoint, data, _headers|
json_data = JSON.parse(data)
if /payout/.match?(endpoint)
assert_match(/#{@amount}/, json_data['amount'])
assert_match(/#{@credit_card.number}/, json_data['cardData']['cardNumber'])
end
end.respond_with(successful_purchase_response)
end

private

def pre_scrubbed
Expand Down Expand Up @@ -140,4 +178,10 @@ def successful_authorize_response
{"internalRequestId":1171104468,"status":"SUCCESS","errCode":0,"reason":"","merchantId":"3755516963854600967","merchantSiteId":"255388","version":"1.0","clientRequestId":"02ba666c-e3e5-4ec9-ae30-3f8500b18c96","sessionToken":"29226538-82c7-4a3c-b363-cb6829b8c32a","clientUniqueId":"c00ed73a7d682bf478295d57bdae3028","orderId":"471361708","paymentOption":{"userPaymentOptionId":"","card":{"ccCardNumber":"4****1390","bin":"476134","last4Digits":"1390","ccExpMonth":"09","ccExpYear":"25","acquirerId":"19","cvv2Reply":"","avsCode":"","cardType":"Debit","cardBrand":"VISA","issuerBankName":"INTL HDQTRS-CENTER OWNED","issuerCountry":"SG","isPrepaid":"false","threeD":{},"processedBrand":"VISA"},"paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"},"transactionStatus":"APPROVED","gwErrorCode":0,"gwExtendedErrorCode":0,"issuerDeclineCode":"","issuerDeclineReason":"","transactionType":"Auth","transactionId":"7110000000001908486","externalTransactionId":"","authCode":"111397","customData":"","fraudDetails":{"finalDecision":"Accept","score":"0"},"externalSchemeTransactionId":"","merchantAdviceCode":""}
RESPONSE
end

def successful_purchase_response
<<~RESPONSE
{"internalRequestId":1172848838, "status":"SUCCESS", "errCode":0, "reason":"", "merchantId":"3755516963854600967", "merchantSiteId":"255388", "version":"1.0", "clientRequestId":"a114381a-0f88-46d0-920c-7b5614f29e5b", "sessionToken":"d3424c9c-dd6d-40dc-85da-a2b92107cbe3", "clientUniqueId":"3ba2a81c46d78837ea819d9f3fe644e7", "orderId":"471833818", "paymentOption":{"userPaymentOptionId":"", "card":{"ccCardNumber":"4****1390", "bin":"476134", "last4Digits":"1390", "ccExpMonth":"09", "ccExpYear":"25", "acquirerId":"19", "cvv2Reply":"", "avsCode":"", "cardType":"Debit", "cardBrand":"VISA", "issuerBankName":"INTL HDQTRS-CENTER OWNED", "issuerCountry":"SG", "isPrepaid":"false", "threeD":{}, "processedBrand":"VISA"}, "paymentAccountReference":"f4iK2pnudYKvTALGdcwEzqj9p4"}, "transactionStatus":"APPROVED", "gwErrorCode":0, "gwExtendedErrorCode":0, "issuerDeclineCode":"", "issuerDeclineReason":"", "transactionType":"Sale", "transactionId":"7110000000001990927", "externalTransactionId":"", "authCode":"111711", "customData":"", "fraudDetails":{"finalDecision":"Accept", "score":"0"}, "externalSchemeTransactionId":"", "merchantAdviceCode":""}
RESPONSE
end
end

0 comments on commit c2122d0

Please sign in to comment.