Skip to content

Commit

Permalink
StripePI: Adding network tokenization fields to Stripe PaymentIntents
Browse files Browse the repository at this point in the history
The `last4` field is the only new `option`, and is added upstream. This work includes fields for one-time use PaymentIntents only.

Test Summary
Local: 5582 tests, 77796 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
Unit at test/unit/gateways/stripe_payment_intents_test.rb: 55 tests, 289 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
Remote at test/remote/gateways/remote_stripe_payment_intents_test.rb: 90 tests, 424 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
  • Loading branch information
BritneyS committed Sep 12, 2023
1 parent 815dcbc commit 3b28d86
Show file tree
Hide file tree
Showing 4 changed files with 662 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Adyen: Add the store field [yunnydang] #4878
* Stripe Payment Intents: Expand balance txns for regular transactions [yunnydang] #4882
* CyberSource (SOAP): Added support for 3DS exemption request fields [BritneyS] #4881
* StripePI: Adding network tokenization fields to Stripe PaymentIntents [BritneyS] #4867

== Version 1.135.0 (August 24, 2023)
* PaymentExpress: Correct endpoints [steveh] #4827
Expand Down
49 changes: 45 additions & 4 deletions lib/active_merchant/billing/gateways/stripe_payment_intents.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class StripePaymentIntentsGateway < StripeGateway

def create_intent(money, payment_method, options = {})
MultiResponse.run do |r|
if payment_method.is_a?(NetworkTokenizationCreditCard)
if payment_method.is_a?(NetworkTokenizationCreditCard) && digital_wallet_payment_method?(payment_method)
r.process { tokenize_apple_google(payment_method, options) }
payment_method = (r.params['token']['id']) if r.success?
end
Expand All @@ -28,6 +28,7 @@ def create_intent(money, payment_method, options = {})
result = add_payment_method_token(post, payment_method, options)
return result if result.is_a?(ActiveMerchant::Billing::Response)

add_network_token_cryptogram_and_eci(post, payment_method)
add_external_three_d_secure_auth_data(post, options)
add_metadata(post, options)
add_return_url(post, options)
Expand Down Expand Up @@ -85,18 +86,18 @@ def add_payment_method_data(payment_method, options = {})
post = {
type: 'card',
card: {
number: payment_method.number,
exp_month: payment_method.month,
exp_year: payment_method.year
}
}

post[:card][:number] = payment_method.number unless adding_network_token_card_data?(payment_method)
post[:card][:cvc] = payment_method.verification_value if payment_method.verification_value
if billing = options[:billing_address] || options[:address]
post[:billing_details] = add_address(billing, options)
end

add_name_only(post, payment_method) if post[:billing_details].nil?
add_network_token_data(post, payment_method, options)
post
end

Expand Down Expand Up @@ -268,8 +269,22 @@ def setup_purchase(money, options = {})
commit(:post, 'payment_intents', post, options)
end

def supports_network_tokenization?
true
end

private

def digital_wallet_payment_method?(payment_method)
payment_method.source == :google_pay || payment_method.source == :apple_pay
end

def adding_network_token_card_data?(payment_method)
return true if payment_method.is_a?(ActiveMerchant::Billing::NetworkTokenizationCreditCard) && payment_method.source == :network_token

false
end

def off_session_request?(options = {})
(options[:off_session] || options[:setup_future_usage]) && options[:confirm] == true
end
Expand Down Expand Up @@ -343,10 +358,36 @@ def add_payment_method_token(post, payment_method, options, responses = [])
when ActiveMerchant::Billing::CreditCard
return create_payment_method_and_extract_token(post, payment_method, options, responses) if options[:verify]

get_payment_method_data_from_card(post, payment_method, options, responses)
when ActiveMerchant::Billing::NetworkTokenizationCreditCard
get_payment_method_data_from_card(post, payment_method, options, responses)
end
end

def add_network_token_data(post_data, payment_method, options)
return unless adding_network_token_card_data?(payment_method)

post_data[:card] ||= {}
post_data[:card][:last4] = options[:last_4]
post_data[:card][:network_token] = {}
post_data[:card][:network_token][:number] = payment_method.number
post_data[:card][:network_token][:exp_month] = payment_method.month
post_data[:card][:network_token][:exp_year] = payment_method.year
post_data[:card][:network_token][:payment_account_reference] = options[:payment_account_reference] if options[:payment_account_reference]

post_data
end

def add_network_token_cryptogram_and_eci(post, payment_method)
return unless adding_network_token_card_data?(payment_method)

post[:payment_method_options] ||= {}
post[:payment_method_options][:card] ||= {}
post[:payment_method_options][:card][:network_token] ||= {}
post[:payment_method_options][:card][:network_token][:cryptogram] = payment_method.payment_cryptogram if payment_method.payment_cryptogram
post[:payment_method_options][:card][:network_token][:electronic_commerce_indicator] = payment_method.eci if payment_method.eci
end

def extract_token_from_string_and_maybe_add_customer_id(post, payment_method)
if payment_method.include?('|')
customer_id, payment_method = payment_method.split('|')
Expand Down Expand Up @@ -385,7 +426,7 @@ def tokenize_apple_google(payment, options = {})
end

def get_payment_method_data_from_card(post, payment_method, options, responses)
return create_payment_method_and_extract_token(post, payment_method, options, responses) unless off_session_request?(options)
return create_payment_method_and_extract_token(post, payment_method, options, responses) unless off_session_request?(options) || adding_network_token_card_data?(payment_method)

post[:payment_method_data] = add_payment_method_data(payment_method, options)
end
Expand Down
41 changes: 41 additions & 0 deletions test/remote/gateways/remote_stripe_payment_intents_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ def setup
last_name: 'Longsen'
)

@network_token_credit_card = network_tokenization_credit_card(
'4000056655665556',
payment_cryptogram: 'AAEBAwQjSQAAXXXXXXXJYe0BbQA=',
source: :network_token,
brand: 'visa',
month: '09',
year: '2030',
first_name: 'Longbob',
last_name: 'Longsen'
)

@destination_account = fixtures(:stripe_destination)[:stripe_user_id]
end

Expand Down Expand Up @@ -212,6 +223,18 @@ def test_successful_purchase_with_google_pay
assert_match('google_pay', purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['wallet']['type'])
end

def test_successful_purchase_with_tokenized_visa
options = {
currency: 'USD',
last_4: '4242'
}

purchase = @gateway.purchase(@amount, @network_token_credit_card, options)
assert_equal(nil, purchase.responses.first.params.dig('token', 'card', 'tokenization_method'))
assert purchase.success?
assert_not_nil(purchase.params.dig('charges', 'data')[0]['payment_method_details']['card']['network_token'])
end

def test_successful_purchase_with_google_pay_when_sending_the_billing_address
options = {
currency: 'GBP',
Expand Down Expand Up @@ -1029,6 +1052,24 @@ def test_create_a_payment_intent_and_manually_capture
assert_equal 'Payment complete.', capture_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message')
end

def test_create_a_payment_intent_and_manually_capture_with_network_token
options = {
currency: 'GBP',
customer: @customer,
confirmation_method: 'manual',
capture_method: 'manual',
confirm: true,
last_4: '4242'
}
assert create_response = @gateway.create_intent(@amount, @network_token_credit_card, options)
intent_id = create_response.params['id']
assert_equal 'requires_capture', create_response.params['status']

assert capture_response = @gateway.capture(@amount, intent_id, options)
assert_equal 'succeeded', capture_response.params['status']
assert_equal 'Payment complete.', capture_response.params.dig('charges', 'data')[0].dig('outcome', 'seller_message')
end

def test_failed_create_a_payment_intent_with_set_error_on_requires_action
options = {
currency: 'GBP',
Expand Down
Loading

0 comments on commit 3b28d86

Please sign in to comment.