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: NT, AP, GP support #5110

Merged
merged 2 commits into from May 9, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
97 changes: 59 additions & 38 deletions lib/active_merchant/billing/gateways/datatrans.rb
Expand Up @@ -15,6 +15,16 @@ class DatatransGateway < Gateway
self.homepage_url = 'https://www.datatrans.ch/'
self.display_name = 'Datatrans'

CREDIT_CARD_SOURCE = {
visa: 'VISA',
master: 'MASTERCARD'
}.with_indifferent_access

DEVICE_SOURCE = {
apple_pay: 'APPLE_PAY',
google_pay: 'GOOGLE_PAY'
}.with_indifferent_access

def initialize(options = {})
requires!(options, :merchant_id, :password)
@merchant_id, @password = options.values_at(:merchant_id, :password)
Expand All @@ -26,32 +36,32 @@ def purchase(money, payment, options = {})
end

def authorize(money, payment, options = {})
post = add_payment_method(payment)
post[:refno] = options[:order_id].to_s if options[:order_id]
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[:order_id]&.to_s }
transaction_id, = authorization.split('|')
post = { refno: options.fetch(:order_id, '') }
transaction_id = authorization.split('|').first
add_currency_amount(post, money, options)
commit('settle', post, { transaction_id: transaction_id, authorization: authorization })
commit('settle', post, { transaction_id: transaction_id })
end

def refund(money, authorization, options = {})
post = { refno: options[:order_id]&.to_s }
transaction_id, = authorization.split('|')
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('|')
commit('cancel', post, { transaction_id: transaction_id, authorization: authorization })
transaction_id = authorization.split('|').first
commit('cancel', post, { transaction_id: transaction_id })
gasb150 marked this conversation as resolved.
Show resolved Hide resolved
end

def supports_scrubbing?
Expand All @@ -67,21 +77,33 @@ def scrub(transcript)

private

def add_payment_method(payment_method)
{
card: {
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)
gasb150 marked this conversation as resolved.
Show resolved Hide resolved
if payment_method.is_a?(NetworkTokenizationCreditCard)
{
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
}
else
{
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)
cvv: payment_method.verification_value.to_s
}
}
end
end

def add_billing_address(post, options)
return unless options[:billing_address]
return unless billing_address = options[:billing_address]

billing_address = options[:billing_address]
post[:billing] = {
name: billing_address[:name],
street: billing_address[:address1],
Expand All @@ -100,29 +122,32 @@ def add_currency_amount(post, money, options)
end

def commit(action, post, options = {})
begin
raw_response = ssl_post(url(action, options), post.to_json, headers)
rescue ResponseError => e
raw_response = e.response.body
end

response = parse(raw_response)

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),
authorization: authorization_from(response),
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)
return unless 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
Expand All @@ -144,21 +169,17 @@ def url(endpoint, options = {})
def success_from(action, response)
case action
when 'authorize', 'credit'
return true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode')
true if response.include?('transactionId') && response.include?('acquirerAuthorizationCode')
when 'settle', 'cancel'
return true if response.dig('response_code') == 204
true if response.dig('response_code') == 204
else
false
end
end

def authorization_from(response, action, options = {})
case action
when 'settle'
options[:authorization]
else
[response['transactionId'], response['acquirerAuthorizationCode']].join('|')
end
def authorization_from(response)
auth = [response['transactionId'], response['acquirerAuthorizationCode']].join('|')
return auth unless auth == '|'
end

def message_from(succeeded, response)
Expand Down
84 changes: 60 additions & 24 deletions test/remote/gateways/remote_datatrans_test.rb
Expand Up @@ -16,36 +16,45 @@ def setup

@billing_address = address

@execute_threed = {
execute_threed: true,
redirect_url: 'http://www.example.com/redirect',
callback_url: 'http://www.example.com/callback',
three_ds_2: {
browser_info: {
width: 390,
height: 400,
depth: 24,
timezone: 300,
user_agent: 'Spreedly Agent',
java: false,
javascript: true,
language: 'en-US',
browser_size: '05',
accept_header: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
}
}
}
@google_pay_card = network_tokenization_credit_card(
'4900000000000094',
payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=',
month: '06',
year: '2025',
source: :google_pay,
verification_value: 569
)

@apple_pay_card = network_tokenization_credit_card(
'4900000000000094',
payment_cryptogram: 'YwAAAAAABaYcCMX/OhNRQAAAAAA=',
month: '06',
year: '2025',
source: :apple_pay,
verification_value: 569
)

@nt_credit_card = network_tokenization_credit_card(
'4111111111111111',
payment_cryptogram: 'EHuWW9PiBkWvqE5juRwDzAUFBAk=',
eci: '07',
source: :network_token,
verification_value: '737',
brand: 'visa'
)
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
Expand All @@ -69,7 +78,7 @@ def test_successful_capture

response = @gateway.capture(@amount, authorize_response.authorization, @options)
assert_success response
assert_equal authorize_response.authorization, response.authorization
assert_equal response.authorization, nil
end

def test_successful_refund
Expand All @@ -78,6 +87,7 @@ def test_successful_refund

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
Expand All @@ -86,9 +96,8 @@ def test_successful_capture_with_less_authorized_amount_and_refund

capture_response = @gateway.capture(@amount - 100, authorize_response.authorization, @options)
assert_success capture_response
assert_equal authorize_response.authorization, capture_response.authorization

response = @gateway.refund(@amount - 200, capture_response.authorization, @options)
response = @gateway.refund(@amount - 200, authorize_response.authorization, @options)
assert_success response
end

Expand All @@ -99,7 +108,7 @@ def test_failed_partial_capture_already_captured
capture_response = @gateway.capture(100, authorize_response.authorization, @options)
assert_success capture_response

response = @gateway.capture(100, capture_response.authorization, @options)
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'
Expand All @@ -112,7 +121,7 @@ def test_failed_partial_capture_refund_refund_exceed_captured
capture_response = @gateway.capture(100, authorize_response.authorization, @options)
assert_success capture_response

response = @gateway.refund(200, capture_response.authorization, @options)
response = @gateway.refund(200, authorize_response.authorization, @options)
assert_failure response
assert_equal response.error_code, 'INVALID_PROPERTY'
assert_equal response.message, 'credit.amount'
Expand Down Expand Up @@ -154,6 +163,8 @@ def test_successful_void

response = @gateway.void(authorize_response.authorization, @options)
assert_success response

assert_equal response.authorization, nil
end

def test_failed_void_because_captured_transaction
Expand Down Expand Up @@ -184,4 +195,29 @@ def test_successful_purchase_with_billing_address

assert_success response
end

def test_successful_purchase_with_network_token
response = @gateway.purchase(@amount, @nt_credit_card, @options)

assert_success response
end

def test_successful_purchase_with_apple_pay
response = @gateway.purchase(@amount, @apple_pay_card, @options)

assert_success response
end

def test_successful_authorize_with_google_pay
response = @gateway.authorize(@amount, @google_pay_card, @options)
assert_success response
end

def test_successful_void_with_google_pay
authorize_response = @gateway.authorize(@amount, @google_pay_card, @options)
assert_success authorize_response

response = @gateway.void(authorize_response.authorization, @options)
assert_success response
end
end