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

Datatrans: Add TPV #5183

Merged
merged 1 commit into from
Jul 23, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 64 additions & 26 deletions lib/active_merchant/billing/gateways/datatrans.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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.test_url = 'https://api.sandbox.datatrans.com/v1/'
self.live_url = 'https://api.datatrans.com/v1/'

self.supported_countries = %w(CH GR US) # to confirm the countries supported.
self.default_currency = 'CHF'
Expand Down Expand Up @@ -72,6 +72,28 @@ def void(authorization, options = {})
commit('cancel', post, { transaction_id: transaction_id })
end

def store(payment_method, options = {})
exp_year = format(payment_method.year, :two_digits)
exp_month = format(payment_method.month, :two_digits)

post = {
requests: [
{
type: 'CARD',
pan: payment_method.number,
expiryMonth: exp_month,
expiryYear: exp_year
}
]
}
commit('tokenize', post, { expiry_month: exp_month, expiry_year: exp_year })
end

def unstore(authorization, options = {})
data_alias = authorization.split('|')[2].split('-')[0]
commit('delete_alias', {}, { alias_id: data_alias }, :delete)
end

gasb150 marked this conversation as resolved.
Show resolved Hide resolved
def supports_scrubbing?
true
end
Expand All @@ -86,27 +108,33 @@ def scrub(transcript)
private

def add_payment_method(post, payment_method)
card = build_card(payment_method)
post[:card] = {
expiryMonth: format(payment_method.month, :two_digits),
expiryYear: format(payment_method.year, :two_digits)
}.merge(card)
end

def build_card(payment_method)
if payment_method.is_a?(NetworkTokenizationCreditCard)
{
case payment_method
when String
token, exp_month, exp_year = payment_method.split('|')[2].split('-')
card = {
type: 'ALIAS',
alias: token,
expiryMonth: exp_month,
expiryYear: exp_year
}
when NetworkTokenizationCreditCard
card = {
type: DEVICE_SOURCE[payment_method.source] ? 'DEVICE_TOKEN' : 'NETWORK_TOKEN',
tokenType: DEVICE_SOURCE[payment_method.source] || CREDIT_CARD_SOURCE[card_brand(payment_method)],
token: payment_method.number,
cryptogram: payment_method.payment_cryptogram
cryptogram: payment_method.payment_cryptogram,
expiryMonth: format(payment_method.month, :two_digits),
expiryYear: format(payment_method.year, :two_digits)
}
else
{
when CreditCard
card = {
number: payment_method.number,
cvv: payment_method.verification_value.to_s
cvv: payment_method.verification_value.to_s,
expiryMonth: format(payment_method.month, :two_digits),
expiryYear: format(payment_method.year, :two_digits)
}
end
post[:card] = card
end

def add_3ds_data(post, payment_method, options)
Expand Down Expand Up @@ -157,15 +185,15 @@ def add_currency_amount(post, money, options)
post[:amount] = amount(money)
end

def commit(action, post, options = {})
response = parse(ssl_post(url(action, options), post.to_json, headers))
def commit(action, post, options = {}, method = :post)
response = parse(ssl_request(method, url(action, options), post.to_json, headers))
succeeded = success_from(action, response)

Response.new(
succeeded,
message_from(succeeded, response),
response,
authorization: authorization_from(response),
authorization: authorization_from(response, action, options),
test: test?,
error_code: error_code_from(response)
)
Expand Down Expand Up @@ -196,26 +224,36 @@ def headers
def url(endpoint, options = {})
case endpoint
when 'settle', 'credit', 'cancel'
"#{test? ? test_url : live_url}#{options[:transaction_id]}/#{endpoint}"
"#{test? ? test_url : live_url}transactions/#{options[:transaction_id]}/#{endpoint}"
when 'tokenize'
"#{test? ? test_url : live_url}aliases/#{endpoint}"
when 'delete_alias'
"#{test? ? test_url : live_url}aliases/#{options[:alias_id]}"
else
"#{test? ? test_url : live_url}#{endpoint}"
"#{test? ? test_url : live_url}transactions/#{endpoint}"
end
end

def success_from(action, response)
case action
when 'authorize', 'credit'
true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode')
response.include?('transactionId') && response.include?('acquirerAuthorizationCode')
when 'settle', 'cancel'
true if response.dig('response_code') == 204
response.dig('response_code') == 204
when 'tokenize'
response.dig('responses', 0, 'alias') && response.dig('overview', 'failed') == 0
when 'delete_alias'
response.dig('response_code') == 204
else
false
end
end

def authorization_from(response)
auth = [response['transactionId'], response['acquirerAuthorizationCode']].join('|')
return auth unless auth == '|'
def authorization_from(response, action, options)
string = [response.dig('responses', 0, 'alias'), options[:expiry_month], options[:expiry_year]].join('-') if action == 'tokenize'

auth = [response['transactionId'], response['acquirerAuthorizationCode'], string].join('|')
return auth unless auth == '||'
end

def message_from(succeeded, response)
Expand Down
44 changes: 43 additions & 1 deletion test/remote/gateways/remote_datatrans_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def setup
@gateway = DatatransGateway.new(fixtures(:datatrans))

@amount = 756
@credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025)
@credit_card = credit_card('4242424242424242', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: Time.now.year + 1)
@bad_amount = 100000 # anything grather than 500 EUR
@credit_card_frictionless = credit_card('4000001000000018', verification_value: '123', first_name: 'John', last_name: 'Smith', month: 6, year: 2025)

Expand Down Expand Up @@ -183,6 +183,48 @@ def test_successful_void
assert_equal response.authorization, nil
end

def test_succesful_store_transaction
store = @gateway.store(@credit_card, @options)
assert_success store
assert_include store.params, 'overview'
assert_equal store.params['overview'], { 'total' => 1, 'successful' => 1, 'failed' => 0 }
assert store.params['responses'].is_a?(Array)
assert_include store.params['responses'][0], 'alias'
assert_equal store.params['responses'][0]['maskedCC'], '424242xxxxxx4242'
assert_include store.params['responses'][0], 'fingerprint'
end

def test_successful_unstore
store = @gateway.store(@credit_card, @options)
assert_success store

unstore = @gateway.unstore(store.authorization, @options)
assert_success unstore
assert_equal unstore.params['response_code'], 204
end

def test_successful_store_purchase_unstore_flow
store = @gateway.store(@credit_card, @options)
assert_success store

purchase = @gateway.purchase(@amount, store.authorization, @options)
assert_success purchase
assert_include purchase.params, 'transactionId'

# second purchase to validate multiple use token
second_purchase = @gateway.purchase(@amount, store.authorization, @options)
assert_success second_purchase

unstore = @gateway.unstore(store.authorization, @options)
assert_success unstore

# purchase after unstore to validate deletion
response = @gateway.purchase(@amount, store.authorization, @options)
assert_failure response
assert_equal response.error_code, 'INVALID_ALIAS'
assert_equal response.message, 'authorize.card.alias'
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
Expand Down
67 changes: 61 additions & 6 deletions test/unit/gateways/datatrans_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def setup
}
})

@transaction_reference = '240214093712238757|093712'
@transaction_reference = '240214093712238757|093712|123alias_token_id123|05|25'

@billing_address = address
@no_country_billing_address = address(country: nil)
Expand Down Expand Up @@ -219,6 +219,43 @@ def test_voids
assert_success response
end

def test_store
response = stub_comms(@gateway, :ssl_request) do
@gateway.store(@credit_card, @options)
end.check_request do |_action, endpoint, data, _headers|
assert_match('aliases/tokenize', endpoint)
parsed_data = JSON.parse(data)
request = parsed_data['requests'][0]
assert_equal('CARD', request['type'])
assert_equal(@credit_card.number, request['pan'])
end.respond_with(successful_store_response)

assert_success response
end

def test_unstore
response = stub_comms(@gateway, :ssl_request) do
@gateway.unstore(@transaction_reference, @options)
end.check_request do |_action, endpoint, data, _headers|
assert_match('aliases/123alias_token_id123', endpoint)
assert_equal data, '{}'
end.respond_with(successful_unstore_response)

assert_success response
end

def test_purchase_with_tpv
response = stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@amount, @transaction_reference, @options)
end.check_request do |_action, endpoint, data, _headers|
parsed_data = JSON.parse(data)
common_assertions_authorize_purchase(endpoint, parsed_data)
assert_equal(@transaction_reference.split('|')[2], parsed_data['card']['alias'])
end.respond_with(successful_purchase_response)

assert_success response
end

def test_required_merchant_id_and_password
error = assert_raises ArgumentError do
DatatransGateway.new
Expand Down Expand Up @@ -274,7 +311,7 @@ def test_get_response_message_from_message_user

def test_url_generation_from_action
action = 'test'
assert_equal "#{@gateway.test_url}#{action}", @gateway.send(:url, action)
assert_equal "#{@gateway.test_url}transactions/#{action}", @gateway.send(:url, action)
end

def test_scrub
Expand All @@ -283,10 +320,14 @@ def test_scrub
end

def test_authorization_from
assert_equal '1234|9248', @gateway.send(:authorization_from, { 'transactionId' => '1234', 'acquirerAuthorizationCode' => '9248' })
assert_equal '1234|', @gateway.send(:authorization_from, { 'transactionId' => '1234' })
assert_equal '|9248', @gateway.send(:authorization_from, { 'acquirerAuthorizationCode' => '9248' })
assert_equal nil, @gateway.send(:authorization_from, {})
assert_equal '1234|9248|', @gateway.send(:authorization_from, { 'transactionId' => '1234', 'acquirerAuthorizationCode' => '9248' }, '', {})
gasb150 marked this conversation as resolved.
Show resolved Hide resolved
assert_equal '1234||', @gateway.send(:authorization_from, { 'transactionId' => '1234' }, '', {})
assert_equal '|9248|', @gateway.send(:authorization_from, { 'acquirerAuthorizationCode' => '9248' }, '', {})
assert_equal nil, @gateway.send(:authorization_from, {}, '', {})
# tes for store
assert_equal '||any_alias-any_month-any_year', @gateway.send(:authorization_from, { 'responses' => [{ 'alias' => 'any_alias' }] }, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' })
# handle nil responses or missing keys
assert_equal '||-any_month-any_year', @gateway.send(:authorization_from, {}, 'tokenize', { expiry_month: 'any_month', expiry_year: 'any_year' })
end

def test_parse
Expand Down Expand Up @@ -314,6 +355,19 @@ def successful_capture_response
'{"response_code": 204}'
end

def successful_store_response
'{
"overview":{"total":1, "successful":1, "failed":0},
"responses":
[{
"type":"CARD",
"alias":"7LHXscqwAAEAAAGQvYQBwc5zIs52AGRs",
"maskedCC":"424242xxxxxx4242",
"fingerprint":"F-dSjBoCMOYxomP49vzhdOYE"
}]
}'
end

def common_assertions_authorize_purchase(endpoint, parsed_data)
assert_match('authorize', endpoint)
assert_equal(@options[:order_id], parsed_data['refno'])
Expand All @@ -324,6 +378,7 @@ def common_assertions_authorize_purchase(endpoint, parsed_data)
alias successful_purchase_response successful_authorize_response
alias successful_refund_response successful_authorize_response
alias successful_void_response successful_capture_response
alias successful_unstore_response successful_capture_response

def pre_scrubbed
<<~PRE_SCRUBBED
Expand Down
Loading