module ActiveMerchant #:nodoc: module Billing #:nodoc: # ActiveMerchant Implementation for Quantum Gateway XML Requester Service # Based on API Doc from 8/6/2009 # # Important Notes # * Support is included for a customer id via the :customer option, invoice number via :invoice option, invoice description via :merchant option and memo via :description option # * You can force email of receipt with :email_receipt => true # * You can force email of merchant receipt with :merchant_receipt => true # * You can exclude CVV with :ignore_cvv => true # * All transactions use dollar values. class QuantumGateway < Gateway LIVE_URL = 'https://secure.quantumgateway.com/cgi/xml_requester.php' # visa, master, american_express, discover self.supported_cardtypes = [:visa, :master, :american_express, :discover] self.supported_countries = ['US'] self.default_currency = 'USD' self.money_format = :dollars self.homepage_url = 'http://www.quantumgateway.com' self.display_name = 'Quantum Gateway' # These are the options that can be used when creating a new Quantum Gateway object. # # :login => Your Quantum Gateway Gateway ID # # :password => Your Quantum Gateway Vault Key or Restrict Key # # NOTE: For testing supply your test GatewayLogin and GatewayKey # # :email_receipt => true if you want a receipt sent to the customer (false be default) # # :merchant_receipt => true if you want to override receiving the merchant receipt # # :ignore_avs => true ignore both AVS and CVV verification # :ignore_cvv => true don't want to use CVV so continue processing even if CVV would have failed # def initialize(options = {}) requires!(options, :login, :password) @options = options super end # Should run against the test servers or not? def test? @options[:test] || Base.gateway_mode == :test end # Request an authorization for an amount from CyberSource # def authorize(money, creditcard, options = {}) setup_address_hash(options) commit(build_auth_request(money, creditcard, options), options ) end # Capture an authorization that has previously been requested def capture(money, authorization, options = {}) setup_address_hash(options) commit(build_capture_request(money, authorization, options), options) end # Purchase is an auth followed by a capture # You must supply an order_id in the options hash def purchase(money, creditcard, options = {}) setup_address_hash(options) commit(build_purchase_request(money, creditcard, options), options) end def void(identification, options = {}) commit(build_void_request(identification, options), options) end def refund(money, identification, options = {}) commit(build_credit_request(money, identification, options), options) end def credit(money, identification, options = {}) deprecated CREDIT_DEPRECATION_MESSAGE refund(money, identification, options) end private def setup_address_hash(options) options[:billing_address] = options[:billing_address] || options[:address] || {} end def build_auth_request(money, creditcard, options) xml = Builder::XmlMarkup.new add_common_credit_card_info(xml,'AUTH_ONLY') add_purchase_data(xml, money) add_creditcard(xml, creditcard) add_address(xml, creditcard, options[:billing_address], options) add_invoice_details(xml, options) add_customer_details(xml, options) add_memo(xml, options) add_business_rules_data(xml) xml.target! end def build_capture_request(money, authorization, options) xml = Builder::XmlMarkup.new add_common_credit_card_info(xml,'PREVIOUS_SALE') transaction_id, _ = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) xml.target! end def build_purchase_request(money, creditcard, options) xml = Builder::XmlMarkup.new add_common_credit_card_info(xml, @options[:ignore_avs] || @options[:ignore_cvv] ? 'SALES' : 'AUTH_CAPTURE') add_address(xml, creditcard, options[:billing_address], options) add_purchase_data(xml, money) add_creditcard(xml, creditcard) add_invoice_details(xml, options) add_customer_details(xml, options) add_memo(xml, options) add_business_rules_data(xml) xml.target! end def build_void_request(authorization, options) xml = Builder::XmlMarkup.new add_common_credit_card_info(xml,'VOID') transaction_id, _ = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) xml.target! end def build_credit_request(money, authorization, options) xml = Builder::XmlMarkup.new add_common_credit_card_info(xml,'RETURN') add_purchase_data(xml, money) transaction_id, cc = authorization_parts_from(authorization) add_transaction_id(xml, transaction_id) xml.tag! 'CreditCardNumber', cc xml.target! end def add_common_credit_card_info(xml, process_type) xml.tag! 'RequestType', 'ProcessSingleTransaction' xml.tag! 'TransactionType', 'CREDIT' xml.tag! 'PaymentType', 'CC' xml.tag! 'ProcessType', process_type end def add_business_rules_data(xml) xml.tag!('CustomerEmail', @options[:email_receipt] ? 'Y' : 'N') xml.tag!('MerchantEmail', @options[:merchant_receipt] ? 'Y' : 'N') end def add_invoice_details(xml, options) xml.tag! 'InvoiceNumber', options[:invoice] xml.tag! 'InvoiceDescription', options[:merchant] end def add_customer_details(xml, options) xml.tag! 'CustomerID', options[:customer] end def add_transaction_id(xml, transaction_id) xml.tag! 'TransactionID', transaction_id end def add_memo(xml, options) xml.tag! 'Memo', options[:description] end def add_purchase_data(xml, money = 0) xml.tag! 'Amount', amount(money) xml.tag! 'TransactionDate', Time.now end def add_address(xml, creditcard, address, options, shipTo = false) xml.tag! 'FirstName', creditcard.first_name xml.tag! 'LastName', creditcard.last_name xml.tag! 'Address', address[:address1] # => there is no support for address2 in quantum xml.tag! 'City', address[:city] xml.tag! 'State', address[:state] xml.tag! 'ZipCode', address[:zip] xml.tag! 'Country', address[:country] xml.tag! 'EmailAddress', options[:email] xml.tag! 'IPAddress', options[:ip] end def add_creditcard(xml, creditcard) xml.tag! 'PaymentType', 'CC' xml.tag! 'CreditCardNumber', creditcard.number xml.tag! 'ExpireMonth', format(creditcard.month, :two_digits) xml.tag! 'ExpireYear', format(creditcard.year, :four_digits) xml.tag!('CVV2', creditcard.verification_value) unless (@options[:ignore_cvv] || creditcard.verification_value.blank? ) end # Where we actually build the full SOAP request using builder def build_request(body, options) xml = Builder::XmlMarkup.new xml.instruct! xml.tag! 'QGWRequest' do xml.tag! 'Authentication' do xml.tag! 'GatewayLogin', @options[:login] xml.tag! 'GatewayKey', @options[:password] end xml.tag! 'Request' do xml << body end end xml.target! end # Contact CyberSource, make the SOAP request, and parse the reply into a Response object def commit(request, options) headers = { 'Content-Type' => 'text/xml' } response = parse(ssl_post(LIVE_URL, build_request(request, options), headers)) success = response[:request_status] == "Success" message = response[:request_message] if success # => checking for connectivity success first success = %w(APPROVED FORCED VOIDED).include?(response[:Status]) message = response[:StatusDescription] authorization = success ? authorization_for(response) : nil end Response.new(success, message, response, :test => test?, :authorization => authorization, :avs_result => { :code => response[:AVSResponseCode] }, :cvv_result => response[:CVV2ResponseCode] ) end # Parse the SOAP response # Technique inspired by the Paypal Gateway def parse(xml) reply = {} begin xml = REXML::Document.new(xml) root = REXML::XPath.first(xml, "//QGWRequest/ResponseSummary") parse_element(reply, root) reply[:request_status] = reply[:Status] reply[:request_message] = "#{reply[:Status]}: #{reply[:StatusDescription]}" if root = REXML::XPath.first(xml, "//QGWRequest/Result") root.elements.to_a.each do |node| parse_element(reply, node) end end rescue Exception => e reply[:request_status] = 'Failure' reply[:request_message] = "Failure: There was a problem parsing the response XML" end return reply end def parse_element(reply, node) if node.has_elements? node.elements.each{|e| parse_element(reply, e) } else if node.parent.name =~ /item/ parent = node.parent.name + (node.parent.attributes["id"] ? "_" + node.parent.attributes["id"] : '') reply[(parent + '_' + node.name).to_sym] = node.text else reply[node.name.to_sym] = node.text end end return reply end def authorization_for(reply) "#{reply[:TransactionID]};#{reply[:CreditCardNumber]}" end def authorization_parts_from(authorization) authorization.split(/;/) end end end end