require "active_support/core_ext/string/access" module ActiveMerchant module Billing class DataCashGateway < Gateway self.default_currency = 'GBP' self.supported_countries = ['GB'] self.supported_cardtypes = [ :visa, :master, :american_express, :discover, :diners_club, :jcb, :maestro, :switch, :solo, :laser ] self.homepage_url = 'http://www.datacash.com/' self.display_name = 'DataCash' self.test_url = 'https://testserver.datacash.com/Transaction' self.live_url = 'https://mars.transaction.datacash.com/Transaction' AUTH_TYPE = 'auth' CANCEL_TYPE = 'cancel' FULFILL_TYPE = 'fulfill' PRE_TYPE = 'pre' REFUND_TYPE = 'refund' TRANSACTION_REFUND_TYPE = 'txn_refund' POLICY_ACCEPT = 'accept' POLICY_REJECT = 'reject' def initialize(options = {}) requires!(options, :login, :password) super end def purchase(money, authorization_or_credit_card, options = {}) requires!(options, :order_id) if authorization_or_credit_card.is_a?(String) request = build_purchase_or_authorization_request_with_continuous_authority_reference_request(AUTH_TYPE, money, authorization_or_credit_card, options) else request = build_purchase_or_authorization_request_with_credit_card_request(AUTH_TYPE, money, authorization_or_credit_card, options) end commit(request) end def authorize(money, authorization_or_credit_card, options = {}) requires!(options, :order_id) if authorization_or_credit_card.is_a?(String) request = build_purchase_or_authorization_request_with_continuous_authority_reference_request(AUTH_TYPE, money, authorization_or_credit_card, options) else request = build_purchase_or_authorization_request_with_credit_card_request(PRE_TYPE, money, authorization_or_credit_card, options) end commit(request) end def capture(money, authorization, options = {}) commit(build_void_or_capture_request(FULFILL_TYPE, money, authorization, options)) end def void(authorization, options = {}) request = build_void_or_capture_request(CANCEL_TYPE, nil, authorization, options) commit(request) end def credit(money, reference_or_credit_card, options = {}) if reference_or_credit_card.is_a?(String) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE refund(money, reference_or_credit_card) else request = build_credit_request(money, reference_or_credit_card, options) commit(request) end end def refund(money, reference, options = {}) commit(build_transaction_refund_request(money, reference)) end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(/()\d+(<\/pan>)/i, '\1[FILTERED]\2'). gsub(/()\d+(<\/cv2>)/i, '\1[FILTERED]\2'). gsub(/().+(<\/password>)/i, '\1[FILTERED]\2') end private def build_void_or_capture_request(type, money, authorization, options) parsed_authorization = parse_authorization_string(authorization) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request, :version => '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :HistoricTxn do xml.tag! :reference, parsed_authorization[:reference] xml.tag! :authcode, parsed_authorization[: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 => options[:currency] || currency(money) xml.tag! :capturemethod, 'ecomm' end end end end xml.target! end def build_purchase_or_authorization_request_with_credit_card_request(type, money, credit_card, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request, :version => '2' do add_authentication(xml) xml.tag! :Transaction do if options[:set_up_continuous_authority] xml.tag! :ContAuthTxn, :type => 'setup' end xml.tag! :CardTxn do xml.tag! :method, type add_credit_card(xml, credit_card, options[:billing_address]) end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money) xml.tag! :capturemethod, 'ecomm' end end end xml.target! end def build_purchase_or_authorization_request_with_continuous_authority_reference_request(type, money, authorization, options) parsed_authorization = parse_authorization_string(authorization) raise ArgumentError, "The continuous authority reference is required for continuous authority transactions" if parsed_authorization[:ca_reference].blank? xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request, :version => '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :ContAuthTxn, :type => 'historic' xml.tag! :HistoricTxn do xml.tag! :reference, parsed_authorization[:ca_reference] xml.tag! :method, type end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) xml.tag! :amount, amount(money), :currency => options[:currency] || currency(money) xml.tag! :capturemethod, 'cont_auth' end end end xml.target! end def build_transaction_refund_request(money, authorization) parsed_authorization = parse_authorization_string(authorization) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request, :version => '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :HistoricTxn do xml.tag! :reference, parsed_authorization[:reference] xml.tag! :method, TRANSACTION_REFUND_TYPE end unless money.nil? xml.tag! :TxnDetails do xml.tag! :amount, amount(money) xml.tag! :capturemethod, 'ecomm' end end end end xml.target! end def build_credit_request(money, credit_card, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! :Request, :version => '2' do add_authentication(xml) xml.tag! :Transaction do xml.tag! :CardTxn do xml.tag! :method, REFUND_TYPE add_credit_card(xml, credit_card, options[:billing_address]) end xml.tag! :TxnDetails do xml.tag! :merchantreference, format_reference_number(options[:order_id]) xml.tag! :amount, amount(money) xml.tag! :capturemethod, 'ecomm' end end end xml.target! end def add_authentication(xml) xml.tag! :Authentication do xml.tag! :client, @options[:login] xml.tag! :password, @options[:password] end end 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?(card_brand(credit_card).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? if address 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? end # 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 def commit(request) response = parse(ssl_post(test? ? self.test_url : self.live_url, request)) Response.new(response[:status] == '1', response[:reason], response, :test => test?, :authorization => "#{response[:datacash_reference]};#{response[:authcode]};#{response[:ca_reference]}" ) end def format_date(month, year) "#{format(month,:two_digits)}/#{format(year, :two_digits)}" end 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 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").first(30) end def parse_authorization_string(authorization) reference, auth_code, ca_reference = authorization.to_s.split(';') {:reference => reference, :auth_code => auth_code, :ca_reference => ca_reference} end end end end