Skip to content
Open
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
186 changes: 105 additions & 81 deletions lib/active_merchant/billing/gateways/stripe.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
require 'active_support/core_ext/hash/slice'

module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module ActiveMerchant # :nodoc:
module Billing # :nodoc:
# This gateway uses an older version of the Stripe API.
# To utilize the updated {Payment Intents API}[https://stripe.com/docs/api/payment_intents], integrate with the StripePaymentIntents gateway
class StripeGateway < Gateway
# version '2020-08-27'

self.live_url = 'https://api.stripe.com/v1/'

# Docs on AVS codes: https://en.wikipedia.org/w/index.php?title=Address_verification_service&_ga=2.97570079.1027215965.1655989706-2008268124.1655989706#AVS_response_codes
# possible response values: https://stripe.com/docs/api/payment_methods/object#payment_method_object-card-checks
AVS_CODE_TRANSLATOR = {
'line1: pass, zip: pass' => 'Y',
'line1: pass, zip: fail' => 'A',
'line1: pass, zip: unchecked' => 'B',
'line1: fail, zip: pass' => 'Z',
'line1: unchecked, zip: unchecked' => 'I',
'line1: fail, zip: fail' => 'N',
'line1: unchecked, zip: pass' => 'P',
'line1: unchecked, zip: unchecked' => 'I'
'line1: pass, zip: pass' => 'Y',
'line1: fail, zip: pass' => 'Z'
}

CVC_CODE_TRANSLATOR = {
Expand Down Expand Up @@ -86,18 +90,10 @@ def authorize(money, payment, options = {})
return Response.new(false, direct_bank_error)
end

MultiResponse.run do |r|
if payment.is_a?(ApplePayPaymentToken)
r.process { tokenize_apple_pay_token(payment) }
payment = StripePaymentToken.new(r.params['token']) if r.success?
end
r.process do
post = create_post_for_auth_or_purchase(money, payment, options)
add_application_fee(post, options) if emv_payment?(payment)
post[:capture] = 'false'
commit(:post, 'charges', post, options)
end
end.responses.last
post = create_post_for_auth_or_purchase(money, payment, options)
add_application_fee(post, options) if emv_payment?(payment)
post[:capture] = 'false'
commit(:post, 'charges', post, options)
end

# To create a charge on a card or a token, call
Expand All @@ -113,17 +109,9 @@ def purchase(money, payment, options = {})
return Response.new(false, direct_bank_error)
end

MultiResponse.run do |r|
if payment.is_a?(ApplePayPaymentToken)
r.process { tokenize_apple_pay_token(payment) }
payment = StripePaymentToken.new(r.params['token']) if r.success?
end
r.process do
post = create_post_for_auth_or_purchase(money, payment, options)
post[:card][:processing_method] = 'quick_chip' if quickchip_payment?(payment)
commit(:post, 'charges', post, options)
end
end.responses.last
post = create_post_for_auth_or_purchase(money, payment, options)
post[:card][:processing_method] = 'quick_chip' if quickchip_payment?(payment)
commit(:post, 'charges', post, options)
end

def capture(money, authorization, options = {})
Expand All @@ -145,7 +133,8 @@ def capture(money, authorization, options = {})

def void(identification, options = {})
post = {}
post[:metadata] = options[:metadata] if options[:metadata]
post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer]
add_metadata(post, options)
post[:reason] = options[:reason] if options[:reason]
post[:expand] = [:charge]
commit(:post, "charges/#{CGI.escape(identification)}/refunds", post, options)
Expand All @@ -156,7 +145,7 @@ def refund(money, identification, options = {})
add_amount(post, money, options)
post[:refund_application_fee] = true if options[:refund_application_fee]
post[:reverse_transfer] = options[:reverse_transfer] if options[:reverse_transfer]
post[:metadata] = options[:metadata] if options[:metadata]
add_metadata(post, options)
post[:reason] = options[:reason] if options[:reason]
post[:expand] = [:charge]

Expand Down Expand Up @@ -196,12 +185,7 @@ def store(payment, options = {})
params = {}
post = {}

if payment.is_a?(ApplePayPaymentToken)
token_exchange_response = tokenize_apple_pay_token(payment)
params = { card: token_exchange_response.params['token']['id'] } if token_exchange_response.success?
elsif payment.is_a?(StripePaymentToken)
add_payment_token(params, payment, options)
elsif payment.is_a?(Check)
if payment.is_a?(Check)
bank_token_response = tokenize_bank_account(payment)
return bank_token_response unless bank_token_response.success?

Expand Down Expand Up @@ -252,17 +236,6 @@ def unstore(identification, options = {}, deprecated_options = {})
commit(:delete, "customers/#{CGI.escape(customer_id)}/cards/#{CGI.escape(card_id)}", nil, options)
end

def tokenize_apple_pay_token(apple_pay_payment_token, options = {})
token_response = api_request(:post, "tokens?pk_token=#{CGI.escape(apple_pay_payment_token.payment_data.to_json)}")
success = !token_response.key?('error')

if success && token_response.key?('id')
Response.new(success, nil, token: token_response)
else
Response.new(success, token_response['error']['message'])
end
end

def verify_credentials
begin
ssl_get(live_url + 'charges/nonexistent', headers)
Expand All @@ -280,6 +253,7 @@ def supports_scrubbing?
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]').
gsub(%r((&?three_d_secure\[cryptogram\]=)[\w=]*(&?)), '\1[FILTERED]\2').
gsub(%r(((\[card\]|card)\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]\3').
gsub(%r(((\[card\]|card)\[cvc\]=)\d+), '\1[FILTERED]').
Expand All @@ -290,7 +264,9 @@ def scrub(transcript)
gsub(%r(((\[card\]|card)\[number\]=)\d+), '\1[FILTERED]').
gsub(%r(((\[card\]|card)\[swipe_data\]=)[^&]+(&?)), '\1[FILTERED]\3').
gsub(%r(((\[bank_account\]|bank_account)\[account_number\]=)\d+), '\1[FILTERED]').
gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[token\]=)[^&]+(&?)), '\1[FILTERED]\3')
gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[token\]=)[^&]+(&?)), '\1[FILTERED]\3').
gsub(%r(((\[payment_method_data\]|payment_method_data)\[card\]\[network_token\]\[number\]=)\d+), '\1[FILTERED]').
gsub(%r(((\[payment_method_options\]|payment_method_options)\[card\]\[network_token\]\[cryptogram\]=)[^&]+(&?)), '\1[FILTERED]')
end

def supports_network_tokenization?
Expand All @@ -301,7 +277,7 @@ def supports_network_tokenization?
def delete_latest_test_external_account(account)
return unless test?

auth_header = { 'Authorization': "Bearer #{options[:login]}" }
auth_header = { 'Authorization' => 'Basic ' + Base64.strict_encode64(options[:login].to_s + ':').strip }
url = "#{live_url}accounts/#{CGI.escape(account)}/external_accounts"
accounts_response = JSON.parse(ssl_get("#{url}?limit=100", auth_header))
to_delete = accounts_response['data'].reject { |ac| ac['default_for_currency'] }
Expand Down Expand Up @@ -362,12 +338,7 @@ def list_webhook_endpoints(options)
def create_post_for_auth_or_purchase(money, payment, options)
post = {}

if payment.is_a?(StripePaymentToken)
add_payment_token(post, payment, options)
else
add_creditcard(post, payment, options)
end

add_creditcard(post, payment, options)
add_charge_details(post, money, payment, options)
post
end
Expand Down Expand Up @@ -434,7 +405,7 @@ def add_level_three(post, options)
post[:level3] = level_three unless level_three.empty?
end

def add_expand_parameters(post, options)
def add_expand_parameters(post, options, method, url)
post[:expand] ||= []
post[:expand].concat(Array.wrap(options[:expand]).map(&:to_sym)).uniq!
end
Expand Down Expand Up @@ -506,9 +477,10 @@ def add_creditcard(post, creditcard, options, use_sources = false)
end

if creditcard.is_a?(NetworkTokenizationCreditCard)
tokenization_method = creditcard.source == :google_pay ? :android_pay : creditcard.source
card[:cryptogram] = creditcard.payment_cryptogram
card[:eci] = creditcard.eci.rjust(2, '0') if creditcard.eci =~ /^[0-9]+$/
card[:tokenization_method] = creditcard.source.to_s
card[:tokenization_method] = tokenization_method.to_s
end
post[:card] = card

Expand All @@ -531,10 +503,6 @@ def add_emv_creditcard(post, icc_data, options = {})
post[:card] = { emv_auth_data: icc_data }
end

def add_payment_token(post, token, options = {})
post[:card] = token.payment_data['id']
end

def add_customer(post, payment, options)
post[:customer] = options[:customer] if options[:customer] && !payment.respond_to?(:number)
end
Expand Down Expand Up @@ -575,7 +543,7 @@ def add_shipping_address(post, payment, options = {})

def add_source_owner(post, creditcard, options)
post[:owner] = {}
post[:owner][:name] = creditcard.name if creditcard.name
post[:owner][:name] = creditcard.name if creditcard.respond_to?(:name) && creditcard.name
post[:owner][:email] = options[:email] if options[:email]

if address = options[:billing_address] || options[:address]
Expand Down Expand Up @@ -611,8 +579,32 @@ def add_radar_data(post, options = {})
post[:radar_options] = radar_options unless radar_options.empty?
end

def add_header_fields(response)
return unless @response_headers.present?

headers = {}
headers['response_headers'] = {}
headers['response_headers']['idempotent_replayed'] = @response_headers['idempotent-replayed'] if @response_headers['idempotent-replayed']
headers['response_headers']['stripe_should_retry'] = @response_headers['stripe-should-retry'] if @response_headers['stripe-should-retry']

response.merge!(headers)
end

def add_card_response_field(response)
return if @card_3d_supported.nil?

card_details = {}
card_details['three_d_secure_usage_supported'] = @card_3d_supported

response.merge!(card_details)
end

def parse(body)
JSON.parse(body)
response = JSON.parse(body)
add_header_fields(response)
add_card_response_field(response)

response
end

def post_data(params)
Expand Down Expand Up @@ -650,18 +642,19 @@ def flatten_array(flattened, array, prefix)
end
end

def headers(options = {})
key = options[:key] || @api_key
idempotency_key = options[:idempotency_key]
def key(options = {})
options[:key] || @api_key
end

def headers(method = :post, options = {})
headers = {
'Authorization' => 'Basic ' + Base64.strict_encode64(key.to_s + ':').strip,
'Authorization' => 'Basic ' + Base64.strict_encode64(key(options).to_s + ':').strip,
'User-Agent' => "Stripe/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}",
'Stripe-Version' => api_version(options),
'X-Stripe-Client-User-Agent' => stripe_client_user_agent(options),
'X-Stripe-Client-User-Metadata' => { ip: options[:ip] }.to_json
}
headers['Idempotency-Key'] = idempotency_key if idempotency_key
headers['Idempotency-Key'] = options[:idempotency_key] if options[:idempotency_key] && method != :get
headers['Stripe-Account'] = options[:stripe_account] if options[:stripe_account]
headers
end
Expand All @@ -679,7 +672,7 @@ def api_version(options)
def api_request(method, endpoint, parameters = nil, options = {})
raw_response = response = nil
begin
raw_response = ssl_request(method, self.live_url + endpoint, post_data(parameters), headers(options))
raw_response = ssl_request(method, self.live_url + endpoint, post_data(parameters), headers(method, options))
response = parse(raw_response)
rescue ResponseError => e
raw_response = e.response.body
Expand All @@ -691,37 +684,56 @@ def api_request(method, endpoint, parameters = nil, options = {})
end

def commit(method, url, parameters = nil, options = {})
add_expand_parameters(parameters, options) if parameters
add_expand_parameters(parameters, options, method, url) if parameters
return Response.new(false, 'Invalid API Key provided') unless key_valid?(options)

response = api_request(method, url, parameters, options)
response['webhook_id'] = options[:webhook_id] if options[:webhook_id]
success = success_from(response, options)

card = card_from_response(response)
avs_code = AVS_CODE_TRANSLATOR["line1: #{card['address_line1_check']}, zip: #{card['address_zip_check']}"]
cvc_code = CVC_CODE_TRANSLATOR[card['cvc_check']]
Response.new(success,
card_checks = card_from_response(response)
avs_code = AVS_CODE_TRANSLATOR["line1: #{card_checks['address_line1_check']}, zip: #{card_checks['address_zip_check'] || card_checks['address_postal_code_check']}"]
cvc_code = CVC_CODE_TRANSLATOR[card_checks['cvc_check']]
Response.new(
success,
message_from(success, response),
response,
test: response_is_test?(response),
authorization: authorization_from(success, url, method, response),
authorization: authorization_from(success, url, method, response, options),
avs_result: { code: avs_code },
cvv_result: cvc_code,
emv_authorization: emv_authorization_from_response(response),
error_code: success ? nil : error_code_from(response))
error_code: success ? nil : error_code_from(response)
)
end

def authorization_from(success, url, method, response)
return response.fetch('error', {})['charge'] unless success
def key_valid?(options)
return true unless test?

%w(sk rk).each do |k|
return false if key(options).start_with?(k) && !key(options).start_with?("#{k}_test")
end

true
end

def authorization_from(success, url, method, response, options)
return error_id(response, url) unless success

if url == 'customers'
[response['id'], response.dig('sources', 'data').first&.dig('id')].join('|')
elsif method == :post && (url.match(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/))
[response['customer'], response['id']].join('|')
elsif method == :post && (url.match(/customers\/.*\/cards/) || url.match(/payment_methods\/.*\/attach/) || options[:action] == :store)
response_id = options[:action] == :store ? response['payment_method'] : response['id']
[response['customer'], response_id].join('|')
else
response['id']
end
end

def error_id(response, url)
response.dig('error', 'charge') || response.dig('error', 'setup_intent', 'id') || response['id']
end

def message_from(success, response)
success ? 'Transaction approved' : response.fetch('error', { 'message' => 'No error details' })['message']
end
Expand All @@ -730,6 +742,18 @@ def success_from(response, options)
!response.key?('error') && response['status'] != 'failed'
end

# Override the regular handle response so we can access the headers
# set header fields and values so we can add them to the response body
def handle_response(response)
@response_headers = response.each_header.to_h if response.respond_to?(:header)
case response.code.to_i
when 200...300
response.body
else
raise ResponseError.new(response)
end
end

def response_error(raw_response)
parse(raw_response)
rescue JSON::ParserError
Expand Down Expand Up @@ -842,4 +866,4 @@ def copy_when_present(dest, dest_path, source, source_path = nil)
end
end
end
end
end
Loading