lib/active_merchant/billing/gateways/card_stream.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardStreamGateway < Gateway
THREEDSECURE_REQUIRED_DEPRECATION_MESSAGE = 'Specifying the :threeDSRequired initialization option is deprecated. Please use the `:threeds_required => true` *transaction* option instead.'
self.test_url = self.live_url = 'https://gateway.cardstream.com/direct/'
self.money_format = :cents
self.default_currency = 'GBP'
self.currencies_without_fractions = %w(CVE ISK JPY UGX)
self.supported_countries = %w[GB US CH SE SG NO JP IS HK NL CZ CA AU]
self.supported_cardtypes = %i[visa master american_express diners_club discover jcb maestro]
self.homepage_url = 'http://www.cardstream.com/'
self.display_name = 'CardStream'
CURRENCY_CODES = {
'AED' => '784',
'ALL' => '008',
'AMD' => '051',
'ANG' => '532',
'ARS' => '032',
'AUD' => '036',
'AWG' => '533',
'BAM' => '977',
'BBD' => '052',
'BGN' => '975',
'BMD' => '060',
'BOB' => '068',
'BRL' => '986',
'BSD' => '044',
'BWP' => '072',
'BZD' => '084',
'CAD' => '124',
'CHF' => '756',
'CLP' => '152',
'CNY' => '156',
'COP' => '170',
'CRC' => '188',
'CZK' => '203',
'DKK' => '208',
'DOP' => '214',
'EGP' => '818',
'EUR' => '978',
'GBP' => '826',
'GEL' => '981',
'GIP' => '292',
'GTQ' => '320',
'GYD' => '328',
'HKD' => '344',
'HNL' => '340',
'HRK' => '191',
'HUF' => '348',
'ISK' => '352',
'IDR' => '360',
'ILS' => '376',
'INR' => '356',
'JPY' => '392',
'JMD' => '388',
'KES' => '404',
'KRW' => '410',
'KYD' => '136',
'LBP' => '422',
'LKR' => '144',
'MAD' => '504',
'MVR' => '462',
'MWK' => '454',
'MXN' => '484',
'MYR' => '458',
'NAD' => '516',
'NGN' => '566',
'NIO' => '558',
'NOK' => '578',
'NPR' => '524',
'NZD' => '554',
'PAB' => '590',
'PEN' => '604',
'PGK' => '598',
'PHP' => '608',
'PKR' => '586',
'PLN' => '985',
'PYG' => '600',
'QAR' => '634',
'RON' => '946',
'RSD' => '941',
'RUB' => '643',
'RWF' => '646',
'SAR' => '682',
'SEK' => '752',
'SGD' => '702',
'SRD' => '968',
'THB' => '764',
'TND' => '788',
'TRY' => '949',
'TTD' => '780',
'TWD' => '901',
'TZS' => '834',
'UAH' => '980',
'UGX' => '800',
'USD' => '840',
'UYU' => '858',
'VND' => '704',
'WST' => '882',
'XAF' => '950',
'XCD' => '951',
'XOF' => '952',
'ZAR' => '710'
}
CVV_CODE = {
'0' => 'U',
'1' => 'P',
'2' => 'M',
'4' => 'N'
}
# 0 - No additional information available.
# 1 - Postcode not checked.
# 2 - Postcode matched.
# 4 - Postcode not matched.
# 8 - Postcode partially matched.
AVS_POSTAL_MATCH = {
'0' => nil,
'1' => nil,
'2' => 'Y',
'4' => 'N',
'8' => 'N'
}
# 0 - No additional information available.
# 1 - Address numeric not checked.
# 2 - Address numeric matched.
# 4 - Address numeric not matched.
# 8 - Address numeric partially matched.
AVS_STREET_MATCH = {
'0' => nil,
'1' => nil,
'2' => 'Y',
'4' => 'N',
'8' => 'N'
}
def initialize(options = {})
requires!(options, :login, :shared_secret)
@threeds_required = false
if options[:threeDSRequired]
ActiveMerchant.deprecated(THREEDSECURE_REQUIRED_DEPRECATION_MESSAGE)
@threeds_required = options[:threeDSRequired]
end
super
end
def authorize(money, credit_card_or_reference, options = {})
post = {}
add_auth_purchase(post, -1, money, credit_card_or_reference, options)
commit('SALE', post)
end
def purchase(money, credit_card_or_reference, options = {})
post = {}
add_auth_purchase(post, 0, money, credit_card_or_reference, options)
commit('SALE', post)
end
def capture(money, authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
add_pair(post, :amount, localized_amount(money, options[:currency] || currency(money)), required: true)
add_remote_address(post, options)
commit('CAPTURE', post)
end
def refund(money, authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
add_amount(post, money, options)
add_remote_address(post, options)
add_country_code(post, options)
response = commit('REFUND_SALE', post)
return response if response.success?
return response unless options[:force_full_refund_if_unsettled]
if response.params['responseCode'] == '65541'
void(authorization, options)
else
response
end
end
def void(authorization, options = {})
post = {}
add_pair(post, :xref, authorization)
add_remote_address(post, options)
commit('CANCEL', post)
end
def verify(creditcard, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, creditcard, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((cardNumber=)\d+), '\1[FILTERED]').
gsub(%r((CVV=)\d+), '\1[FILTERED]')
end
private
def add_auth_purchase(post, pair_value, money, credit_card_or_reference, options)
add_pair(post, :captureDelay, pair_value)
add_amount(post, money, options)
add_invoice(post, credit_card_or_reference, money, options)
add_credit_card_or_reference(post, credit_card_or_reference)
add_customer_data(post, options)
add_remote_address(post, options)
add_country_code(post, options)
add_threeds_fields(post, options)
end
def add_amount(post, money, options)
currency = options[:currency] || currency(money)
add_pair(post, :amount, localized_amount(money, currency), required: true)
add_pair(post, :currencyCode, currency_code(currency))
end
def add_customer_data(post, options)
add_pair(post, :customerEmail, options[:email])
if (address = options[:billing_address] || options[:address])
add_pair(post, :customerAddress, "#{address[:address1]} #{address[:address2]}".strip)
add_pair(post, :customerPostCode, address[:zip])
add_pair(post, :customerPhone, options[:phone])
add_pair(post, :customerCountryCode, address[:country] || 'GB')
else
add_pair(post, :customerCountryCode, 'GB')
end
end
def add_invoice(post, credit_card_or_reference, money, options)
add_pair(post, :transactionUnique, options[:order_id], required: true)
add_pair(post, :orderRef, options[:description] || options[:order_id], required: true)
add_pair(post, :statementNarrative1, options[:merchant_name]) if options[:merchant_name]
add_pair(post, :statementNarrative2, options[:dynamic_descriptor]) if options[:dynamic_descriptor]
if credit_card_or_reference.respond_to?(:number) && %w[american_express diners_club].include?(card_brand(credit_card_or_reference).to_s)
add_pair(post, :item1Quantity, 1)
add_pair(post, :item1Description, (options[:description] || options[:order_id]).slice(0, 15))
add_pair(post, :item1GrossValue, localized_amount(money, options[:currency] || currency(money)))
end
add_pair(post, :type, options[:type] || '1')
add_threeds_required(post, options)
end
def add_credit_card_or_reference(post, credit_card_or_reference)
if credit_card_or_reference.respond_to?(:number)
add_credit_card(post, credit_card_or_reference)
else
add_reference(post, credit_card_or_reference.to_s)
end
end
def add_reference(post, reference)
add_pair(post, :xref, reference, required: true)
end
def add_credit_card(post, credit_card)
add_pair(post, :customerName, credit_card.name, required: true)
add_pair(post, :cardNumber, credit_card.number, required: true)
add_pair(post, :cardExpiryMonth, format(credit_card.month, :two_digits), required: true)
add_pair(post, :cardExpiryYear, format(credit_card.year, :two_digits), required: true)
add_pair(post, :cardCVV, credit_card.verification_value)
end
def add_threeds_required(post, options)
add_pair(post, :threeDSRequired, options[:threeds_required] || @threeds_required ? 'Y' : 'N')
end
def add_threeds_fields(post, options)
return unless three_d_secure = options[:three_d_secure]
add_pair(post, :threeDSEnrolled, formatted_enrollment(three_d_secure[:enrolled]))
if three_d_secure[:enrolled] == 'true'
add_pair(post, :threeDSAuthenticated, three_d_secure[:authentication_response_status])
if three_d_secure[:authentication_response_status] == 'Y'
post[:threeDSECI] = three_d_secure[:eci]
post[:threeDSCAVV] = three_d_secure[:cavv]
post[:threeDSXID] = three_d_secure[:xid] || three_d_secure[:ds_transaction_id]
end
end
end
def add_remote_address(post, options = {})
add_pair(post, :remoteAddress, options[:ip] || '1.1.1.1')
end
def add_country_code(post, options)
post[:countryCode] = options[:country_code] || self.supported_countries[0]
end
def normalize_line_endings(str)
str.gsub(/%0D%0A|%0A%0D|%0D/, '%0A')
end
def add_hmac(post)
result = post.sort.collect { |key, value| "#{key}=#{normalize_line_endings(CGI.escape(value.to_s))}" }.join('&')
result = Digest::SHA512.hexdigest("#{result}#{@options[:shared_secret]}")
add_pair(post, :signature, result)
end
def parse(body)
result = {}
pairs = body.split('&')
pairs.each do |pair|
a = pair.split('=')
# because some value pairs don't have a value
result[a[0].to_sym] = a[1] == nil ? '' : CGI.unescape(a[1])
end
result
end
def commit(action, parameters)
parameters.update(
merchantID: @options[:login],
action: action
)
# adds a signature to the post hash/array
add_hmac(parameters)
response = parse(ssl_post(self.live_url, post_data(action, parameters)))
Response.new(
response[:responseCode] == '0',
response[:responseCode] == '0' ? 'APPROVED' : response[:responseMessage],
response,
test: test?,
authorization: response[:xref],
cvv_result: CVV_CODE[response[:avscv2ResponseCode].to_s[0, 1]],
avs_result: avs_from(response)
)
end
def avs_from(response)
postal_match = AVS_POSTAL_MATCH[response[:avscv2ResponseCode].to_s[1, 1]]
street_match = AVS_STREET_MATCH[response[:avscv2ResponseCode].to_s[2, 1]]
code = if postal_match == 'Y' && street_match == 'Y'
'M'
elsif postal_match == 'Y'
'P'
elsif street_match == 'Y'
'A'
else
'I'
end
AVSResult.new({
code: code,
postal_match: postal_match,
street_match: street_match
})
end
def currency_code(currency)
CURRENCY_CODES[currency]
end
def post_data(action, parameters = {})
parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
def add_pair(post, key, value, options = {})
post[key] = value if !value.blank? || options[:required]
end
def formatted_enrollment(val)
case val
when 'Y', 'N', 'U' then val
when true, 'true' then 'Y'
when false, 'false' then 'N'
end
end
end
end
end