lib/active_merchant/billing/gateways/payex.rb
require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class PayexGateway < Gateway
class_attribute :live_external_url, :test_external_url, :live_confined_url, :test_confined_url
self.live_external_url = 'https://external.payex.com/'
self.test_external_url = 'https://test-external.payex.com/'
self.live_confined_url = 'https://confined.payex.com/'
self.test_confined_url = 'https://test-confined.payex.com/'
self.money_format = :cents
self.supported_countries = %w[DK FI NO SE]
self.supported_cardtypes = %i[visa master american_express discover]
self.homepage_url = 'http://payex.com/'
self.display_name = 'Payex'
self.default_currency = 'EUR'
TRANSACTION_STATUS = {
sale: '0',
initialize: '1',
credit: '2',
authorize: '3',
cancel: '4',
failure: '5',
capture: '6'
}
SOAP_ACTIONS = {
initialize: { name: 'Initialize8', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
purchasecc: { name: 'PurchaseCC', url: 'pxconfined/pxorder.asmx', xmlns: 'http://confined.payex.com/PxOrder/', confined: true },
cancel: { name: 'Cancel2', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
capture: { name: 'Capture5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
credit: { name: 'Credit5', url: 'pxorder/pxorder.asmx', xmlns: 'http://external.payex.com/PxOrder/' },
create_agreement: { name: 'CreateAgreement3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' },
delete_agreement: { name: 'DeleteAgreement', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' },
autopay: { name: 'AutoPay3', url: 'pxagreement/pxagreement.asmx', xmlns: 'http://external.payex.com/PxAgreement/' }
}
def initialize(options = {})
requires!(options, :account, :encryption_key)
super
end
# Public: Send an authorize Payex request
#
# amount - The monetary amount of the transaction in cents.
# payment_method - The Active Merchant payment method or the +store+ authorization for stored transactions.
# options - A standard ActiveMerchant options hash:
# :currency - Three letter currency code for the transaction (default: "EUR")
# :order_id - The unique order ID for this transaction (required).
# :product_number - The merchant product number (default: '1').
# :description - The merchant description for this product (default: The :order_id).
# :ip - The client IP address (default: '127.0.0.1').
# :vat - The vat amount (optional).
#
# Returns an ActiveMerchant::Billing::Response object
def authorize(amount, payment_method, options = {})
requires!(options, :order_id)
amount = amount(amount)
if payment_method.respond_to?(:number)
# credit card authorization
MultiResponse.new.tap do |r|
r.process { send_initialize(amount, true, options) }
r.process { send_purchasecc(payment_method, r.params['orderref']) }
end
else
# stored authorization
send_autopay(amount, payment_method, true, options)
end
end
# Public: Send a purchase Payex request
#
# amount - The monetary amount of the transaction in cents.
# payment_method - The Active Merchant payment method or the +store+ authorization for stored transactions.
# options - A standard ActiveMerchant options hash:
# :currency - Three letter currency code for the transaction (default: "EUR")
# :order_id - The unique order ID for this transaction (required).
# :product_number - The merchant product number (default: '1').
# :description - The merchant description for this product (default: The :order_id).
# :ip - The client IP address (default: '127.0.0.1').
# :vat - The vat amount (optional).
#
# Returns an ActiveMerchant::Billing::Response object
def purchase(amount, payment_method, options = {})
requires!(options, :order_id)
amount = amount(amount)
if payment_method.respond_to?(:number)
# credit card purchase
MultiResponse.new.tap do |r|
r.process { send_initialize(amount, false, options) }
r.process { send_purchasecc(payment_method, r.params['orderref']) }
end
else
# stored purchase
send_autopay(amount, payment_method, false, options)
end
end
# Public: Capture money from a previously authorized transaction
#
# money - The amount to capture
# authorization - The authorization token from the authorization request
#
# Returns an ActiveMerchant::Billing::Response object
def capture(money, authorization, options = {})
amount = amount(money)
send_capture(amount, authorization)
end
# Public: Voids an authorize transaction
#
# authorization - The authorization returned from the successful authorize transaction.
# options - A standard ActiveMerchant options hash
#
# Returns an ActiveMerchant::Billing::Response object
def void(authorization, options = {})
send_cancel(authorization)
end
# Public: Refunds a purchase transaction
#
# money - The amount to refund
# authorization - The authorization token from the purchase request.
# options - A standard ActiveMerchant options hash:
# :order_id - The unique order ID for this transaction (required).
# :vat_amount - The vat amount (optional).
#
# Returns an ActiveMerchant::Billing::Response object
def refund(money, authorization, options = {})
requires!(options, :order_id)
amount = amount(money)
send_credit(authorization, amount, options)
end
# Public: Stores a credit card and creates a Payex agreement with a customer
#
# creditcard - The credit card to store.
# options - A standard ActiveMerchant options hash:
# :order_id - The unique order ID for this transaction (required).
# :merchant_ref - A reference that links this agreement to something the merchant takes money for (default: '1')
# :currency - Three letter currency code for the transaction (default: "EUR")
# :product_number - The merchant product number (default: '1').
# :description - The merchant description for this product (default: The :order_id).
# :ip - The client IP address (default: '127.0.0.1').
# :max_amount - The maximum amount to allow to be charged (default: 100000).
# :vat - The vat amount (optional).
#
# Returns an ActiveMerchant::Billing::Response object where the authorization is set to the agreement_ref which is used for stored payments.
def store(creditcard, options = {})
requires!(options, :order_id)
amount = amount(1) # 1 cent for authorization
MultiResponse.run(:first) do |r|
r.process { send_create_agreement(options) }
r.process { send_initialize(amount, true, options.merge({ agreement_ref: r.authorization })) }
order_ref = r.params['orderref']
r.process { send_purchasecc(creditcard, order_ref) }
end
end
# Public: Unstores a customer's credit card and deletes their Payex agreement.
#
# authorization - The authorization token from the store request.
#
# Returns an ActiveMerchant::Billing::Response object
def unstore(authorization, options = {})
send_delete_agreement(authorization)
end
private
def send_initialize(amount, is_auth, options = {})
properties = {
accountNumber: @options[:account],
purchaseOperation: is_auth ? 'AUTHORIZATION' : 'SALE',
price: amount,
priceArgList: nil,
currency: (options[:currency] || default_currency),
vat: options[:vat] || 0,
orderID: options[:order_id],
productNumber: options[:product_number] || '1',
description: options[:description] || options[:order_id],
clientIPAddress: options[:client_ip_address] || '127.0.0.1',
clientIdentifier: nil,
additionalValues: nil,
externalID: nil,
returnUrl: 'http://example.net', # set to dummy value since this is not used but is required
view: 'CREDITCARD',
agreementRef: options[:agreement_ref], # this is used to attach a stored agreement to a transaction as part of the store card
cancelUrl: nil,
clientLanguage: nil
}
hash_fields = %i[accountNumber purchaseOperation price priceArgList currency vat orderID
productNumber description clientIPAddress clientIdentifier additionalValues
externalID returnUrl view agreementRef cancelUrl clientLanguage]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:initialize]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_purchasecc(payment_method, order_ref)
properties = {
accountNumber: @options[:account],
orderRef: order_ref,
transactionType: 1, # online payment
cardNumber: payment_method.number,
cardNumberExpireMonth: format(payment_method.month, :two_digits),
cardNumberExpireYear: format(payment_method.year, :two_digits),
cardHolderName: payment_method.name,
cardNumberCVC: payment_method.verification_value
}
hash_fields = %i[accountNumber orderRef transactionType cardNumber cardNumberExpireMonth
cardNumberExpireYear cardNumberCVC cardHolderName]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:purchasecc]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_autopay(amount, authorization, is_auth, options = {})
properties = {
accountNumber: @options[:account],
agreementRef: authorization,
price: amount,
productNumber: options[:product_number] || '1',
description: options[:description] || options[:order_id],
orderId: options[:order_id],
purchaseOperation: is_auth ? 'AUTHORIZATION' : 'SALE',
currency: (options[:currency] || default_currency)
}
hash_fields = %i[accountNumber agreementRef price productNumber description orderId purchaseOperation currency]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:autopay]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_capture(amount, transaction_number, options = {})
properties = {
accountNumber: @options[:account],
transactionNumber: transaction_number,
amount: amount,
orderId: options[:order_id] || '',
vatAmount: options[:vat_amount] || 0,
additionalValues: ''
}
hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:capture]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_credit(transaction_number, amount, options = {})
properties = {
accountNumber: @options[:account],
transactionNumber: transaction_number,
amount: amount,
orderId: options[:order_id],
vatAmount: options[:vat_amount] || 0,
additionalValues: ''
}
hash_fields = %i[accountNumber transactionNumber amount orderId vatAmount additionalValues]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:credit]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_cancel(transaction_number)
properties = {
accountNumber: @options[:account],
transactionNumber: transaction_number
}
hash_fields = %i[accountNumber transactionNumber]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:cancel]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_create_agreement(options)
properties = {
accountNumber: @options[:account],
merchantRef: options[:merchant_ref] || '1',
description: options[:description] || options[:order_id],
purchaseOperation: 'SALE',
maxAmount: options[:max_amount] || 100000, # default to 1,000
notifyUrl: '',
startDate: options[:startDate] || '',
stopDate: options[:stopDate] || ''
}
hash_fields = %i[accountNumber merchantRef description purchaseOperation maxAmount notifyUrl startDate stopDate]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:create_agreement]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def send_delete_agreement(authorization)
properties = {
accountNumber: @options[:account],
agreementRef: authorization
}
hash_fields = %i[accountNumber agreementRef]
add_request_hash(properties, hash_fields)
soap_action = SOAP_ACTIONS[:delete_agreement]
request = build_xml_request(soap_action, properties)
commit(soap_action, request)
end
def url_for(soap_action)
File.join(base_url(soap_action), soap_action[:url])
end
def base_url(soap_action)
if soap_action[:confined]
test? ? test_confined_url : live_confined_url
else
test? ? test_external_url : live_external_url
end
end
# this will add a hash to the passed in properties as required by Payex requests
def add_request_hash(properties, fields)
data = fields.map { |e| properties[e] }
data << @options[:encryption_key]
properties['hash_'] = Digest::MD5.hexdigest(data.join(''))
end
def build_xml_request(soap_action, properties)
builder = Nokogiri::XML::Builder.new
builder.__send__('soap12:Envelope', { 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
'xmlns:soap12' => 'http://www.w3.org/2003/05/soap-envelope' }) do |root|
root.__send__('soap12:Body') do |body|
body.__send__(soap_action[:name], xmlns: soap_action[:xmlns]) do |doc|
properties.each do |key, val|
doc.send(key, val)
end
end
end
end
builder.to_xml
end
def parse(xml)
response = {}
xmldoc = Nokogiri::XML(xml)
body = xmldoc.xpath('//soap:Body/*[1]')[0].inner_text
doc = Nokogiri::XML(body)
doc.root&.xpath('*')&.each do |node|
if node.elements.size == 0
response[node.name.downcase.to_sym] = node.text
else
node.elements.each do |childnode|
name = "#{node.name.downcase}_#{childnode.name.downcase}"
response[name.to_sym] = childnode.text
end
end
end
response
end
# Commits all requests to the Payex soap endpoint
def commit(soap_action, request)
url = url_for(soap_action)
headers = {
'Content-Type' => 'application/soap+xml; charset=utf-8',
'Content-Length' => request.size.to_s
}
response = parse(ssl_post(url, request, headers))
Response.new(
success?(response),
message_from(response),
response,
test: test?,
authorization: build_authorization(response)
)
end
def build_authorization(response)
# agreementref is for the store transaction, everything else gets transactionnumber
response[:transactionnumber] || response[:agreementref]
end
def success?(response)
response[:status_errorcode] == 'OK' && response[:transactionstatus] != TRANSACTION_STATUS[:failure]
end
def message_from(response)
response[:status_description]
end
end
end
end