Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: ----- Adding Datatrans gateway with support for basic functionality including authorize, purchase, capture, refund and void. Scrubbing method. Unit and remote tests. [SER-1193](https://spreedly.atlassian.net/browse/SER-1193) Tests ----- Remote Test: Finished in 18.393965 seconds. 15 tests, 43 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications 100% passed Unit Tests: 19 tests, 45 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed RuboCop: 795 files inspected, no offenses detected
- Loading branch information
Gustavo Sanmartin
authored and
Gustavo Sanmartin
committed
Apr 22, 2024
1 parent
42a3072
commit a8287d3
Showing
4 changed files
with
626 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,179 @@ | ||
module ActiveMerchant #:nodoc: | ||
module Billing #:nodoc: | ||
class DatatransGateway < Gateway | ||
self.test_url = 'https://api.sandbox.datatrans.com/v1/transactions/' | ||
self.live_url = 'https://api.datatrans.com/v1/transactions/' | ||
|
||
self.supported_countries = %w(CH GR US) # to confirm the countries supported. | ||
self.default_currency = 'CHF' | ||
self.currencies_without_fractions = %w(CHF EUR USD) | ||
self.currencies_with_three_decimal_places = %w() | ||
self.supported_cardtypes = %i[master visa american_express unionpay diners_club discover jcb maestro dankort] | ||
|
||
self.money_format = :cents | ||
|
||
self.homepage_url = 'https://www.datatrans.ch/' | ||
self.display_name = 'Datatrans' | ||
|
||
def initialize(options = {}) | ||
requires!(options, :merchant_id, :password) | ||
@merchant_id, @password = options.values_at(:merchant_id, :password) | ||
super | ||
end | ||
|
||
def purchase(money, payment, options = {}) | ||
authorize(money, payment, options.merge(auto_settle: true)) | ||
end | ||
|
||
def authorize(money, payment, options = {}) | ||
post = { refno: options.fetch(:order_id, '') } | ||
add_payment_method(post, payment) | ||
add_currency_amount(post, money, options) | ||
add_billing_address(post, options) | ||
post[:autoSettle] = options[:auto_settle] if options[:auto_settle] | ||
commit('authorize', post) | ||
end | ||
|
||
def capture(money, authorization, options = {}) | ||
post = { refno: options.fetch(:order_id, '') } | ||
transaction_id = authorization.split('|').first | ||
add_currency_amount(post, money, options) | ||
commit('settle', post, { transaction_id: transaction_id }) | ||
end | ||
|
||
def refund(money, authorization, options = {}) | ||
post = { refno: options.fetch(:order_id, '') } | ||
transaction_id = authorization.split('|').first | ||
add_currency_amount(post, money, options) | ||
commit('credit', post, { transaction_id: transaction_id }) | ||
end | ||
|
||
def void(authorization, options = {}) | ||
post = {} | ||
transaction_id = authorization.split('|').first | ||
commit('cancel', post, { transaction_id: transaction_id }) | ||
end | ||
|
||
def supports_scrubbing? | ||
true | ||
end | ||
|
||
def scrub(transcript) | ||
transcript. | ||
gsub(%r((Authorization: Basic )[\w =]+), '\1[FILTERED]'). | ||
gsub(%r((\"number\\":\\")\d+), '\1[FILTERED]\2'). | ||
gsub(%r((\"cvv\\":\\")\d+), '\1[FILTERED]\2') | ||
end | ||
|
||
private | ||
|
||
def add_payment_method(post, payment_method) | ||
post[:card] = { | ||
number: payment_method.number, | ||
cvv: payment_method.verification_value.to_s, | ||
expiryMonth: format(payment_method.month, :two_digits), | ||
expiryYear: format(payment_method.year, :two_digits) | ||
} | ||
end | ||
|
||
def add_billing_address(post, options) | ||
return unless billing_address = options[:billing_address] | ||
|
||
post[:billing] = { | ||
name: billing_address[:name], | ||
street: billing_address[:address1], | ||
street2: billing_address[:address2], | ||
city: billing_address[:city], | ||
country: Country.find(billing_address[:country]).code(:alpha3).value, # pass country alpha 2 to country alpha 3, | ||
phoneNumber: billing_address[:phone], | ||
zipCode: billing_address[:zip], | ||
email: options[:email] | ||
}.compact | ||
end | ||
|
||
def add_currency_amount(post, money, options) | ||
post[:currency] = (options[:currency] || currency(money)) | ||
post[:amount] = amount(money) | ||
end | ||
|
||
def commit(action, post, options = {}) | ||
response = parse(ssl_post(url(action, options), post.to_json, headers)) | ||
succeeded = success_from(action, response) | ||
|
||
Response.new( | ||
succeeded, | ||
message_from(succeeded, response), | ||
response, | ||
authorization: authorization_from(response, action, options), | ||
test: test?, | ||
error_code: error_code_from(response) | ||
) | ||
rescue ResponseError => e | ||
response = parse(e.response.body) | ||
Response.new(false, message_from(false, response), response, test: test?, error_code: error_code_from(response)) | ||
end | ||
|
||
def parse(response) | ||
JSON.parse response | ||
rescue JSON::ParserError | ||
msg = 'Invalid JSON response received from Datatrans. Please contact them for support if you continue to receive this message.' | ||
msg += " (The raw response returned by the API was #{response.inspect})" | ||
{ | ||
'successful' => false, | ||
'response' => {}, | ||
'errors' => [msg] | ||
} | ||
end | ||
|
||
def headers | ||
{ | ||
'Content-Type' => 'application/json; charset=UTF-8', | ||
'Authorization' => "Basic #{Base64.strict_encode64("#{@merchant_id}:#{@password}")}" | ||
} | ||
end | ||
|
||
def url(endpoint, options = {}) | ||
case endpoint | ||
when 'settle', 'credit', 'cancel' | ||
"#{test? ? test_url : live_url}#{options[:transaction_id]}/#{endpoint}" | ||
else | ||
"#{test? ? test_url : live_url}#{endpoint}" | ||
end | ||
end | ||
|
||
def success_from(action, response) | ||
case action | ||
when 'authorize', 'credit' | ||
true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode') | ||
when 'settle', 'cancel' | ||
true if response.dig('response_code') == 204 | ||
else | ||
false | ||
end | ||
end | ||
|
||
def authorization_from(response, action, options = {}) | ||
[response['transactionId'], response['acquirerAuthorizationCode']].join('|') | ||
end | ||
|
||
def message_from(succeeded, response) | ||
return if succeeded | ||
|
||
response.dig('error', 'message') | ||
end | ||
|
||
def error_code_from(response) | ||
response.dig('error', 'code') | ||
end | ||
|
||
def handle_response(response) | ||
case response.code.to_i | ||
when 200...300 | ||
response.body || { response_code: response.code.to_i }.to_json | ||
else | ||
raise ResponseError.new(response) | ||
end | ||
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,171 @@ | ||
require 'test_helper' | ||
|
||
class RemoteDatatransTest < Test::Unit::TestCase | ||
def setup | ||
@gateway = DatatransGateway.new(fixtures(:datatrans)) | ||
|
||
@amount = 756 | ||
@credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 0o6, year: 2025) | ||
@bad_amount = 100000 # anything grather than 500 EUR | ||
|
||
@options = { | ||
order_id: SecureRandom.random_number(1000000000).to_s, | ||
description: 'An authorize', | ||
email: 'john.smith@test.com' | ||
} | ||
|
||
@billing_address = address | ||
end | ||
|
||
def test_successful_authorize | ||
response = @gateway.authorize(@amount, @credit_card, @options) | ||
|
||
assert_success response | ||
assert_include response.params, 'transactionId' | ||
end | ||
|
||
def test_successful_purchase | ||
response = @gateway.purchase(@amount, @credit_card, @options) | ||
assert_success response | ||
assert_include response.params, 'transactionId' | ||
end | ||
|
||
def test_failed_authorize | ||
# the bad amount currently is only setle to EUR currency | ||
response = @gateway.purchase(@bad_amount, @credit_card, @options.merge({ currency: 'EUR' })) | ||
assert_failure response | ||
assert_equal response.error_code, 'BLOCKED_CARD' | ||
assert_equal response.message, 'card blocked' | ||
end | ||
|
||
def test_failed_authorize_invalid_currency | ||
response = @gateway.purchase(@amount, @credit_card, @options.merge({ currency: 'DKK' })) | ||
assert_failure response | ||
assert_equal response.error_code, 'INVALID_PROPERTY' | ||
assert_equal response.message, 'authorize.currency' | ||
end | ||
|
||
def test_successful_capture | ||
authorize_response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success authorize_response | ||
|
||
response = @gateway.capture(@amount, authorize_response.authorization, @options) | ||
assert_success response | ||
assert_equal response.authorization, '|' | ||
end | ||
|
||
def test_successful_refund | ||
purchase_response = @gateway.purchase(@amount, @credit_card, @options) | ||
assert_success purchase_response | ||
|
||
response = @gateway.refund(@amount, purchase_response.authorization, @options) | ||
assert_success response | ||
assert_include response.params, 'transactionId' | ||
end | ||
|
||
def test_successful_capture_with_less_authorized_amount_and_refund | ||
authorize_response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success authorize_response | ||
|
||
capture_response = @gateway.capture(@amount - 100, authorize_response.authorization, @options) | ||
assert_success capture_response | ||
|
||
response = @gateway.refund(@amount - 200, authorize_response.authorization, @options) | ||
assert_success response | ||
end | ||
|
||
def test_failed_partial_capture_already_captured | ||
authorize_response = @gateway.authorize(2500, @credit_card, @options) | ||
assert_success authorize_response | ||
|
||
capture_response = @gateway.capture(100, authorize_response.authorization, @options) | ||
assert_success capture_response | ||
|
||
response = @gateway.capture(100, authorize_response.authorization, @options) | ||
assert_failure response | ||
assert_equal response.error_code, 'INVALID_TRANSACTION_STATUS' | ||
assert_equal response.message, 'already settled' | ||
end | ||
|
||
def test_failed_partial_capture_refund_refund_exceed_captured | ||
authorize_response = @gateway.authorize(200, @credit_card, @options) | ||
assert_success authorize_response | ||
|
||
capture_response = @gateway.capture(100, authorize_response.authorization, @options) | ||
assert_success capture_response | ||
|
||
response = @gateway.refund(200, authorize_response.authorization, @options) | ||
assert_failure response | ||
assert_equal response.error_code, 'INVALID_PROPERTY' | ||
assert_equal response.message, 'credit.amount' | ||
end | ||
|
||
def test_failed_consecutive_partial_refund_when_total_exceed_amount | ||
purchase_response = @gateway.purchase(700, @credit_card, @options) | ||
|
||
assert_success purchase_response | ||
|
||
refund_response_1 = @gateway.refund(200, purchase_response.authorization, @options) | ||
assert_success refund_response_1 | ||
|
||
refund_response_2 = @gateway.refund(200, purchase_response.authorization, @options) | ||
assert_success refund_response_2 | ||
|
||
refund_response_3 = @gateway.refund(200, purchase_response.authorization, @options) | ||
assert_success refund_response_3 | ||
|
||
refund_response_4 = @gateway.refund(200, purchase_response.authorization, @options) | ||
assert_failure refund_response_4 | ||
assert_equal refund_response_4.error_code, 'INVALID_PROPERTY' | ||
assert_equal refund_response_4.message, 'credit.amount' | ||
end | ||
|
||
def test_failed_refund_not_settle_transaction | ||
purchase_response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success purchase_response | ||
|
||
response = @gateway.refund(@amount, purchase_response.authorization, @options) | ||
assert_failure response | ||
assert_equal response.error_code, 'INVALID_TRANSACTION_STATUS' | ||
assert_equal response.message, 'the transaction cannot be credited' | ||
end | ||
|
||
def test_successful_void | ||
authorize_response = @gateway.authorize(@amount, @credit_card, @options) | ||
assert_success authorize_response | ||
|
||
response = @gateway.void(authorize_response.authorization, @options) | ||
assert_success response | ||
|
||
assert_equal response.authorization, '|' | ||
end | ||
|
||
def test_failed_void_because_captured_transaction | ||
omit("the transaction could take about 20 minutes to | ||
pass from settle to transmited, use a previos | ||
transaction acutually transmited and comment this | ||
omition") | ||
|
||
# this is a previos transmited transaction, if the test fail use another, check dashboard to confirm it. | ||
previous_authorization = '240417191339383491|339523493' | ||
response = @gateway.void(previous_authorization, @options) | ||
assert_failure response | ||
assert_equal 'Action denied : Wrong transaction status', response.message | ||
end | ||
|
||
def test_transcript_scrubbing | ||
transcript = capture_transcript(@gateway) do | ||
@gateway.purchase(@amount, @credit_card, @options) | ||
end | ||
transcript = @gateway.scrub(transcript) | ||
|
||
assert_scrubbed(@credit_card.number, transcript) | ||
assert_scrubbed(@credit_card.verification_value, transcript) | ||
end | ||
|
||
def test_successful_purchase_with_billing_address | ||
response = @gateway.purchase(@amount, @credit_card, @options.merge({ billing_address: @billing_address })) | ||
|
||
assert_success response | ||
end | ||
end |
Oops, something went wrong.