lib/active_merchant/billing/gateways/barclaycard_smartpay.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class BarclaycardSmartpayGateway < Gateway
self.test_url = 'https://pal-test.barclaycardsmartpay.com/pal/servlet'
self.live_url = 'https://pal-live.barclaycardsmartpay.com/pal/servlet'
self.supported_countries = %w[AL AD AM AT AZ BY BE BA BG HR CY CZ DK EE FI FR DE GR HU IS IE IT KZ LV LI LT LU MK MT MD MC ME NL NO PL PT RO RU SM RS SK SI ES SE CH TR UA GB VA]
self.default_currency = 'EUR'
self.currencies_with_three_decimal_places = %w(BHD KWD OMR RSD TND IQD JOD LYD)
self.money_format = :cents
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb dankort maestro]
self.currencies_without_fractions = %w(CVE DJF GNF IDR JPY KMF KRW PYG RWF UGX VND VUV XAF XOF XPF)
self.homepage_url = 'https://www.barclaycardsmartpay.com/'
self.display_name = 'Barclaycard Smartpay'
API_VERSION = 'v40'
def initialize(options = {})
requires!(options, :company, :merchant, :password)
super
end
def purchase(money, creditcard, options = {})
requires!(options, :order_id)
MultiResponse.run do |r|
r.process { authorize(money, creditcard, options) }
r.process { capture(money, r.authorization, options) }
end
end
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
post = payment_request(money, options)
post[:amount] = amount_hash(money, options[:currency])
post[:card] = credit_card_hash(creditcard)
post[:billingAddress] = billing_address_hash(options) if options[:billing_address]
post[:deliveryAddress] = shipping_address_hash(options) if options[:shipping_address]
post[:shopperStatement] = options[:shopper_statement] if options[:shopper_statement]
add_3ds(post, options)
commit('authorise', post)
end
def capture(money, authorization, options = {})
requires!(options, :order_id)
post = modification_request(authorization, options)
post[:modificationAmount] = amount_hash(money, options[:currency])
commit('capture', post)
end
def refund(money, authorization, options = {})
requires!(options, :order_id)
post = modification_request(authorization, options)
post[:modificationAmount] = amount_hash(money, options[:currency])
commit('refund', post)
end
def credit(money, creditcard, options = {})
post = payment_request(money, options)
post[:amount] = amount_hash(money, options[:currency])
post[:card] = credit_card_hash(creditcard)
post[:dateOfBirth] = options[:date_of_birth] if options[:date_of_birth]
post[:entityType] = options[:entity_type] if options[:entity_type]
post[:nationality] = options[:nationality] if options[:nationality]
post[:shopperName] = options[:shopper_name] if options[:shopper_name]
if options[:third_party_payout]
post[:recurring] = options[:recurring_contract] || { contract: 'PAYOUT' }
MultiResponse.run do |r|
r.process {
commit(
'storeDetailAndSubmitThirdParty',
post,
@options[:store_payout_account],
@options[:store_payout_password]
)
}
r.process {
commit(
'confirmThirdParty',
modification_request(r.authorization, @options),
@options[:review_payout_account],
@options[:review_payout_password]
)
}
end
else
commit('refundWithData', post)
end
end
def void(identification, options = {})
requires!(options, :order_id)
post = modification_request(identification, options)
commit('cancel', post)
end
def verify(creditcard, options = {})
authorize(0, creditcard, options)
end
def store(creditcard, options = {})
post = store_request(options)
post[:card] = credit_card_hash(creditcard)
post[:recurring] = { contract: 'RECURRING' }
commit('store', post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r(((?:\r\n)?Authorization: Basic )[^\r\n]+(\r\n)?), '\1[FILTERED]').
gsub(%r((card.number=)\d+), '\1[FILTERED]').
gsub(%r((card.cvc=)\d+), '\1[FILTERED]')
end
private
# Smartpay may return AVS codes not covered by standard AVSResult codes.
# Smartpay's descriptions noted below.
AVS_MAPPING = {
'0' => 'R', # Unknown
'1' => 'A', # Address matches, postal code doesn't
'2' => 'N', # Neither postal code nor address match
'3' => 'R', # AVS unavailable
'4' => 'E', # AVS not supported for this card type
'5' => 'U', # No AVS data provided
'6' => 'Z', # Postal code matches, address doesn't match
'7' => 'D', # Both postal code and address match
'8' => 'U', # Address not checked, postal code unknown
'9' => 'B', # Address matches, postal code unknown
'10' => 'N', # Address doesn't match, postal code unknown
'11' => 'U', # Postal code not checked, address unknown
'12' => 'B', # Address matches, postal code not checked
'13' => 'U', # Address doesn't match, postal code not checked
'14' => 'P', # Postal code matches, address unknown
'15' => 'P', # Postal code matches, address not checked
'16' => 'N', # Postal code doesn't match, address unknown
'17' => 'U', # Postal code doesn't match, address not checked
'18' => 'I' # Neither postal code nor address were checked
}
def commit(action, post, account = 'ws', password = @options[:password])
request = post_data(flatten_hash(post))
request_headers = headers(account, password)
raw_response = ssl_post(build_url(action), request, request_headers)
response = parse(raw_response)
Response.new(
success_from(response),
message_from(response),
response,
test: test?,
avs_result: AVSResult.new(code: parse_avs_code(response)),
authorization: response['recurringDetailReference'] || authorization_from(post, response)
)
rescue ResponseError => e
case e.response.code
when '401'
return Response.new(false, 'Invalid credentials', {}, test: test?)
when '403'
return Response.new(false, 'Not allowed', {}, test: test?)
when '422', '500'
if e.response.body.split(/\W+/).any? { |word| %w(validation configuration security).include?(word) }
error_message = e.response.body[/#{Regexp.escape('message=')}(.*?)#{Regexp.escape('&')}/m, 1].tr('+', ' ')
error_code = e.response.body[/#{Regexp.escape('errorCode=')}(.*?)#{Regexp.escape('&')}/m, 1]
return Response.new(false, error_code + ': ' + error_message, {}, test: test?)
end
end
raise
end
def authorization_from(parameters, response)
authorization = [parameters[:originalReference], response['pspReference']].compact
return nil if authorization.empty?
return authorization.join('#')
end
def parse_avs_code(response)
AVS_MAPPING[response['additionalData']['avsResult'][0..1].strip] if response.dig('additionalData', 'avsResult')
end
def flatten_hash(hash, prefix = nil)
flat_hash = {}
hash.each_pair do |key, val|
conc_key = prefix.nil? ? key : "#{prefix}.#{key}"
if val.is_a?(Hash)
flat_hash.merge!(flatten_hash(val, conc_key))
else
flat_hash[conc_key] = val
end
end
flat_hash
end
def headers(account, password)
{
'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8',
'Authorization' => 'Basic ' + Base64.strict_encode64("#{account}@Company.#{@options[:company]}:#{password}").strip
}
end
def parse(response)
parsed_response = {}
params = CGI.parse(response)
params.each do |key, value|
parsed_key = key.split('.', 2)
if parsed_key.size > 1
parsed_response[parsed_key[0]] ||= {}
parsed_response[parsed_key[0]][parsed_key[1]] = value[0]
else
parsed_response[parsed_key[0]] = value[0]
end
end
parsed_response
end
def post_data(data)
data.map do |key, val|
"#{key}=#{CGI.escape(val.to_s)}"
end.reduce do |x, y|
"#{x}&#{y}"
end
end
def message_from(response)
return response['resultCode'] if response.has_key?('resultCode') # Payment request
return response['response'] if response['response'] # Modification request
return response['result'] if response.has_key?('result') # Store/Recurring request
'Failure' # Negative fallback in case of error
end
def success_from(response)
return true if response['result'] == 'Success'
successful_results = %w(Authorised Received [payout-submit-received])
successful_responses = %w([capture-received] [cancel-received] [refund-received] [payout-confirm-received])
successful_results.include?(response['resultCode']) || successful_responses.include?(response['response'])
end
def build_url(action)
case action
when 'store'
"#{test? ? self.test_url : self.live_url}/Recurring/#{API_VERSION}/storeToken"
when 'finalize3ds'
"#{test? ? self.test_url : self.live_url}/Payment/#{API_VERSION}/authorise3d"
when 'storeDetailAndSubmitThirdParty', 'confirmThirdParty'
"#{test? ? self.test_url : self.live_url}/Payout/#{API_VERSION}/#{action}"
else
"#{test? ? self.test_url : self.live_url}/Payment/#{API_VERSION}/#{action}"
end
end
def billing_address_hash(options)
address = options[:address] || options[:billing_address] if options[:address] || options[:billing_address]
street = options[:street] || parse_street(address)
house = options[:house_number] || parse_house_number(address)
create_address_hash(address, house, street)
end
def shipping_address_hash(options)
address = options[:shipping_address]
street = options[:shipping_street] || parse_street(address)
house = options[:shipping_house_number] || parse_house_number(address)
create_address_hash(address, house, street)
end
def parse_street(address)
address_to_parse = "#{address[:address1]} #{address[:address2]}"
street = address[:street] || address_to_parse.split(/\s+/).keep_if { |x| x !~ /\d/ }.join(' ')
street.empty? ? 'Not Provided' : street
end
def parse_house_number(address)
address_to_parse = "#{address[:address1]} #{address[:address2]}"
house = address[:houseNumberOrName] || address_to_parse.split(/\s+/).keep_if { |x| x =~ /\d/ }.join(' ')
house.empty? ? 'Not Provided' : house
end
def create_address_hash(address, house, street)
hash = {}
hash[:houseNumberOrName] = house
hash[:street] = street
hash[:city] = address[:city]
hash[:stateOrProvince] = address[:state]
hash[:postalCode] = address[:zip]
hash[:country] = address[:country]
hash.keep_if { |_, v| v }
end
def amount_hash(money, currency)
currency = currency || currency(money)
hash = {}
hash[:currency] = currency
hash[:value] = localized_amount(money, currency) if money
hash
end
def credit_card_hash(creditcard)
hash = {}
hash[:cvc] = creditcard.verification_value if creditcard.verification_value
hash[:expiryMonth] = format(creditcard.month, :two_digits) if creditcard.month
hash[:expiryYear] = format(creditcard.year, :four_digits) if creditcard.year
hash[:holderName] = creditcard.name if creditcard.name
hash[:number] = creditcard.number if creditcard.number
hash
end
def modification_request(reference, options)
hash = {}
hash[:merchantAccount] = @options[:merchant]
hash[:originalReference] = psp_reference_from(reference)
hash.keep_if { |_, v| v }
end
def psp_reference_from(authorization)
authorization.nil? ? nil : authorization.split('#').first
end
def payment_request(money, options)
hash = {}
hash[:merchantAccount] = @options[:merchant]
hash[:reference] = options[:order_id]
hash[:shopperEmail] = options[:email]
hash[:shopperIP] = options[:ip]
hash[:shopperReference] = options[:customer]
hash[:shopperInteraction] = options[:shopper_interaction]
hash[:deviceFingerprint] = options[:device_fingerprint]
hash.keep_if { |_, v| v }
end
def store_request(options)
hash = {}
hash[:merchantAccount] = @options[:merchant]
hash[:shopperEmail] = options[:email]
hash[:shopperReference] = options[:customer] if options[:customer]
hash.keep_if { |_, v| v }
end
def add_3ds(post, options)
if three_ds_2_options = options[:three_ds_2]
device_channel = three_ds_2_options[:channel]
if device_channel == 'app'
post[:threeDS2RequestData] = { deviceChannel: device_channel }
else
add_browser_info(three_ds_2_options[:browser_info], post)
post[:threeDS2RequestData] = { deviceChannel: device_channel, notificationURL: three_ds_2_options[:notification_url] }
end
if options.has_key?(:execute_threed)
post[:additionalData] ||= {}
post[:additionalData][:executeThreeD] = options[:execute_threed]
post[:additionalData][:scaExemption] = options[:sca_exemption] if options[:sca_exemption]
end
else
return unless options[:execute_threed] || options[:threed_dynamic]
post[:browserInfo] = { userAgent: options[:user_agent], acceptHeader: options[:accept_header] }
post[:additionalData] = { executeThreeD: 'true' } if options[:execute_threed]
end
end
def add_browser_info(browser_info, post)
return unless browser_info
post[:browserInfo] = {
acceptHeader: browser_info[:accept_header],
colorDepth: browser_info[:depth],
javaEnabled: browser_info[:java],
language: browser_info[:language],
screenHeight: browser_info[:height],
screenWidth: browser_info[:width],
timeZoneOffset: browser_info[:timezone],
userAgent: browser_info[:user_agent]
}
end
end
end
end