# Author:: MoneySpyder, http://moneyspyder.co.uk module ActiveMerchant module Billing # ActiveMerchant Datacash Gateway # # Datacash allows a policy for CV2 checks. There is currently no way # to modify this programatically. The policy may be changed in the # add_credit_card method. # class DataCashGateway < Gateway # Datacash server URLs TEST_URL = 'https://testserver.datacash.com/Transaction' LIVE_URL = 'https://mars.transaction.datacash.com/Transaction' # Different Card Transaction Types AUTH_TYPE = 'auth' CANCEL_TYPE = 'cancel' FULFILL_TYPE = 'fulfill' PRE_TYPE = 'pre' # Constant strings for use in the ExtendedPolicy complex element for # CV2 checks POLICY_ACCEPT = 'accept' POLICY_REJECT = 'reject' #Datacash success code DATACASH_SUCCESS = '1' # Class attributes attr_reader :url attr_reader :response attr_reader :options # Create a new DataCashGateway # # The gateway requires that a valid :login and :password be passed # in the options hash # # Parameters: # -options: # :login - the Datacash account login # :password - the Datacash account password # :test - boolean, use the test or live Datacash url # def initialize(options = {}) requires!(options, :login, :password) @options = options super end # Purchase the item straight away # # Parameters: # -money: Money object for the total to be charged # -credit_card: ActiveMerchant::Billing::CreditCard details for the transaction # -options: # # Returns: # -ActiveRecord::Billing::Response object # def purchase(money, credit_card, options = {}) if result = test_result_from_cc_number(credit_card.number) return result end request = build_purchase_or_authorization_request(AUTH_TYPE, money, credit_card, options) commit(request) end # Authorize the transaction # # Reserves the funds on the customer's credit card, but does not # charge the card. # # Parameters: # -money: Money object for the total to be charged # -credit_card: ActiveMerchant::Billing::CreditCard details for the transaction # -options: # # Returns: # -ActiveRecord::Billing::Response object # def authorize(money, credit_card, options = {}) if result = test_result_from_cc_number(credit_card.number) return result end request = build_purchase_or_authorization_request(PRE_TYPE, money, credit_card, options) commit(request) end # Datacash requires both the reference and the authcode of the original # authorization. To maintain the same interface as the other # gateways the two numbers are concatenated together with an ; separator as # the authorization number returned by authorization # Captures the funds from an authorized transaction. # authorization must be a valid Datacash reference and :authcode must be # a valid Datacash authcode from a prior authorized transaction. # # This needs to create a 'historic txn' to fulfill # # Parameters: # -money: Money object for the total to be charged # -authorization: the Datacash reference and authcode from the previous authorization # # Returns: # -ActiveRecord::Billing::Response object # def capture(money, authorization, options = {}) request = build_void_or_capture_request(FULFILL_TYPE, money, authorization, options) commit(request) end # Void a previous transaction # # This needs to create a 'historic txn' to fulfil # # Parameters: # -authorization: the Datacash reference from the previous authorization # # Returns: # -ActiveRecord::Billing::Response object # def void(authorization, options = {}) request = build_void_or_capture_request(CANCEL_TYPE, nil, authorization, options) commit(request) end # From the DataCash docs; Page 13, the following cards are # usable # # American Express, ATM, Carte Blanche, Diners Club, Discover, # EnRoute, GE Capital, JCB, Laser, Maestro, Mastercard, Solo, # Switch, Unknown, Visa, Visa Delta, VISA Electron, Visa Purchasing # # Parameters: # -none # # Returns: # -the list of all supported cards # def self.supported_cardtypes [ :visa, :master, :american_express, :discover, :diners_club, :jcb, :switch, :solo ] end # Return whether or not the gateway is in test mode # # Parameters: # -none # # Returns: # -boolean # def test? @options[:test] || Base.gateway_mode == :test end private # Create the xml document for a 'cancel' or 'fulfill' transaction. # # Final XML should look like: # # # 99000001 # ****** # # # # 25.00 # # # 4900200000000001 # A6 # fulfill # # # # # Parameters: # -type must be FULFILL_TYPE or CANCEL_TYPE # -money - optional - A money object with the price and currency # -authorization - the Datacash reference number from a previous # succesful authorize transaction # -authcode - the Datacash authcode # -order_id - The merchants reference # # Returns: # -Builder xml document # def build_void_or_capture_request(type, money, authorization, options) reference, auth_code = authorization.to_s.split(';') xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request do add_authentication(xml) xml.tag! :Transaction do xml.tag! :HistoricTxn do xml.tag! :reference, reference xml.tag! :authcode, auth_code xml.tag! :method, type end if money xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) xml.tag! :amount, amount(money), :currency => currency(money) end end end end xml.target! end # Create the xml document for an 'auth' or 'pre' transaction. # # Final XML should look like: # # # # 99000000 # ******* # # # # 123456 # 10.00 # # # # 4444********1111 # 03/04 # # Flat 7 # 89 Jumble # Street # Mytown # AV12FR # 123 # # # # # # # # auth, # # # # # Parameters: # -type must be 'auth' or 'pre' # -money - A money object with the price and currency # -credit_card - The credit_card details to use # -options: # :order_id is the merchant reference number # :billing_address is the billing address for the cc # :address is the delivery address # # Returns: # -xml: Builder document containing the markup # def build_purchase_or_authorization_request(type, money, credit_card, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request do add_authentication(xml) xml.tag! :Transaction do xml.tag! :CardTxn do xml.tag! :method, type add_credit_card(xml, credit_card, options[:billing_address] || options[:address]) end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) xml.tag! :amount, amount(money), :currency => currency(money) end end end xml.target! end # Adds the authentication element to the passed builder xml doc # # Parameters: # -xml: Builder document that is being built up # # Returns: # -none: The results is stored in the passed xml document # def add_authentication(xml) xml.tag! :Authentication do xml.tag! :client, @options[:login] xml.tag! :password, @options[:password] end end # Add credit_card detals to the passed XML Builder doc # # Parameters: # -xml: Builder document that is being built up # -credit_card: ActiveMerchant::Billing::CreditCard object # -billing_address: Hash containing all of the billing address details # # Returns: # -none: The results is stored in the passed xml document # def add_credit_card(xml, credit_card, address) xml.tag! :Card do # DataCash calls the CC number 'pan' xml.tag! :pan, credit_card.number xml.tag! :expirydate, format_date(credit_card.month, credit_card.year) # optional values - for Solo etc if [ 'switch', 'solo' ].include?(credit_card.type.to_s) xml.tag! :issuenumber, credit_card.issue_number unless credit_card.issue_number.blank? if !credit_card.start_month.blank? && !credit_card.start_year.blank? xml.tag! :startdate, format_date(credit_card.start_month, credit_card.start_year) end end xml.tag! :Cv2Avs do xml.tag! :cv2, credit_card.verification_value if credit_card.verification_value? xml.tag! :street_address1, address[:address1] unless address[:address1].blank? xml.tag! :street_address2, address[:address2] unless address[:address2].blank? xml.tag! :street_address3, address[:address3] unless address[:address3].blank? xml.tag! :street_address4, address[:address4] unless address[:address4].blank? xml.tag! :postcode, address[:zip] unless address[:zip].blank? # The ExtendedPolicy defines what to do when the passed data # matches, or not... # # All of the following elements MUST be present for the # xml to be valid (or can drop the ExtendedPolicy and use # a predefined one xml.tag! :ExtendedPolicy do xml.tag! :cv2_policy, :notprovided => POLICY_REJECT, :notchecked => POLICY_REJECT, :matched => POLICY_ACCEPT, :notmatched => POLICY_REJECT, :partialmatch => POLICY_REJECT xml.tag! :postcode_policy, :notprovided => POLICY_ACCEPT, :notchecked => POLICY_ACCEPT, :matched => POLICY_ACCEPT, :notmatched => POLICY_REJECT, :partialmatch => POLICY_ACCEPT xml.tag! :address_policy, :notprovided => POLICY_ACCEPT, :notchecked => POLICY_ACCEPT, :matched => POLICY_ACCEPT, :notmatched => POLICY_REJECT, :partialmatch => POLICY_ACCEPT end end end end # Send the passed data to DataCash for processing # # Parameters: # -request: The XML data that is to be sent to Datacash # # Returns: # - ActiveMerchant::Billing::Response object # def commit(request) url = test? ? TEST_URL : LIVE_URL @response = parse(ssl_post(url, request)) success = @response[:status] == DATACASH_SUCCESS message = @response[:reason] Response.new(success, message, @response, :test => test?, :authorization => "#{@response[:datacash_reference]};#{@response[:authcode]}" ) end # Find the currency of the Money object passed # # Parameters: # -money: The money object that we are looking at # # Returns: # -string: The three digit currency code (These are # ISO 4217 codes) # def currency(money) money.respond_to?(:currency) ? money.currency : 'GBP' end # Returns a date string in the format Datacash expects # # Parameters: # -month: integer, the month # -year: integer, the year # # Returns: # -String: date in MM/YY format # def format_date(month, year) "#{format(month,:two_digits)}/#{format(year, :two_digits)}" end # Parse the datacash response and create a Response object # # Parameters: # -body: The XML returned from Datacash # # Returns: # -a hash with all of the values returned in the Datacash XML response # def parse(body) response = {} xml = REXML::Document.new(body) root = REXML::XPath.first(xml, "//Response") root.elements.to_a.each do |node| parse_element(response, node) end response end # Parse an xml element # # Parameters: # -response: The hash that the values are being returned in # -node: The node that is currently being read # # Returns: # - none (results are stored in the passed hash) def parse_element(response, node) if node.has_elements? node.elements.each{|e| parse_element(response, e) } else response[node.name.underscore.to_sym] = node.text end end def format_reference_number(number) number.to_s.gsub(/[^A-Za-z0-9]/, '').rjust(6, "0") end end end end