Skip to content

Commit

Permalink
Merge pull request #1659 from Shopify/stripe_icc_data
Browse files Browse the repository at this point in the history
[Stripe] Add EMV "chip & sign", "chip & offline PIN" and Maestro support
  • Loading branch information
bizla committed Apr 22, 2015
2 parents 74cfc7b + 0f71a22 commit 456cf85
Show file tree
Hide file tree
Showing 9 changed files with 387 additions and 28 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -39,6 +39,7 @@
* Fat Zebra: Add multi-currency support [nagash]
* Fat Zebra: Add auth/capture capability [nagash]
* Fat Zebra: Add soft descriptor support [nagash]
* Stripe: Add EMV "chip & sign", "chip & offline PIN" and Maestro support [bizla]

== Version 1.47.0 (February 25, 2015)

Expand Down
15 changes: 15 additions & 0 deletions lib/active_merchant/billing/credit_card.rb
Expand Up @@ -138,6 +138,17 @@ def brand=(value)
# @return [String]
attr_accessor :track_data

# Returns or sets the ICC/ASN1 credit card data for a EMV transaction, typically this is a BER-encoded TLV string.
#
# @return [String]
attr_accessor :icc_data

# Returns or sets a fallback reason for a EMV transaction whereby the customer's card entered a fallback scenario.
# This can be an arbitrary string.
#
# @return [String]
attr_accessor :fallback_reason

def type
ActiveMerchant.deprecated "CreditCard#type is deprecated and will be removed from a future release of ActiveMerchant. Please use CreditCard#brand instead."
brand
Expand Down Expand Up @@ -253,6 +264,10 @@ def self.requires_name?
require_name
end

def emv?
icc_data.present?
end

private

def validate_essential_attributes #:nodoc:
Expand Down
6 changes: 3 additions & 3 deletions lib/active_merchant/billing/credit_card_methods.rb
Expand Up @@ -3,7 +3,7 @@ module Billing #:nodoc:
# Convenience methods that can be included into a custom Credit Card object, such as an ActiveRecord based Credit Card object.
module CreditCardMethods
CARD_COMPANIES = {
'visa' => /^4\d{12}(\d{3})?$/,
'visa' => /^4\d{12}(\d{3})?(\d{3})?$/,
'master' => /^(5[1-5]\d{4}|677189)\d{10}$/,
'discover' => /^(6011|65\d{2}|64[4-9]\d)\d{12}|(62\d{14})$/,
'american_express' => /^3[47]\d{13}$/,
Expand Down Expand Up @@ -49,11 +49,11 @@ def valid_start_year?(year)
def valid_card_verification_value?(cvv, brand)
cvv.to_s =~ /^\d{#{card_verification_value_length(brand)}}$/
end

def card_verification_value_length(brand)
brand == 'american_express' ? 4 : 3
end

def valid_issue_number?(number)
(number.to_s =~ /^\d{1,2}$/)
end
Expand Down
59 changes: 41 additions & 18 deletions lib/active_merchant/billing/gateways/stripe.rb
Expand Up @@ -27,7 +27,7 @@ class StripeGateway < Gateway
self.supported_countries = %w(AT AU BE CA CH DE DK ES FI FR GB IE IT LU NL NO SE US)
self.default_currency = 'USD'
self.money_format = :cents
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club, :maestro]

self.homepage_url = 'https://stripe.com/'
self.display_name = 'Stripe'
Expand Down Expand Up @@ -62,7 +62,7 @@ def authorize(money, payment, options = {})
end
r.process do
post = create_post_for_auth_or_purchase(money, payment, options)
post[:capture] = "false"
post[:capture] = "false" unless emv_payment?(payment)
commit(:post, 'charges', post, options)
end
end.responses.last
Expand Down Expand Up @@ -90,10 +90,16 @@ def purchase(money, payment, options = {})

def capture(money, authorization, options = {})
post = {}

add_amount(post, money, options)
add_application_fee(post, options)

commit(:post, "charges/#{CGI.escape(authorization)}/capture", post, options)
if emv_tc_response = options.delete(:icc_data)
post[:card] = { emv_approval_data: emv_tc_response }
commit(:post, "charges/#{CGI.escape(authorization)}", post, options)
else
commit(:post, "charges/#{CGI.escape(authorization)}/capture", post, options)
end
end

def void(identification, options = {})
Expand Down Expand Up @@ -226,23 +232,22 @@ def type

def create_post_for_auth_or_purchase(money, payment, options)
post = {}
add_amount(post, money, options, true)

if payment.is_a?(StripePaymentToken)
add_payment_token(post, payment, options)
else
add_creditcard(post, payment, options)
end
add_customer(post, payment, options)
add_customer_data(post, options)
post[:description] = options[:description]
post[:statement_description] = options[:statement_description]

post[:metadata] = options[:metadata] || {}
post[:metadata][:email] = options[:email] if options[:email]
post[:metadata][:order_id] = options[:order_id] if options[:order_id]
post.delete(:metadata) if post[:metadata].empty?
unless emv_payment?(payment)
add_amount(post, money, options, true)
add_customer_data(post, options)
add_metadata(post, options)
post[:description] = options[:description]
post[:statement_description] = options[:statement_description]
add_customer(post, payment, options)
add_flags(post, options)
end

add_flags(post, options)
add_application_fee(post, options)
post
end
Expand Down Expand Up @@ -283,17 +288,19 @@ def add_address(post, options)

def add_creditcard(post, creditcard, options)
card = {}
if creditcard.respond_to?(:number)
if emv_payment?(creditcard)
add_emv_creditcard(post, creditcard.icc_data)
elsif creditcard.respond_to?(:number)
if creditcard.respond_to?(:track_data) && creditcard.track_data.present?
card[:swipe_data] = creditcard.track_data
post[:card] = { fallback_reason: creditcard.fallback_reason } if creditcard.fallback_reason
else
card[:number] = creditcard.number
card[:exp_month] = creditcard.month
card[:exp_year] = creditcard.year
card[:cvc] = creditcard.verification_value if creditcard.verification_value?
card[:name] = creditcard.name if creditcard.name
end

post[:card] = card

if creditcard.is_a?(NetworkTokenizationCreditCard)
Expand All @@ -314,6 +321,10 @@ def add_creditcard(post, creditcard, options)
end
end

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
Expand All @@ -327,6 +338,13 @@ def add_flags(post, options)
post[:recurring] = true if (options[:eci] == 'recurring' || options[:recurring])
end

def add_metadata(post, options = {})
post[:metadata] = options[:metadata] || {}
post[:metadata][:email] = options[:email] if options[:email]
post[:metadata][:order_id] = options[:order_id] if options[:order_id]
post.delete(:metadata) if post[:metadata].empty?
end

def fetch_application_fees(identification, options = {})
options.merge!(:key => @fee_refund_api_key)

Expand Down Expand Up @@ -386,11 +404,11 @@ def api_request(method, endpoint, parameters = nil, options = {})

def commit(method, url, parameters = nil, options = {})
add_expand_parameters(parameters, options) if parameters

response = api_request(method, url, parameters, options)

success = !response.key?("error")

card = response["card"] || response["active_card"] || {}
card = response["card"] || response["active_card"] || response["source"] || {}
avs_code = AVS_CODE_TRANSLATOR["line1: #{card["address_line1_check"]}, zip: #{card["address_zip_check"]}"]
cvc_code = CVC_CODE_TRANSLATOR[card["cvc_check"]]

Expand All @@ -401,6 +419,7 @@ def commit(method, url, parameters = nil, options = {})
:authorization => success ? response["id"] : response["error"]["charge"],
:avs_result => { :code => avs_code },
:cvv_result => cvc_code,
:emv_authorization => card["emv_auth_data"],
:error_code => success ? nil : STANDARD_ERROR_CODE_MAPPING[response["error"]["code"]]
)
end
Expand All @@ -426,6 +445,10 @@ def json_error(raw_response)
def non_fractional_currency?(currency)
CURRENCIES_WITHOUT_FRACTIONS.include?(currency.to_s)
end

def emv_payment?(payment)
payment.respond_to?(:emv?) && payment.emv?
end
end
end
end
5 changes: 3 additions & 2 deletions lib/active_merchant/billing/response.rb
Expand Up @@ -4,7 +4,7 @@ class Error < ActiveMerchantError #:nodoc:
end

class Response
attr_reader :params, :message, :test, :authorization, :avs_result, :cvv_result, :error_code
attr_reader :params, :message, :test, :authorization, :avs_result, :cvv_result, :error_code, :emv_authorization

def success?
@success
Expand All @@ -24,6 +24,7 @@ def initialize(success, message, params = {}, options = {})
@authorization = options[:authorization]
@fraud_review = options[:fraud_review]
@error_code = options[:error_code]
@emv_authorization = options[:emv_authorization]

@avs_result = if options[:avs_result].kind_of?(AVSResult)
options[:avs_result].to_hash
Expand Down Expand Up @@ -79,7 +80,7 @@ def success?
(primary_response ? primary_response.success? : true)
end

%w(params message test authorization avs_result cvv_result error_code test? fraud_review?).each do |m|
%w(params message test authorization avs_result cvv_result error_code emv_authorization test? fraud_review?).each do |m|
class_eval %(
def #{m}
(@responses.empty? ? nil : primary_response.#{m})
Expand Down
10 changes: 10 additions & 0 deletions test/fixtures.yml
Expand Up @@ -799,6 +799,16 @@ stripe:
login:
fee_refund_login:

# Used only for remote tests that use EMV credit card mocks
stripe_emv_uk:
login:
fee_refund_login:

# Used only for remote tests that use EMV credit card mocks
stripe_emv_us:
login:
fee_refund_login:

# Working credentials, no need to replace
swipe_checkout:
login: 2077103073D8B5
Expand Down
83 changes: 78 additions & 5 deletions test/remote/gateways/remote_stripe_test.rb
Expand Up @@ -12,6 +12,11 @@ def setup
@declined_card = credit_card('4000000000000002')
@new_credit_card = credit_card('5105105105105100')

@emv_credit_cards = {
uk: ActiveMerchant::Billing::CreditCard.new(icc_data: '500B56495341204352454449545F201A56495341204143515549524552205445535420434152442030315F24031512315F280208405F2A0208265F300202015F34010182025C008407A0000000031010950502000080009A031408259B02E8009C01009F02060000000734499F03060000000000009F0607A00000000310109F0902008C9F100706010A03A080009F120F4352454449544F20444520564953419F1A0208269F1C0831373030303437309F1E0831373030303437309F2608EB2EC0F472BEA0A49F2701809F3303E0B8C89F34031E03009F3501229F360200C39F37040A27296F9F4104000001319F4502DAC5DFAE5711476173FFFFFF0119D15122011758989389DFAE5A08476173FFFFFF011957114761739001010119D151220117589893895A084761739001010119'),
us: ActiveMerchant::Billing::CreditCard.new(icc_data: '50074D41455354524F571167999989000018123D25122200835506065A0967999989000018123F5F20134D54495032362D204D41455354524F203132415F24032512315F280200565F2A0208405F300202205F340101820278008407A0000000043060950500000080009A031504219B02E8009C01009F02060000000010009F03060000000000009F0607A00000000430609F090200029F10120210A7800F040000000000000000000000FF9F12074D61657374726F9F1A0208409F1C0831303030333331369F1E0831303030333331369F2608460245B808BCA1369F2701809F3303E0B8C89F34034403029F3501229F360200279F3704EA2C3A7A9F410400000094DF280104DFAE5711679999FFFFFFF8123D2512220083550606DFAE5A09679999FFFFFFF8123F')
}

@options = {
:currency => @currency,
:description => 'ActiveMerchant Test Purchase',
Expand Down Expand Up @@ -43,6 +48,28 @@ def test_successful_purchase
assert response.params["paid"]
assert_equal "ActiveMerchant Test Purchase", response.params["description"]
assert_equal "wow@example.com", response.params["metadata"]["email"]
end

# for EMV contact transactions, it's advised to do a separate auth + capture
# to satisfy the EMV chip's transaction flow, but this works as a legal
# API call. You shouldn't use it in a real EMV implementation, though.
def test_successful_purchase_with_emv_credit_card_in_uk
@gateway = StripeGateway.new(fixtures(:stripe_emv_uk))
assert response = @gateway.purchase(@amount, @emv_credit_cards[:uk], @options)
assert_success response
assert_equal "charge", response.params["object"]
assert response.params["paid"]
assert_match CHARGE_ID_REGEX, response.authorization
end

# perform separate auth & capture rather than a purchase in practice for the
# reasons mentioned above.
def test_successful_purchase_with_emv_credit_card_in_us
@gateway = StripeGateway.new(fixtures(:stripe_emv_us))
assert response = @gateway.purchase(@amount, @emv_credit_cards[:us], @options)
assert_success response
assert_equal "charge", response.params["object"]
assert response.params["paid"]
assert_match CHARGE_ID_REGEX, response.authorization
end

Expand Down Expand Up @@ -77,18 +104,42 @@ def test_unsuccessful_purchase
def test_authorization_and_capture
assert authorization = @gateway.authorize(@amount, @credit_card, @options)
assert_success authorization
assert !authorization.params["captured"]
refute authorization.params["captured"]
assert_equal "ActiveMerchant Test Purchase", authorization.params["description"]
assert_equal "wow@example.com", authorization.params["metadata"]["email"]

assert capture = @gateway.capture(@amount, authorization.authorization)
assert_success capture
end

def test_authorization_and_capture_with_emv_credit_card_in_uk
@gateway = StripeGateway.new(fixtures(:stripe_emv_uk))
assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:uk], @options)
assert_success authorization
assert authorization.emv_authorization, "Authorization should contain emv_authorization containing the EMV ARPC"
refute authorization.params["captured"]

assert capture = @gateway.capture(@amount, authorization.authorization)
assert_success capture
assert capture.emv_authorization, "Capture should contain emv_authorization containing the EMV TC"
end

def test_authorization_and_capture_with_emv_credit_card_in_us
@gateway = StripeGateway.new(fixtures(:stripe_emv_us))
assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:us], @options)
assert_success authorization
assert authorization.emv_authorization, "Authorization should contain emv_authorization containing the EMV ARPC"
refute authorization.params["captured"]

assert capture = @gateway.capture(@amount, authorization.authorization)
assert_success capture
assert capture.emv_authorization, "Capture should contain emv_authorization containing the EMV TC"
end

def test_authorization_and_capture_with_apple_pay_payment_token
assert authorization = @gateway.authorize(@amount, @apple_pay_payment_token, @options)
assert_success authorization
assert !authorization.params["captured"]
refute authorization.params["captured"]
assert_equal "ActiveMerchant Test Purchase", authorization.params["description"]
assert_equal "wow@example.com", authorization.params["metadata"]["email"]

Expand All @@ -99,7 +150,29 @@ def test_authorization_and_capture_with_apple_pay_payment_token
def test_authorization_and_void
assert authorization = @gateway.authorize(@amount, @credit_card, @options)
assert_success authorization
assert !authorization.params["captured"]
refute authorization.params["captured"]

assert void = @gateway.void(authorization.authorization)
assert_success void
end

def test_authorization_and_void_with_emv_credit_card_in_us
@gateway = StripeGateway.new(fixtures(:stripe_emv_us))
assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:us], @options)
assert_success authorization
assert authorization.emv_authorization, "Authorization should contain emv_authorization containing the EMV ARPC"
refute authorization.params["captured"]

assert void = @gateway.void(authorization.authorization)
assert_success void
end

def test_authorization_and_void_with_emv_credit_card_in_uk
@gateway = StripeGateway.new(fixtures(:stripe_emv_uk))
assert authorization = @gateway.authorize(@amount, @emv_credit_cards[:uk], @options)
assert_success authorization
assert authorization.emv_authorization, "Authorization should contain emv_authorization containing the EMV ARPC"
refute authorization.params["captured"]

assert void = @gateway.void(authorization.authorization)
assert_success void
Expand All @@ -108,7 +181,7 @@ def test_authorization_and_void
def test_authorization_and_void_with_apple_pay_payment_token
assert authorization = @gateway.authorize(@amount, @apple_pay_payment_token, @options)
assert_success authorization
assert !authorization.params["captured"]
refute authorization.params["captured"]

assert void = @gateway.void(authorization.authorization)
assert_success void
Expand Down Expand Up @@ -294,7 +367,7 @@ def test_card_present_authorize_and_capture
@credit_card.track_data = '%B378282246310005^LONGSON/LONGBOB^1705101130504392?'
assert authorization = @gateway.authorize(@amount, @credit_card, @options)
assert_success authorization
assert !authorization.params["captured"]
refute authorization.params["captured"]

assert capture = @gateway.capture(@amount, authorization.authorization)
assert_success capture
Expand Down

0 comments on commit 456cf85

Please sign in to comment.