module ActiveMerchant #:nodoc: module Billing #:nodoc: class QbmsGateway < Gateway API_VERSION = '4.0' class_attribute :test_url, :live_url self.test_url = "https://webmerchantaccount.ptc.quickbooks.com/j/AppGateway" self.live_url = "https://webmerchantaccount.quickbooks.com/j/AppGateway" self.homepage_url = 'http://payments.intuit.com/' self.display_name = 'QuickBooks Merchant Services' self.default_currency = 'USD' self.supported_cardtypes = [ :visa, :master, :discover, :american_express, :diners_club, :jcb ] self.supported_countries = [ 'US' ] TYPES = { :authorize => 'CustomerCreditCardAuth', :capture => 'CustomerCreditCardCapture', :purchase => 'CustomerCreditCardCharge', :refund => 'CustomerCreditCardTxnVoidOrRefund', :void => 'CustomerCreditCardTxnVoid', :query => 'MerchantAccountQuery', } # Creates a new QbmsGateway # # The gateway requires that a valid app id, app login, and ticket be passed # in the +options+ hash. # # ==== Options # # * :login -- The App Login (REQUIRED) # * :ticket -- The Connection Ticket. (REQUIRED) # * :pem -- The PEM-encoded SSL client key and certificate. (REQUIRED) # * :test -- +true+ or +false+. If true, perform transactions against the test server. # Otherwise, perform transactions against the production server. # def initialize(options = {}) requires!(options, :login, :ticket) super end # Performs an authorization, which reserves the funds on the customer's credit card, but does not # charge the card. # # ==== Parameters # # * money -- The amount to be authorized as an Integer value in cents. # * creditcard -- The CreditCard details for the transaction. # * options -- A hash of optional parameters. # def authorize(money, creditcard, options = {}) commit(:authorize, money, options.merge(:credit_card => creditcard)) end # Perform a purchase, which is essentially an authorization and capture in a single operation. # # ==== Parameters # # * money -- The amount to be purchased as an Integer value in cents. # * creditcard -- The CreditCard details for the transaction. # * options -- A hash of optional parameters. # def purchase(money, creditcard, options = {}) commit(:purchase, money, options.merge(:credit_card => creditcard)) end # Captures the funds from an authorized transaction. # # ==== Parameters # # * money -- The amount to be captured as an Integer value in cents. # * authorization -- The authorization returned from the previous authorize request. # def capture(money, authorization, options = {}) commit(:capture, money, options.merge(:transaction_id => authorization)) end # Void a previous transaction # # ==== Parameters # # * authorization - The authorization returned from the previous authorize request. # def void(authorization, options = {}) commit(:void, nil, options.merge(:transaction_id => authorization)) end # Credit an account. # # This transaction is also referred to as a Refund and indicates to the gateway that # money should flow from the merchant to the customer. # # ==== Parameters # # * money -- The amount to be credited to the customer as an Integer value in cents. # * identification -- The ID of the original transaction against which the credit is being issued. # * options -- A hash of parameters. # # def credit(money, identification, options = {}) ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE refund(money, identification, options = {}) end def refund(money, identification, options = {}) commit(:refund, money, options.merge(:transaction_id => identification)) end # Query the merchant account status def query commit(:query, nil, {}) end def supports_scrubbing? true end def scrub(transcript) transcript. gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). gsub(%r(()[^<]*())i, '\1[FILTERED]\2'). gsub(%r(()[^<]*())i, '\1[FILTERED]\2') end private def hosted? @options[:pem] end def commit(action, money, parameters) url = test? ? self.test_url : self.live_url type = TYPES[action] parameters[:trans_request_id] ||= SecureRandom.hex(10) req = build_request(type, money, parameters) data = ssl_post(url, req, "Content-Type" => "application/x-qbmsxml") response = parse(type, data) message = (response[:status_message] || '').strip Response.new(success?(response), message, response, :test => test?, :authorization => response[:credit_card_trans_id], :fraud_review => fraud_review?(response), :avs_result => { :code => avs_result(response) }, :cvv_result => cvv_result(response) ) end def success?(response) response[:status_code] == 0 end def fraud_review?(response) [10100, 10101].member? response[:status_code] end def parse(type, body) xml = REXML::Document.new(body) signon = REXML::XPath.first(xml, "//SignonMsgsRs/#{hosted? ? 'SignonAppCertRs' : 'SignonDesktopRs'}") status_code = signon.attributes["statusCode"].to_i if status_code != 0 return { :status_code => status_code, :status_message => signon.attributes["statusMessage"], } end response = REXML::XPath.first(xml, "//QBMSXMLMsgsRs/#{type}Rs") results = { :status_code => response.attributes["statusCode"].to_i, :status_message => response.attributes["statusMessage"], } response.elements.each do |e| name = e.name.underscore.to_sym value = e.text() if old_value = results[name] results[name] = [old_value] if !old_value.kind_of?(Array) results[name] << value else results[name] = value end end results end def build_request(type, money, parameters = {}) xml = Builder::XmlMarkup.new(:indent => 0) xml.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') xml.instruct!(:qbmsxml, :version => API_VERSION) xml.tag!("QBMSXML") do xml.tag!("SignonMsgsRq") do xml.tag!(hosted? ? "SignonAppCertRq" : "SignonDesktopRq") do xml.tag!("ClientDateTime", Time.now.xmlschema) xml.tag!("ApplicationLogin", @options[:login]) xml.tag!("ConnectionTicket", @options[:ticket]) end end xml.tag!("QBMSXMLMsgsRq") do xml.tag!("#{type}Rq") do method("build_#{type}").call(xml, money, parameters) end end end xml.target! end def build_CustomerCreditCardAuth(xml, money, parameters) cc = parameters[:credit_card] name = "#{cc.first_name} #{cc.last_name}"[0...30] xml.tag!("TransRequestID", parameters[:trans_request_id]) xml.tag!("CreditCardNumber", cc.number) xml.tag!("ExpirationMonth", cc.month) xml.tag!("ExpirationYear", cc.year) xml.tag!("IsECommerce", "true") xml.tag!("Amount", amount(money)) xml.tag!("NameOnCard", name) add_address(xml, parameters) xml.tag!("CardSecurityCode", cc.verification_value) if cc.verification_value? end def build_CustomerCreditCardCapture(xml, money, parameters) xml.tag!("TransRequestID", parameters[:trans_request_id]) xml.tag!("CreditCardTransID", parameters[:transaction_id]) xml.tag!("Amount", amount(money)) end def build_CustomerCreditCardCharge(xml, money, parameters) cc = parameters[:credit_card] name = "#{cc.first_name} #{cc.last_name}"[0...30] xml.tag!("TransRequestID", parameters[:trans_request_id]) xml.tag!("CreditCardNumber", cc.number) xml.tag!("ExpirationMonth", cc.month) xml.tag!("ExpirationYear", cc.year) xml.tag!("IsECommerce", "true") xml.tag!("Amount", amount(money)) xml.tag!("NameOnCard", name) add_address(xml, parameters) xml.tag!("CardSecurityCode", cc.verification_value) if cc.verification_value? end def build_CustomerCreditCardTxnVoidOrRefund(xml, money, parameters) xml.tag!("TransRequestID", parameters[:trans_request_id]) xml.tag!("CreditCardTransID", parameters[:transaction_id]) xml.tag!("Amount", amount(money)) end def build_CustomerCreditCardTxnVoid(xml, money, parameters) xml.tag!("TransRequestID", parameters[:trans_request_id]) xml.tag!("CreditCardTransID", parameters[:transaction_id]) end # Called reflectively by build_request def build_MerchantAccountQuery(xml, money, parameters) end def add_address(xml, parameters) if address = parameters[:billing_address] || parameters[:address] xml.tag!("CreditCardAddress", (address[:address1] || "")[0...30]) xml.tag!("CreditCardPostalCode", (address[:zip] || "")[0...9]) end end def cvv_result(response) case response[:card_security_code_match] when "Pass" then 'M' when "Fail" then 'N' when "NotAvailable" then 'P' end end def avs_result(response) case "#{response[:avs_street]}|#{response[:avs_zip]}" when "Pass|Pass" then "D" when "Pass|Fail" then "A" when "Pass|NotAvailable" then "B" when "Fail|Pass" then "Z" when "Fail|Fail" then "C" when "Fail|NotAvailable" then "N" when "NotAvailable|Pass" then "P" when "NotAvailable|Fail" then "N" when "NotAvailable|NotAvailable" then "U" end end end end end