Skip to content

Commit

Permalink
Braintree: Create credit card nonce (activemerchant#4897)
Browse files Browse the repository at this point in the history
Co-authored-by: Gustavo Sanmartin <[email protected]>
  • Loading branch information
gasb150 and Gustavo Sanmartin committed Oct 3, 2023
1 parent b10e6c2 commit f207855
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* Adding Oauth Response for access tokens [almalee24] #4851
* CheckoutV2: Update stored credentials [almalee24] #4901
* Revert "Adding Oauth Response for access tokens" [almalee24] #4906
* Braintree: Create credit card nonce [gasb150] #4897

== Version 1.135.0 (August 24, 2023)
* PaymentExpress: Correct endpoints [steveh] #4827
Expand Down
75 changes: 60 additions & 15 deletions lib/active_merchant/billing/gateways/braintree/token_nonce.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def create_token_nonce_for_payment_method(payment_method)
json_response = JSON.parse(resp)

message = json_response['errors'].map { |err| err['message'] }.join("\n") if json_response['errors'].present?
token = json_response.dig('data', 'tokenizeUsBankAccount', 'paymentMethod', 'id')
token = token_from(payment_method, json_response)

return token, message
end
Expand All @@ -41,7 +41,7 @@ def client_token

private

def graphql_query
def graphql_bank_query
<<-GRAPHQL
mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
tokenizeUsBankAccount(input: $input) {
Expand All @@ -58,6 +58,23 @@ def graphql_query
GRAPHQL
end

def graphql_credit_query
<<-GRAPHQL
mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) {
tokenizeCreditCard(input: $input) {
paymentMethod {
id
details {
... on CreditCardDetails {
last4
}
}
}
}
}
GRAPHQL
end

def billing_address_from_options
return nil if options[:billing_address].blank?

Expand All @@ -72,7 +89,42 @@ def billing_address_from_options
}.compact
end

def build_nonce_credit_card_request(payment_method)
billing_address = billing_address_from_options
key_replacements = { city: :locality, state: :region, zipCode: :postalCode }
billing_address.transform_keys! { |key| key_replacements[key] || key }
{
creditCard: {
number: payment_method.number,
expirationYear: payment_method.year.to_s,
expirationMonth: payment_method.month.to_s.rjust(2, '0'),
cvv: payment_method.verification_value,
cardholderName: payment_method.name,
billingAddress: billing_address
}
}
end

def build_nonce_request(payment_method)
input = payment_method.is_a?(Check) ? build_nonce_bank_request(payment_method) : build_nonce_credit_card_request(payment_method)
graphql_query = payment_method.is_a?(Check) ? graphql_bank_query : graphql_credit_query

{
clientSdkMetadata: {
platform: 'web',
source: 'client',
integration: 'custom',
sessionId: SecureRandom.uuid,
version: '3.83.0'
},
query: graphql_query,
variables: {
input: input
}
}.to_json
end

def build_nonce_bank_request(payment_method)
input = {
usBankAccount: {
achMandate: options[:ach_mandate],
Expand All @@ -94,19 +146,12 @@ def build_nonce_request(payment_method)
}
end

{
clientSdkMetadata: {
platform: 'web',
source: 'client',
integration: 'custom',
sessionId: SecureRandom.uuid,
version: '3.83.0'
},
query: graphql_query,
variables: {
input: input
}
}.to_json
input
end

def token_from(payment_method, response)
tokenized_field = payment_method.is_a?(Check) ? 'tokenizeUsBankAccount' : 'tokenizeCreditCard'
response.dig('data', tokenized_field, 'paymentMethod', 'id')
end
end
end
Expand Down
13 changes: 11 additions & 2 deletions test/remote/gateways/remote_braintree_token_nonce_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_unsucesfull_create_token_with_invalid_state
tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account)

assert_nil tokenized_bank_account
assert_equal "Field 'state' of variable 'input' has coerced Null value for NonNull type 'UsStateCode!'", err_messages
assert_equal "Variable 'input' has an invalid value: Field 'state' has coerced Null value for NonNull type 'UsStateCode!'", err_messages
end

def test_unsucesfull_create_token_with_invalid_zip_code
Expand All @@ -57,7 +57,7 @@ def test_unsucesfull_create_token_with_invalid_zip_code
tokenized_bank_account, err_messages = generator.create_token_nonce_for_payment_method(bank_account)

assert_nil tokenized_bank_account
assert_equal "Field 'zipCode' of variable 'input' has coerced Null value for NonNull type 'UsZipCode!'", err_messages
assert_equal "Variable 'input' has an invalid value: Field 'zipCode' has coerced Null value for NonNull type 'UsZipCode!'", err_messages
end

def test_url_generation
Expand All @@ -80,4 +80,13 @@ def test_url_generation

assert_equal 'https://payments.braintree-api.com/graphql', generator.url
end

def test_successfully_create_token_nonce_for_credit_card
generator = TokenNonce.new(@braintree_backend, @options)
credit_card = credit_card('4111111111111111')
tokenized_credit_card, err_messages = generator.create_token_nonce_for_payment_method(credit_card)
assert_not_nil tokenized_credit_card
assert_match %r(^tokencc_), tokenized_credit_card
assert_nil err_messages
end
end
187 changes: 187 additions & 0 deletions test/unit/gateways/braintree_token_nonce_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
require 'test_helper'

class BraintreeTokenNonceTest < Test::Unit::TestCase
def setup
@gateway = BraintreeBlueGateway.new(
merchant_id: 'test',
public_key: 'test',
private_key: 'test',
test: true
)

@braintree_backend = @gateway.instance_eval { @braintree_gateway }

@options = {
billing_address: {
name: 'Adrain',
address1: '96706 Onie Plains',
address2: '01897 Alysa Lock',
country: 'XXX',
city: 'Miami',
state: 'FL',
zip: '32191',
phone_number: '693-630-6935'
},
ach_mandate: 'ach_mandate'
}
@generator = TokenNonce.new(@braintree_backend, @options)
end

def test_build_nonce_request_for_credit_card
credit_card = credit_card('4111111111111111')
response = @generator.send(:build_nonce_request, credit_card)
parse_response = JSON.parse response
assert_client_sdk_metadata(parse_response)
assert_equal normalize_graph(parse_response['query']), normalize_graph(credit_card_query)
assert_includes parse_response['variables']['input'], 'creditCard'

credit_card_input = parse_response['variables']['input']['creditCard']

assert_equal credit_card_input['number'], credit_card.number
assert_equal credit_card_input['expirationYear'], credit_card.year.to_s
assert_equal credit_card_input['expirationMonth'], credit_card.month.to_s.rjust(2, '0')
assert_equal credit_card_input['cvv'], credit_card.verification_value
assert_equal credit_card_input['cardholderName'], credit_card.name
assert_billing_address_mapping(credit_card_input, credit_card)
end

def test_build_nonce_request_for_bank_account
bank_account = check({ account_number: '4012000033330125', routing_number: '011000015' })
response = @generator.send(:build_nonce_request, bank_account)
parse_response = JSON.parse response
assert_client_sdk_metadata(parse_response)
assert_equal normalize_graph(parse_response['query']), normalize_graph(bank_account_query)
assert_includes parse_response['variables']['input'], 'usBankAccount'

bank_account_input = parse_response['variables']['input']['usBankAccount']

assert_equal bank_account_input['routingNumber'], bank_account.routing_number
assert_equal bank_account_input['accountNumber'], bank_account.account_number
assert_equal bank_account_input['accountType'], bank_account.account_type.upcase
assert_equal bank_account_input['achMandate'], @options[:ach_mandate]

assert_billing_address_mapping(bank_account_input, bank_account)

assert_equal bank_account_input['individualOwner']['firstName'], bank_account.first_name
assert_equal bank_account_input['individualOwner']['lastName'], bank_account.last_name
end

def test_token_from
credit_card = credit_card(number: 4111111111111111)
c_token = @generator.send(:token_from, credit_card, token_credit_response)
assert_match(/tokencc_/, c_token)

bakn_account = check({ account_number: '4012000033330125', routing_number: '011000015' })
b_token = @generator.send(:token_from, bakn_account, token_bank_response)
assert_match(/tokenusbankacct_/, b_token)
end

def test_nil_token_from
credit_card = credit_card(number: 4111111111111111)
c_token = @generator.send(:token_from, credit_card, token_bank_response)
assert_nil c_token

bakn_account = check({ account_number: '4012000033330125', routing_number: '011000015' })
b_token = @generator.send(:token_from, bakn_account, token_credit_response)
assert_nil b_token
end

def assert_billing_address_mapping(request_input, payment_method)
assert_equal request_input['billingAddress']['streetAddress'], @options[:billing_address][:address1]
assert_equal request_input['billingAddress']['extendedAddress'], @options[:billing_address][:address2]

if payment_method.is_a?(Check)
assert_equal request_input['billingAddress']['city'], @options[:billing_address][:city]
assert_equal request_input['billingAddress']['state'], @options[:billing_address][:state]
assert_equal request_input['billingAddress']['zipCode'], @options[:billing_address][:zip]
else
assert_equal request_input['billingAddress']['locality'], @options[:billing_address][:city]
assert_equal request_input['billingAddress']['region'], @options[:billing_address][:state]
assert_equal request_input['billingAddress']['postalCode'], @options[:billing_address][:zip]
end
end

def assert_client_sdk_metadata(parse_response)
assert_equal parse_response['clientSdkMetadata']['platform'], 'web'
assert_equal parse_response['clientSdkMetadata']['source'], 'client'
assert_equal parse_response['clientSdkMetadata']['integration'], 'custom'
assert_match(/\A[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\z/i, parse_response['clientSdkMetadata']['sessionId'])
assert_equal parse_response['clientSdkMetadata']['version'], '3.83.0'
end

private

def normalize_graph(graph)
graph.gsub(/\s+/, ' ').strip
end

def bank_account_query
<<-GRAPHQL
mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
tokenizeUsBankAccount(input: $input) {
paymentMethod {
id
details {
... on UsBankAccountDetails {
last4
}
}
}
}
}
GRAPHQL
end

def credit_card_query
<<-GRAPHQL
mutation TokenizeCreditCard($input: TokenizeCreditCardInput!) {
tokenizeCreditCard(input: $input) {
paymentMethod {
id
details {
... on CreditCardDetails {
last4
}
}
}
}
}
GRAPHQL
end

def token_credit_response
{
'data' => {
'tokenizeCreditCard' => {
'paymentMethod' => {
'id' => 'tokencc_bc_72n3ms_74wsn3_jp2vn4_gjj62v_g33',
'details' => {
'last4' => '1111'
}
}
}
},
'extensions' => {
'requestId' => 'a093afbb-42a9-4a85-973f-0ca79dff9ba6'
}
}
end

def token_bank_response
{
'data' => {
'tokenizeUsBankAccount' => {
'paymentMethod' => {
'id' => 'tokenusbankacct_bc_zrg45z_7wz95v_nscrks_q4zpjs_5m7',
'details' => {
'last4' => '0125'
}
}
}
},
'extensions' => {
'requestId' => '769b26d5-27e4-4602-b51d-face8b6ffdd5'
}
}
end
end

0 comments on commit f207855

Please sign in to comment.