require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class IxopayGateway < Gateway
self.test_url = 'https://secure.ixopay.com/transaction'
self.live_url = 'https://secure.ixopay.com/transaction'
self.supported_countries = %w(AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW)
self.default_currency = 'EUR'
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro]
self.homepage_url = 'https://www.ixopay.com'
self.display_name = 'Ixopay'
def initialize(options={})
requires!(options, :username, :password, :secret, :api_key)
@secret = options[:secret]
super
end
def purchase(money, payment_method, options={})
request = build_xml_request do |xml|
add_card_data(xml, payment_method)
add_debit(xml, money, options)
end
commit(request)
end
def authorize(money, payment_method, options={})
request = build_xml_request do |xml|
add_card_data(xml, payment_method)
add_preauth(xml, money, options)
end
commit(request)
end
def capture(money, authorization, options={})
request = build_xml_request do |xml|
add_capture(xml, money, authorization, options)
end
commit(request)
end
def refund(money, authorization, options={})
request = build_xml_request do |xml|
add_refund(xml, money, authorization, options)
end
commit(request)
end
def void(authorization, options={})
request = build_xml_request do |xml|
add_void(xml, authorization)
end
commit(request)
end
def verify(credit_card, options={})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
def supports_scrubbing?
true
end
def scrub(transcript)
clean_transcript = remove_invalid_utf_8_byte_sequences(transcript)
clean_transcript.
gsub(%r((Authorization: Gateway )(.*)(:)), '\1[FILTERED]\3').
gsub(%r(()(.*)()), '\1[FILTERED]\3').
gsub(%r(()(.*)()), '\1[FILTERED]\3').
gsub(%r(()\d+()), '\1[FILTERED]\2')
end
private
def remove_invalid_utf_8_byte_sequences(text)
text.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
end
def headers(xml)
timestamp = Time.now.httpdate
signature = generate_signature('POST', xml, timestamp)
{
'Authorization' => "Gateway #{options[:api_key]}:#{signature}",
'Date' => timestamp,
'Content-Type' => 'text/xml; charset=utf-8'
}
end
def generate_signature(http_method, xml, timestamp)
content_type = 'text/xml; charset=utf-8'
message = "#{http_method}\n#{Digest::MD5.hexdigest(xml)}\n#{content_type}\n#{timestamp}\n\n/transaction"
digest = OpenSSL::Digest.new('sha512')
hmac = OpenSSL::HMAC.digest(digest, @secret, message)
Base64.encode64(hmac).delete("\n")
end
def parse(body)
xml = Nokogiri::XML(body)
response = Hash.from_xml(xml.to_s)['result']
response.deep_transform_keys(&:underscore).transform_keys(&:to_sym)
end
def build_xml_request
builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.transactionWithCard 'xmlns' => 'http://secure.ixopay.com/Schema/V2/TransactionWithCard' do
xml.username @options[:username]
xml.password Digest::SHA1.hexdigest(@options[:password])
yield(xml)
end
end
builder.to_xml
end
def add_card_data(xml, payment_method)
xml.cardData do
xml.cardHolder payment_method.name
xml.pan payment_method.number
xml.cvv payment_method.verification_value
xml.expirationMonth format(payment_method.month, :two_digits)
xml.expirationYear format(payment_method.year, :four_digits)
end
end
def add_debit(xml, money, options)
currency = options[:currency] || currency(money)
description = options[:description].blank? ? 'Purchase' : options[:description]
xml.debit do
xml.transactionId new_transaction_id
add_customer_data(xml, options)
add_extra_data(xml, options[:extra_data]) if options[:extra_data]
xml.amount localized_amount(money, currency)
xml.currency currency
xml.description description
xml.callbackUrl(options[:callback_url])
add_stored_credentials(xml, options)
end
end
def add_preauth(xml, money, options)
description = options[:description].blank? ? 'Preauthorize' : options[:description]
currency = options[:currency] || currency(money)
callback_url = options[:callback_url]
xml.preauthorize do
xml.transactionId new_transaction_id
add_customer_data(xml, options)
add_extra_data(xml, options[:extra_data]) if options[:extra_data]
xml.amount localized_amount(money, currency)
xml.currency currency
xml.description description
xml.callbackUrl callback_url
add_stored_credentials(xml, options)
end
end
def add_refund(xml, money, authorization, options)
currency = options[:currency] || currency(money)
xml.refund do
xml.transactionId new_transaction_id
add_extra_data(xml, options[:extra_data]) if options[:extra_data]
xml.referenceTransactionId authorization&.split('|')&.first
xml.amount localized_amount(money, currency)
xml.currency currency
end
end
def add_void(xml, authorization)
xml.void do
xml.transactionId new_transaction_id
add_extra_data(xml, options[:extra_data]) if options[:extra_data]
xml.referenceTransactionId authorization&.split('|')&.first
end
end
def add_capture(xml, money, authorization, options)
currency = options[:currency] || currency(money)
xml.capture_ do
xml.transactionId new_transaction_id
add_extra_data(xml, options[:extra_data]) if options[:extra_data]
xml.referenceTransactionId authorization&.split('|')&.first
xml.amount localized_amount(money, currency)
xml.currency currency
end
end
def add_customer_data(xml, options)
# Ixopay returns an error if the elements are not added in the order used here.
xml.customer do
add_billing_address(xml, options[:billing_address]) if options[:billing_address]
add_shipping_address(xml, options[:shipping_address]) if options[:shipping_address]
xml.company options[:billing_address][:company] if options.dig(:billing_address, :company)
xml.email options[:email]
xml.ipAddress(options[:ip] || '127.0.0.1')
end
end
def add_billing_address(xml, address)
if address[:name]
xml.firstName split_names(address[:name])[0]
xml.lastName split_names(address[:name])[1]
end
xml.billingAddress1 address[:address1]
xml.billingAddress2 address[:address2]
xml.billingCity address[:city]
xml.billingPostcode address[:zip]
xml.billingState address[:state]
xml.billingCountry address[:country]
xml.billingPhone address[:phone]
end
def add_shipping_address(xml, address)
if address[:name]
xml.shippingFirstName split_names(address[:name])[0]
xml.shippingLastName split_names(address[:name])[1]
end
xml.shippingCompany address[:company]
xml.shippingAddress1 address[:address1]
xml.shippingAddress2 address[:address2]
xml.shippingCity address[:city]
xml.shippingPostcode address[:zip]
xml.shippingState address[:state]
xml.shippingCountry address[:country]
xml.shippingPhone address[:phone]
end
def new_transaction_id
SecureRandom.uuid
end
# Ixopay does not pass any parameters for cardholder/merchant initiated.
# Ixopay also doesn't support installment transactions, only recurring
# ("RECURRING") and unscheduled ("CARDONFILE").
#
# Furthermore, Ixopay is slightly unusual in its application of stored
# credentials in that the gateway does not return a true
# network_transaction_id that can be sent on subsequent transactions.
def add_stored_credentials(xml, options)
return unless stored_credential = options[:stored_credential]
if stored_credential[:initial_transaction]
xml.transactionIndicator 'INITIAL'
elsif stored_credential[:reason_type] == 'recurring'
xml.transactionIndicator 'RECURRING'
elsif stored_credential[:reason_type] == 'unscheduled'
xml.transactionIndicator 'CARDONFILE'
end
end
def add_extra_data(xml, extra_data)
extra_data.each do |k, v|
xml.extraData(v, key: k)
end
end
def commit(request)
url = (test? ? test_url : live_url)
# ssl_post raises an exception for any non-2xx HTTP status from the gateway
response =
begin
parse(ssl_post(url, request, headers(request)))
rescue StandardError => error
parse(error.response.body)
end
Response.new(
success_from(response),
message_from(response),
response,
authorization: authorization_from(response),
test: test?,
error_code: error_code_from(response)
)
end
def success_from(response)
response[:success] == 'true'
end
def message_from(response)
response.dig(:errors, 'error', 'message') || response[:return_type]
end
def authorization_from(response)
response[:reference_id] ? "#{response[:reference_id]}|#{response[:purchase_id]}" : nil
end
def error_code_from(response)
response.dig(:errors, 'error', 'code') unless success_from(response)
end
end
end
end