require 'nokogiri' module ActiveMerchant #:nodoc: module Billing #:nodoc: class ProPayGateway < Gateway self.test_url = 'https://xmltest.propay.com/API/PropayAPI.aspx' self.live_url = 'https://epay.propay.com/api/propayapi.aspx' self.supported_countries = %w[US CA] self.default_currency = 'USD' self.money_format = :cents self.supported_cardtypes = %i[visa master american_express discover] self.homepage_url = 'https://www.propay.com/' self.display_name = 'ProPay' STATUS_RESPONSE_CODES = { '00' => 'Success', '20' => 'Invalid username', '21' => 'Invalid transType', '22' => 'Invalid Currency Code', '23' => 'Invalid accountType', '24' => 'Invalid sourceEmail', '25' => 'Invalid firstName', '26' => 'Invalid mInitial', '27' => 'Invalid lastName', '28' => 'Invalid billAddr', '29' => 'Invalid aptNum', '30' => 'Invalid city', '31' => 'Invalid state', '32' => 'Invalid billZip', '33' => 'Invalid mailAddr', '34' => 'Invalid mailApt', '35' => 'Invalid mailCity', '36' => 'Invalid mailState', '37' => 'Invalid mailZip', '38' => 'Invalid dayPhone', '39' => 'Invalid evenPhone', '40' => 'Invalid ssn', '41' => 'Invalid dob', '42' => 'Invalid recEmail', '43' => 'Invalid knownAccount', '44' => 'Invalid amount', '45' => 'Invalid invNum', '46' => 'Invalid rtNum', '47' => 'Invalid accntNum', '48' => 'Invalid ccNum', '49' => 'Invalid expDate', '50' => 'Invalid cvv2', '51' => 'Invalid transNum and/or Unable to act perform actions on transNum due to funding', '52' => 'Invalid splitNum', '53' => 'A ProPay account with this email address already exists AND/OR User has no account number', '54' => 'A ProPay account with this social security number already exists', '55' => 'The email address provided does not correspond to a ProPay account.', '56' => 'Recipient’s email address shouldn’t have a ProPay account and does', '57' => 'Cannot settle transaction because it already expired', '58' => 'Credit card declined', '59' => 'Invalid Credential or IP address not allowed', '60' => 'Credit card authorization timed out; retry at a later time', '61' => 'Amount exceeds single transaction limit', '62' => 'Amount exceeds monthly volume limit', '63' => 'Insufficient funds in account', '64' => 'Over credit card use limit', '65' => 'Miscellaneous error', '66' => 'Denied a ProPay account', '67' => 'Unauthorized service requested', '68' => 'Account not affiliated', '69' => 'Duplicate invoice number (The same card was charged for the same amount with the same invoice number (including blank invoices) in a 1 minute period. Details about the original transaction are included whenever a 69 response is returned. These details include a repeat of the auth code, the original AVS response, and the original CVV response.)', '70' => 'Duplicate external ID', '71' => 'Account previously set up, but problem affiliating it with partner', '72' => 'The ProPay Account has already been upgraded to a Premium Account', '73' => 'Invalid Destination Account', '74' => 'Account or Trans Error', '75' => 'Money already pulled', '76' => 'Not Premium (used only for push/pull transactions)', '77' => 'Empty results', '78' => 'Invalid Authentication', '79' => 'Generic account status error', '80' => 'Invalid Password', '81' => 'Account Expired', '82' => 'InvalidUserID', '83' => 'BatchTransCountError', '84' => 'InvalidBeginDate', '85' => 'InvalidEndDate', '86' => 'InvalidExternalID', '87' => 'DuplicateUserID', '88' => 'Invalid track 1', '89' => 'Invalid track 2', '90' => 'Transaction already refunded', '91' => 'Duplicate Batch ID' } TRANSACTION_RESPONSE_CODES = { '00' => 'Success', '1' => 'Transaction blocked by issuer', '4' => 'Pick up card and deny transaction', '5' => 'Problem with the account', '6' => 'Customer requested stop to recurring payment', '7' => 'Customer requested stop to all recurring payments', '8' => 'Honor with ID only', '9' => 'Unpaid items on customer account', '12' => 'Invalid transaction', '13' => 'Amount Error', '14' => 'Invalid card number', '15' => 'No such issuer. Could not route transaction', '16' => 'Refund error', '17' => 'Over limit', '19' => 'Reenter transaction or the merchant account may be boarded incorrectly', '25' => 'Invalid terminal 41 Lost card', '43' => 'Stolen card', '51' => 'Insufficient funds', '52' => 'No such account', '54' => 'Expired card', '55' => 'Incorrect PIN', '57' => 'Bank does not allow this type of purchase', '58' => 'Credit card network does not allow this type of purchase for your merchant account.', '61' => 'Exceeds issuer withdrawal limit', '62' => 'Issuer does not allow this card to be charged for your business.', '63' => 'Security Violation', '65' => 'Activity limit exceeded', '75' => 'PIN tries exceeded', '76' => 'Unable to locate account', '78' => 'Account not recognized', '80' => 'Invalid Date', '82' => 'Invalid CVV2', '83' => 'Cannot verify the PIN', '85' => 'Service not supported for this card', '93' => 'Cannot complete transaction. Customer should call 800 number.', '95' => 'Misc Error Transaction failure', '96' => 'Issuer system malfunction or timeout.', '97' => 'Approved for a lesser amount. ProPay will not settle and consider this a decline.', '98' => 'Failure HV', '99' => 'Generic decline or unable to parse issuer response code' } def initialize(options={}) requires!(options, :cert_str) super end def purchase(money, payment, options={}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) add_address(xml, options) add_account(xml, options) add_recurring(xml, options) xml.transType '04' end commit(request) end def authorize(money, payment, options={}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) add_address(xml, options) add_account(xml, options) add_recurring(xml, options) xml.transType '05' end commit(request) end def capture(money, authorization, options={}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_account(xml, options) xml.transNum authorization xml.transType '06' end commit(request) end def refund(money, authorization, options={}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_account(xml, options) xml.transNum authorization xml.transType '07' end commit(request) end def void(authorization, options={}) refund(nil, authorization, options) end def credit(money, payment, options={}) request = build_xml_request do |xml| add_invoice(xml, money, options) add_payment(xml, payment, options) add_account(xml, options) xml.transType '35' 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) transcript. gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2'). gsub(%r(().+()), '\1[FILTERED]\2') end private def add_payment(xml, payment, options) xml.ccNum payment.number xml.expDate "#{format(payment.month, :two_digits)}#{format(payment.year, :two_digits)}" xml.CVV2 payment.verification_value xml.cardholderName payment.name end def add_address(xml, options) if address = options[:billing_address] || options[:address] xml.addr address[:address1] xml.aptNum address[:address2] xml.city address[:city] xml.state address[:state] xml.zip address[:zip].to_s.delete('-') end end def add_account(xml, options) xml.accountNum options[:account_num] end def add_invoice(xml, money, options) xml.amount amount(money) xml.currencyCode options[:currency] || currency(money) xml.invNum options[:order_id] || SecureRandom.hex(25) end def add_recurring(xml, options) xml.recurringPayment options[:recurring_payment] end def parse(body) results = {} xml = Nokogiri::XML(body) resp = xml.xpath('//XMLResponse/XMLTrans') resp.children.each do |element| results[element.name.underscore.downcase.to_sym] = element.text end results end def commit(parameters) url = (test? ? test_url : live_url) response = parse(ssl_post(url, parameters)) Response.new( success_from(response), message_from(response), response, authorization: authorization_from(response), avs_result: AVSResult.new(code: response[:avs]), cvv_result: CVVResult.new(response[:cvv2_resp]), test: test?, error_code: error_code_from(response) ) end def success_from(response) response[:status] == '00' end def message_from(response) return 'Success' if success_from(response) message = STATUS_RESPONSE_CODES[response[:status]] message += " - #{TRANSACTION_RESPONSE_CODES[response[:response_code]]}" if response[:response_code] message end def authorization_from(response) response[:trans_num] end def error_code_from(response) response[:status] unless success_from(response) end def build_xml_request builder = Nokogiri::XML::Builder.new do |xml| xml.XMLRequest do xml.certStr @options[:cert_str] xml.class_ 'partner' xml.XMLTrans do yield(xml) end end end builder.to_xml end end def underscore(camel_cased_word) camel_cased_word.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2'). gsub(/([a-z\d])([A-Z])/, '\1_\2'). tr('-', '_'). downcase end end end