Skip to content

Commit

Permalink
Datatrans: NT, AP, GP support
Browse files Browse the repository at this point in the history
    Summary:
    -----
    This PR includes for Datatrans the
    NetworkToken, ApplePay and GooglePay code
    to perform transactions with that PM
    [SER-1195](https://spreedly.atlassian.net/browse/SER-1195)

    Tests
    -----
    Remote Test:
    Finished in 23.393965 seconds.
    19 tests, 51 assertions, 0 failures, 0 errors, 0 pendings, 1 omissions, 0 notifications
    100% passed

    Unit Tests:
    23 tests, 111 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 May 6, 2024
1 parent 4b0ee60 commit 18af313
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 172 deletions.
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 })
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)
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

0 comments on commit 18af313

Please sign in to comment.