require 'digest/md5' module ActiveMerchant #:nodoc: module Billing #:nodoc: # This gateway accepts the following arguments: # :login => your PayJunction username # :password => your PayJunction pass # Example use: # # gateway = ActiveMerchant::Billing::Base.gateway(:pay_gate_xml).new( # :login => "my_account", # :password => "my_pass" # ) # # # set up credit card obj as in main ActiveMerchant example # creditcard = ActiveMerchant::Billing::CreditCard.new( # :type => 'visa', # :number => '4242424242424242', # :month => 8, # :year => 2009, # :first_name => 'Bob', # :last_name => 'Bobsen' # ) # # # run request # response = gateway.purchase(1000, creditcard) # charge 10 dollars # # 1) Check whether the transaction was successful # # response.success? # # 2) Retrieve the message returned by PayJunction # # response.message # # 3) Retrieve the unique transaction ID returned by PayGateXML # # response.authorization # # This gateway has many other features which are not implemented here yet # The basic setup here only supports auth/capture transactions. # # Test Transactions # # PayGateXML has a global test user/pass, but you can also sign up for your own. # The class and the test come equipped with the global test creds # # Usage Details # # Below is a map of only SOME of the values accepted by PayGateXML and how you should submit # each to ActiveMerchant # # PayGateXML Field ActiveMerchant Use # # pgid use :login value to gateway instantation # pwd use :password value to gateway instantiation # # cname credit_card.name # cc credit_card.number # exp credit_card values formatted to YYYYMMDD # budp South Africa only - set to 0 if purchase is not on budget # amt include as argument to method for your transaction type # ver do nothing, always set to current API version # # cref provide as :invoice in options, varchar(80) # cur 3 char field, currently only ZAR # cvv credit_card.verification # bno batch processing number, i.e. you supply this # # others -- not used in this implementation # nurl, rurl - must remain blank or absent or they will use an alternative authentication mechanism # email, ip - must remain blank or absent or they will use a PayGate extra service call PayProtector # threed - must remain blank unless you are using your own 3D Secure server # class PayGateXmlGateway < Gateway self.live_url = 'https://www.paygate.co.za/payxml/process.trans' # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['US', 'ZA'] # The card types supported by the payment gateway self.supported_cardtypes = [:visa, :master, :american_express, :diners_club] # The homepage URL of the gateway self.homepage_url = 'http://paygate.co.za/' # The name of the gateway self.display_name = 'PayGate PayXML' # PayGate only supports Rands self.default_currency = 'ZAR' # PayGate accepts only lowest denomination self.money_format = :cents # PayGateXML public test account - you can get a private one too TEST_ID_3DSECURE = '10011013800' TEST_ID = '10011021600' TEST_PWD = 'test' API_VERSION = '4.0' DECLINE_CODES = { # Credit Card Errors - These RESULT_CODEs are returned if the transaction cannot be authorized due to a problem with the card. The TRANSACTION_STATUS will be 2 900001 => "Call for Approval", 900002 => "Card Expired", 900003 => "Insufficient Funds", 900004 => "Invalid Card Number", 900005 => "Bank Interface Timeout", # indicates a communications failure between the banks systems 900006 => "Invalid Card", 900007 => "Declined", 900009 => "Lost Card", 900010 => "Invalid Card Length", 900011 => "Suspected Fraud", 900012 => "Card Reported As Stolen", 900013 => "Restricted Card", 900014 => "Excessive Card Usage", 900015 => "Card Blacklisted", 900207 => "Declined; authentication failed", # indicates the cardholder did not enter their MasterCard SecureCode / Verified by Visa password correctly 990020 => "Auth Declined", 991001 => "Invalid expiry date", 991002 => "Invalid amount", # Communication Errors - These RESULT_CODEs are returned if the transaction cannot be completed due to an unexpected error. TRANSACTION_STATUS will be 0. 900205 => "Unexpected authentication result (phase 1)", 900206 => "Unexpected authentication result (phase 1)", 990001 => "Could not insert into Database", 990022 => "Bank not available", 990053 => "Error processing transaction", # Miscellaneous - Unless otherwise noted, the TRANSACTION_STATUS will be 0. 900209 => "Transaction verification failed (phase 2)", # Indicates the verification data returned from MasterCard SecureCode / Verified by Visa has been altered 900210 => "Authentication complete; transaction must be restarted", # Indicates that the MasterCard SecuerCode / Verified by Visa transaction has already been completed. Most likely caused by the customer clicking the refresh button 990024 => "Duplicate Transaction Detected. Please check before submitting", 990028 => "Transaction cancelled" # Customer clicks the 'Cancel' button on the payment page } SUCCESS_CODES = %w( 990004 990005 990017 990012 990018 990031 ) TRANSACTION_CODES = { 0 => 'Not Done', 1 => 'Approved', 2 => 'Declined', 3 => 'Paid', 4 => 'Refunded', 5 => 'Received by PayGate', 6 => 'Replied to Client' } def initialize(options = {}) requires!(options, :login, :password) super end def purchase(money, creditcard, options = {}) MultiResponse.run do |r| r.process{authorize(money, creditcard, options)} r.process{capture(money, r.authorization, options)} end end def authorize(money, creditcard, options = {}) action = 'authtx' options.merge!(:money => money, :creditcard => creditcard) commit(action, build_request(action, options)) end def capture(money, authorization, options = {}) action = 'settletx' options.merge!(:money => money, :authorization => authorization) commit(action, build_request(action, options), authorization) end def refund(money, authorization, options={}) action = 'refundtx' options.merge!(:money => money, :authorization => authorization) commit(action, build_request(action, options)) end private def successful?(response) SUCCESS_CODES.include?(response[:res]) end def build_request(action, options={}) xml = Builder::XmlMarkup.new xml.instruct! xml.tag! 'protocol', :ver => API_VERSION, :pgid => (test? ? TEST_ID : @options[:login]), :pwd => @options[:password] do |protocol| money = options.delete(:money) authorization = options.delete(:authorization) creditcard = options.delete(:creditcard) case action when 'authtx' build_authorization(protocol, money, creditcard, options) when 'settletx' build_capture(protocol, money, authorization, options) when 'refundtx' build_refund(protocol, money, authorization, options) else raise "no action specified for build_request" end end xml.target! end def build_authorization(xml, money, creditcard, options={}) xml.tag! 'authtx', { :cref => options[:order_id], :cname => creditcard.name, :cc => creditcard.number, :exp => "#{format(creditcard.month, :two_digits)}#{format(creditcard.year, :four_digits)}", :budp => 0, :amt => amount(money), :cur => (options[:currency] || currency(money)), :cvv => creditcard.verification_value, :email => options[:email], :ip => options[:ip] } end def build_capture(xml, money, authorization, options={}) xml.tag! 'settletx', { :tid => authorization } end def build_refund(xml, money, authorization, options={}) xml.tag! 'refundtx', { :tid => authorization, :amt => amount(money) } end def parse(action, body) hash = {} xml = REXML::Document.new(body) response_action = action.gsub(/tx/, 'rx') root = REXML::XPath.first(xml.root, response_action) # we might have gotten an error if root.nil? root = REXML::XPath.first(xml.root, 'errorrx') end root.attributes.each do |name, value| hash[name.to_sym] = value end hash end def commit(action, request, authorization = nil) response = parse(action, ssl_post(self.live_url, request)) Response.new(successful?(response), message_from(response), response, :test => test?, :authorization => authorization ? authorization : response[:tid] ) end def message_from(response) (response[:rdesc] || response[:edesc]) end end end end