lib/active_merchant/billing/gateways/trans_first_transaction_express.rb
require 'nokogiri'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class TransFirstTransactionExpressGateway < Gateway
self.display_name = 'TransFirst Transaction Express'
self.homepage_url = 'http://transactionexpress.com/'
self.test_url = 'https://ws.cert.transactionexpress.com/portal/merchantframework/MerchantWebServices-v1?wsdl'
self.live_url = 'https://ws.transactionexpress.com/portal/merchantframework/MerchantWebServices-v1?wsdl'
self.supported_countries = ['US']
self.default_currency = 'USD'
self.money_format = :cents
self.supported_cardtypes = %i[visa master american_express discover diners_club]
V1_NAMESPACE = 'http://postilion/realtime/merchantframework/xsd/v1/'
SOAPENV_NAMESPACE = 'http://schemas.xmlsoap.org/soap/envelope/'
AUTHORIZATION_FIELD_SEPARATOR = '|'
APPROVAL_CODES = %w(00 10)
RESPONSE_MESSAGES = {
'00' => 'Approved',
'01' => 'Refer to card issuer',
'02' => 'Refer to card issuer, special condition',
'03' => 'Invalid merchant',
'04' => 'Pick-up card',
'05' => 'Do not honor',
'06' => 'Error',
'07' => 'Pick-up card, special condition',
'08' => 'Honor with identification',
'09' => 'Request in progress',
'10' => 'Approved, partial authorization',
'11' => 'VIP Approval',
'12' => 'Invalid transaction',
'13' => 'Invalid amount',
'14' => 'Invalid card number',
'15' => 'No such issuer',
'16' => 'Approved, update track 3',
'17' => 'Customer cancellation',
'18' => 'Customer dispute',
'19' => 'Re-enter transaction',
'20' => 'Invalid response',
'21' => 'No action taken',
'22' => 'Suspected malfunction',
'23' => 'Unacceptable transaction fee',
'24' => 'File update not supported',
'25' => 'Unable to locate record',
'26' => 'Duplicate record',
'27' => 'File update field edit error',
'28' => 'File update file locked',
'29' => 'File update failed',
'30' => 'Format error',
'31' => 'Bank not supported',
'33' => 'Expired card, pick-up',
'34' => 'Suspected fraud, pick-up',
'35' => 'Contact acquirer, pick-up',
'36' => 'Restricted card, pick-up',
'37' => 'Call acquirer security, pick-up',
'38' => 'PIN tries exceeded, pick-up',
'39' => 'No credit account',
'40' => 'Function not supported',
'41' => 'Lost card, pick-up',
'42' => 'No universal account',
'43' => 'Stolen card, pick-up',
'44' => 'No investment account',
'45' => 'Account closed',
'46' => 'Identification required',
'47' => 'Identification cross-check required',
'48' => 'No customer record',
'49' => 'Reserved for future Realtime use',
'50' => 'Reserved for future Realtime use',
'51' => 'Not sufficient funds',
'52' => 'No checking account',
'53' => 'No savings account',
'54' => 'Expired card',
'55' => 'Incorrect PIN',
'56' => 'No card record',
'57' => 'Transaction not permitted to cardholder',
'58' => 'Transaction not permitted on terminal',
'59' => 'Suspected fraud',
'60' => 'Contact acquirer',
'61' => 'Exceeds withdrawal limit',
'62' => 'Restricted card',
'63' => 'Security violation',
'64' => 'Original amount incorrect',
'65' => 'Exceeds withdrawal frequency',
'66' => 'Call acquirer security',
'67' => 'Hard capture',
'68' => 'Response received too late',
'69' => 'Advice received too late (the response from a request was received too late )',
'70' => 'Reserved for future use',
'71' => 'Reserved for future Realtime use',
'72' => 'Reserved for future Realtime use',
'73' => 'Reserved for future Realtime use',
'74' => 'Reserved for future Realtime use',
'75' => 'PIN tries exceeded',
'76' => 'Reversal: Unable to locate previous message (no match on Retrieval Reference Number)/ Reserved for future Realtime use',
'77' => 'Previous message located for a repeat or reversal, but repeat or reversal data is inconsistent with original message/ Intervene, bank approval required',
'78' => 'Invalid/non-existent account – Decline (MasterCard specific)/ Intervene, bank approval required for partial amount',
'79' => 'Already reversed (by Switch)/ Reserved for client-specific use (declined)',
'80' => 'No financial Impact (Reserved for declined debit)/ Reserved for client-specific use (declined)',
'81' => 'PIN cryptographic error found by the Visa security module during PIN decryption/ Reserved for client-specific use (declined)',
'82' => 'Incorrect CVV/ Reserved for client-specific use (declined)',
'83' => 'Unable to verify PIN/ Reserved for client-specific use (declined)',
'84' => 'Invalid Authorization Life Cycle – Decline (MasterCard) or Duplicate Transaction Detected (Visa)/ Reserved for client-specific use (declined)',
'85' => 'No reason to decline a request for Account Number Verification or Address Verification/ Reserved for client-specific use (declined)',
'86' => 'Cannot verify PIN/ Reserved for client-specific use (declined)',
'87' => 'Reserved for client-specific use (declined)',
'88' => 'Reserved for client-specific use (declined)',
'89' => 'Reserved for client-specific use (declined)',
'90' => 'Cut-off in progress',
'91' => 'Issuer or switch inoperative',
'92' => 'Routing error',
'93' => 'Violation of law',
'94' => 'Duplicate Transmission (Integrated Debit and MasterCard)',
'95' => 'Reconcile error',
'96' => 'System malfunction',
'97' => 'Reserved for future Realtime use',
'98' => 'Exceeds cash limit',
'99' => 'Reserved for future Realtime use',
'1106' => 'Reserved for future Realtime use',
'0A' => 'Reserved for future Realtime use',
'A0' => 'Reserved for future Realtime use',
'A1' => 'ATC not incremented',
'A2' => 'ATC limit exceeded',
'A3' => 'ATC configuration error',
'A4' => 'CVR check failure',
'A5' => 'CVR configuration error',
'A6' => 'TVR check failure',
'A7' => 'TVR configuration error',
'A8' => 'Reserved for future Realtime use',
'B1' => 'Surcharge amount not permitted on Visa cards or EBT Food Stamps/ Reserved for future Realtime use',
'B2' => 'Surcharge amount not supported by debit network issuer/ Reserved for future Realtime use',
'C1' => 'Unacceptable PIN',
'C2' => 'PIN Change failed',
'C3' => 'PIN Unblock failed',
'D1' => 'MAC Error',
'E1' => 'Prepay error',
'N1' => 'Network Error within the TXP platform',
'N0' => 'Force STIP/ Reserved for client-specific use (declined)',
'N3' => 'Cash service not available/ Reserved for client-specific use (declined)',
'N4' => 'Cash request exceeds Issuer limit/ Reserved for client-specific use (declined)',
'N5' => 'Ineligible for re-submission/ Reserved for client-specific use (declined)',
'N7' => 'Decline for CVV2 failure/ Reserved for client-specific use (declined)',
'N8' => 'Transaction amount exceeds preauthorized approval amount/ Reserved for client-specific use (declined)',
'P0' => 'Approved; PVID code is missing, invalid, or has expired',
'P1' => 'Declined; PVID code is missing, invalid, or has expired/ Reserved for client-specific use (declined)',
'P2' => 'Invalid biller Information/ Reserved for client-specific use (declined)/ Reserved for client-specific use (declined)',
'R0' => 'The transaction was declined or returned, because the cardholder requested that payment of a specific recurring or installment payment transaction be stopped/ Reserved for client-specific use (declined)',
'R1' => 'The transaction was declined or returned, because the cardholder requested that payment of all recurring or installment payment transactions for a specific merchant account be stopped/ Reserved for client-specific use (declined)',
'Q1' => 'Card Authentication failed/ Reserved for client-specific use (declined)',
'XA' => 'Forward to Issuer/ Reserved for client-specific use (declined)',
'XD' => 'Forward to Issuer/ Reserved for client-specific use (declined)'
}
EXTENDED_RESPONSE_MESSAGES = {
'B40K' => 'Declined Post – Credit linked to unextracted settle transaction'
}
TRANSACTION_CODES = {
authorize: 0,
void_authorize: 2,
purchase: 1,
capture: 3,
void_purchase: 6,
void_capture: 6,
refund: 4,
credit: 5,
void_refund: 13,
void_credit: 13,
verify: 9,
purchase_echeck: 11,
refund_echeck: 16,
void_echeck: 16,
wallet_sale: 14
}
def initialize(options = {})
requires!(options, :gateway_id, :reg_key)
super
end
def purchase(amount, payment_method, options = {})
if credit_card?(payment_method)
action = :purchase
request = build_xml_transaction_request do |doc|
add_credit_card(doc, payment_method)
add_contact(doc, payment_method.name, options)
add_amount(doc, amount)
add_order_number(doc, options)
end
elsif echeck?(payment_method)
action = :purchase_echeck
request = build_xml_transaction_request do |doc|
add_echeck(doc, payment_method)
add_contact(doc, payment_method.name, options)
add_amount(doc, amount)
add_order_number(doc, options)
end
else
action = :wallet_sale
wallet_id = split_authorization(payment_method).last
request = build_xml_transaction_request do |doc|
add_amount(doc, amount)
add_wallet_id(doc, wallet_id)
end
end
commit(action, request)
end
def authorize(amount, payment_method, options = {})
if credit_card?(payment_method)
request = build_xml_transaction_request do |doc|
add_credit_card(doc, payment_method)
add_contact(doc, payment_method.name, options)
add_amount(doc, amount)
end
else
wallet_id = split_authorization(payment_method).last
request = build_xml_transaction_request do |doc|
add_amount(doc, amount)
add_wallet_id(doc, wallet_id)
end
end
commit(:authorize, request)
end
def capture(amount, authorization, options = {})
transaction_id = split_authorization(authorization)[1]
request = build_xml_transaction_request do |doc|
add_amount(doc, amount)
add_original_transaction_data(doc, transaction_id)
end
commit(:capture, request)
end
def void(authorization, options = {})
action, transaction_id = split_authorization(authorization)
request = build_xml_transaction_request do |doc|
add_original_transaction_data(doc, transaction_id)
end
commit(void_type(action), request)
end
def refund(amount, authorization, options = {})
action, transaction_id = split_authorization(authorization)
request = build_xml_transaction_request do |doc|
add_amount(doc, amount) unless action == 'purchase_echeck'
add_original_transaction_data(doc, transaction_id)
end
commit(refund_type(action), request)
end
def credit(amount, payment_method, options = {})
request = build_xml_transaction_request do |doc|
add_pan(doc, payment_method)
add_amount(doc, amount)
end
commit(:credit, request)
end
def verify(credit_card, options = {})
request = build_xml_transaction_request do |doc|
add_credit_card(doc, credit_card)
add_contact(doc, credit_card.name, options)
end
commit(:verify, request)
end
def store(payment_method, options = {})
store_customer_request = build_xml_payment_storage_request do |doc|
store_customer_details(doc, payment_method.name, options)
end
MultiResponse.run do |r|
r.process { commit(:store, store_customer_request) }
return r unless r.success? && r.params['custId']
customer_id = r.params['custId']
store_payment_method_request = build_xml_payment_storage_request do |doc|
doc['v1'].cust do
add_customer_id(doc, customer_id)
doc['v1'].pmt do
doc['v1'].type 0 # add
add_credit_card(doc, payment_method)
end
end
end
r.process { commit(:store, store_payment_method_request) }
end
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((<[^>]+pan>)[^<]+(<))i, '\1[FILTERED]\2').
gsub(%r((<[^>]+sec>)[^<]+(<))i, '\1[FILTERED]\2').
gsub(%r((<[^>]+id>)[^<]+(<))i, '\1[FILTERED]\2').
gsub(%r((<[^>]+regKey>)[^<]+(<))i, '\1[FILTERED]\2').
gsub(%r((<[^>]+acctNr>)[^<]+(<))i, '\1[FILTERED]\2')
end
private
CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") }
CURRENCY_CODES['USD'] = '840'
def headers
{
'Content-Type' => 'text/xml'
}
end
def commit(action, request)
request = add_transaction_code_to_request(request, action)
raw_response =
begin
ssl_post(url, request, headers)
rescue ActiveMerchant::ResponseError => e
e.response.body
end
response = parse(raw_response)
succeeded = success_from(response)
Response.new(
succeeded,
message_from(succeeded, response),
response,
error_code: error_code_from(succeeded, response),
authorization: authorization_from(action, response),
avs_result: AVSResult.new(code: response['avsRslt']),
cvv_result: CVVResult.new(response['secRslt']),
test: test?
)
end
def url
test? ? test_url : live_url
end
def parse(xml)
response = {}
doc = Nokogiri::XML(xml).remove_namespaces!
doc.css('Envelope Body *').each do |node|
# node.name is more readable, but uniq_name is occasionally necessary
uniq_name = [node.parent.name, node.name].join('_')
response[uniq_name] = node.text
response[node.name] = node.text
end
response
end
def success_from(response)
fault = response['Fault']
approved_transaction = APPROVAL_CODES.include?(response['rspCode'])
found_contact = response['FndRecurrProfResponse']
return !fault && (approved_transaction || found_contact)
end
def error_code_from(succeeded, response)
return if succeeded
response['errorCode'] || response['rspCode']
end
def message_from(succeeded, response)
return 'Succeeded' if succeeded
if response['rspCode']
code = response['rspCode']
extended_code = response['extRspCode']
message = RESPONSE_MESSAGES[code]
extended = EXTENDED_RESPONSE_MESSAGES[extended_code]
ach_response = response['achResponse']
[message, extended, ach_response].compact.join('. ')
else
response['faultstring']
end
end
def authorization_from(action, response)
authorization = response['tranNr'] || response['pmtId']
# guard so we don't return something like "purchase|"
return unless authorization
[action, authorization].join(AUTHORIZATION_FIELD_SEPARATOR)
end
# -- helper methods ----------------------------------------------------
def credit_card?(payment_method)
payment_method.respond_to?(:verification_value)
end
def echeck?(payment_method)
payment_method.respond_to?(:routing_number)
end
def split_authorization(authorization)
authorization.split(AUTHORIZATION_FIELD_SEPARATOR)
end
def void_type(action)
action == 'purchase_echeck' ? :void_echeck : :"void_#{action}"
end
def refund_type(action)
action == 'purchase_echeck' ? :refund_echeck : :refund
end
# -- request methods ---------------------------------------------------
def build_xml_transaction_request
build_xml_request('SendTranRequest') do |doc|
yield doc
end
end
def build_xml_payment_storage_request
build_xml_request('UpdtRecurrProfRequest') do |doc|
yield doc
end
end
def build_xml_payment_update_request
merchant_product_type = 5 # credit card
build_xml_request('UpdtRecurrProfRequest', merchant_product_type) do |doc|
yield doc
end
end
def build_xml_payment_search_request
build_xml_request('FndRecurrProfRequest') do |doc|
yield doc
end
end
def build_xml_request(wrapper, merchant_product_type = nil)
Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml['soapenv'].Envelope('xmlns:soapenv' => SOAPENV_NAMESPACE) do
xml['soapenv'].Body do
xml['v1'].send(wrapper, 'xmlns:v1' => V1_NAMESPACE) do
add_merchant(xml)
yield(xml)
end
end
end
end.doc.root.to_xml
end
def add_transaction_code_to_request(request, action)
# store requests don't get a transaction code
return request if action == :store
doc = Nokogiri::XML::Document.parse(request)
merc_nodeset = doc.xpath('//v1:merc', 'v1' => V1_NAMESPACE)
merc_nodeset.after "<v1:tranCode>#{TRANSACTION_CODES[action]}</v1:tranCode>"
doc.root.to_xml
end
def add_merchant(doc, product_type = nil)
doc['v1'].merc do
doc['v1'].id @options[:gateway_id]
doc['v1'].regKey @options[:reg_key]
doc['v1'].inType '1'
doc['v1'].prodType product_type if product_type
end
end
def add_amount(doc, money)
doc['v1'].reqAmt amount(money)
end
def add_order_number(doc, options)
return unless options[:order_id]
doc['v1'].authReq {
doc['v1'].ordNr options[:order_id]
}
end
def add_credit_card(doc, payment_method)
doc['v1'].card {
doc['v1'].pan payment_method.number
doc['v1'].sec payment_method.verification_value if payment_method.verification_value?
doc['v1'].xprDt expiration_date(payment_method)
}
end
def add_echeck(doc, payment_method)
doc['v1'].achEcheck {
doc['v1'].bankRtNr payment_method.routing_number
doc['v1'].acctNr payment_method.account_number
}
end
def expiration_date(payment_method)
yy = format(payment_method.year, :two_digits)
mm = format(payment_method.month, :two_digits)
yy + mm
end
def add_pan(doc, payment_method)
doc['v1'].card do
doc['v1'].pan payment_method.number
end
end
def add_contact(doc, fullname, options)
doc['v1'].contact do
doc['v1'].fullName fullname unless fullname.blank?
doc['v1'].coName options[:company_name] if options[:company_name]
doc['v1'].title options[:title] if options[:title]
if (billing_address = options[:billing_address])
if billing_address[:phone]
doc['v1'].phone do
doc['v1'].type(options[:phone_number_type] || '4')
doc['v1'].nr billing_address[:phone].gsub(/\D/, '')
end
end
doc['v1'].addrLn1 billing_address[:address1] if billing_address[:address1]
doc['v1'].addrLn2 billing_address[:address2] unless billing_address[:address2].blank?
doc['v1'].city billing_address[:city] if billing_address[:city]
doc['v1'].state billing_address[:state] if billing_address[:state]
doc['v1'].zipCode billing_address[:zip].delete('-') if billing_address[:zip]
doc['v1'].ctry 'US'
end
doc['v1'].email options[:email] if options[:email]
doc['v1'].type options[:contact_type] if options[:contact_type]
doc['v1'].stat options[:contact_stat] if options[:contact_stat]
if (shipping_address = options[:shipping_address])
doc['v1'].ship do
doc['v1'].fullName fullname unless fullname.blank?
doc['v1'].addrLn1 shipping_address[:address1] if shipping_address[:address1]
doc['v1'].addrLn2 shipping_address[:address2] unless shipping_address[:address2].blank?
doc['v1'].city shipping_address[:city] if shipping_address[:city]
doc['v1'].state shipping_address[:state] if shipping_address[:state]
doc['v1'].zipCode shipping_address[:zip].delete('-') if shipping_address[:zip]
doc['v1'].phone shipping_address[:phone].gsub(/\D/, '') if shipping_address[:phone]
doc['v1'].email shipping_address[:email] if shipping_address[:email]
end
end
end
end
def add_name(doc, payment_method)
doc['v1'].contact do
doc['v1'].fullName payment_method.name unless payment_method.name.blank?
end
end
def add_original_transaction_data(doc, authorization)
doc['v1'].origTranData do
doc['v1'].tranNr authorization
end
end
def store_customer_details(doc, fullname, options)
options[:contact_type] = 1 # recurring
options[:contact_stat] = 1 # active
doc['v1'].cust do
doc['v1'].type 0 # add
add_contact(doc, fullname, options)
end
end
def add_customer_id(doc, customer_id)
doc['v1'].contact do
doc['v1'].id customer_id
end
end
def add_wallet_id(doc, wallet_id)
doc['v1'].recurMan do
doc['v1'].id wallet_id
end
end
end
end
end