Browse files

Add Quantum Gateway.

  • Loading branch information...
1 parent 063f342 commit 8e6ce67d68226a37da6026e98c9daa1ae998509a @ntalbott ntalbott committed Jan 14, 2011
View
1 CHANGELOG
@@ -1,5 +1,6 @@
= ActiveMerchant CHANGELOG
+* Add QuantumGateway [Joshua Lippiner]
* SagePayForm: Added send_email_confirmation (default false) to enable confirmation emails [wisq]
== Version 1.9.4 (Jan 5, 2011)
View
4 CONTRIBUTORS
@@ -179,3 +179,7 @@ iDEAL/Rabobank Gateway (January 10, 2011)
* Original code by Soemirno Kartosoewito
* Refactored by Cody Fauser
* Refactored and updated by Jonathan Rudenberg
+
+Quantum Gateway
+
+* Joshua Lippiner
View
1 README.rdoc
@@ -53,6 +53,7 @@ The {ActiveMerchant Wiki}[http://github.com/Shopify/active_merchant/wikis] conta
* {Protx}[http://www.protx.com] - GB
* {Psigate}[http://www.psigate.com/] - CA
* {PSL Payment Solutions}[http://www.paymentsolutionsltd.com/] - GB
+* {Quantum Gateway}[http://www.quantumgateway.com] - US
* {Quickpay}[http://quickpay.dk/] - DK
* {Rabobank Nederland}[http://www.rabobank.nl/] - NL
* {Realex}[http://www.realexpayments.com/] - IE, GB
View
277 lib/active_merchant/billing/gateways/quantum.rb
@@ -0,0 +1,277 @@
+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 credit(money, identification, options = {})
+ commit(build_credit_request(money, identification, options), 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
View
4 test/fixtures.yml
@@ -249,6 +249,10 @@ psl_visa_debit_address:
quickpay:
login: 89898989
password: "n5KrR5e2538awi9hUk6728LHTQ6E4uG1z4IFSb2pPN9j76DqJ2vA4698X315M1cd"
+
+quantum:
+ login: X
+ password: Y
realex:
login: X
View
75 test/remote/gateways/remote_quantum_test.rb
@@ -0,0 +1,75 @@
+require 'test_helper'
+
+class RemoteQuantumTest < Test::Unit::TestCase
+
+
+ def setup
+ @gateway = QuantumGateway.new(fixtures(:quantum))
+
+ @amount = 100
+ @credit_card = credit_card('4000100011112224')
+ end
+
+ def test_successful_purchase
+ assert response = @gateway.purchase(@amount, @credit_card)
+ assert_success response
+ assert_equal 'Transaction is APPROVED', response.message
+ end
+
+ def test_unsuccessful_purchase
+ assert response = @gateway.purchase(1, @credit_card)
+ assert_failure response
+ assert_equal 'Transaction is DECLINED', response.message
+ end
+
+ def test_authorize_and_capture
+ amount = @amount
+ assert auth = @gateway.authorize(amount, @credit_card)
+ assert_success auth
+ assert_equal 'Transaction is APPROVED', auth.message
+ assert auth.authorization
+ assert capture = @gateway.capture(amount, auth.authorization)
+ assert_success capture
+ end
+
+ def test_failed_capture
+ assert response = @gateway.capture(@amount, '')
+ assert_failure response
+ assert_equal 'ERROR: TransactionID not found', response.message
+ end
+
+ def test_credit
+ assert response = @gateway.purchase(@amount, @credit_card)
+ assert_success response
+
+ assert response = @gateway.credit(@amount, response.authorization)
+ assert_success response
+ end
+
+ def test_void
+ assert response = @gateway.purchase(@amount, @credit_card)
+ assert_success response
+
+ assert response = @gateway.void(response.authorization)
+ assert_success response
+ end
+
+ def test_passing_billing_address
+ options = {:billing_address => address}
+ assert response = @gateway.purchase(@amount, @credit_card, options)
+ assert_success response
+ assert_equal 'Transaction is APPROVED', response.message
+ end
+
+ # For some reason, Quantum Gateway currently returns an HTML response if the login is invalid
+ # So we check to see if the parse failed and report
+ def test_invalid_login
+ gateway = QuantumGateway.new(
+ :login => '',
+ :password => ''
+ )
+ assert response = gateway.purchase(@amount, @credit_card)
+ assert_failure response
+ assert_equal 'ERROR: Invalid Gateway Login!!', response.message
+ end
+end
View
3 test/test_helper.rb
@@ -33,6 +33,9 @@
ActiveMerchant::Billing::Base.mode = :test
+require 'logger'
+ActiveMerchant::Billing::Gateway.logger = Logger.new(STDOUT) if ENV['DEBUG_ACTIVE_MERCHANT'] == 'true'
+
# Test gateways
class SimpleTestGateway < ActiveMerchant::Billing::Gateway
end
View
114 test/unit/gateways/quantum_test.rb
@@ -0,0 +1,114 @@
+require 'test_helper'
+
+class QuantumTest < Test::Unit::TestCase
+ def setup
+ @gateway = QuantumGateway.new(
+ :login => '',
+ :password => ''
+ )
+
+ @credit_card = credit_card
+ @amount = 100
+
+ @options = {
+ :billing_address => address,
+ :description => 'Store Purchase'
+ }
+ end
+
+ def test_successful_purchase
+ @gateway.expects(:ssl_post).returns(successful_purchase_response)
+
+ assert response = @gateway.purchase(@amount, @credit_card, @options)
+ assert_success response
+
+ # Replace with authorization number from the successful response
+ assert_equal '2983691;2224', response.authorization
+ assert response.test?
+ end
+
+ def test_unsuccessful_request
+ @gateway.expects(:ssl_post).returns(failed_purchase_response)
+
+ assert response = @gateway.purchase(@amount, @credit_card, @options)
+ assert_failure response
+ assert response.test?
+ end
+
+ private
+
+ # Place raw successful response from gateway here
+ def successful_purchase_response
+ %(<QGWRequest>
+ <ResponseSummary>
+ <RequestType>ProcessSingleTransaction</RequestType>
+ <Status>Success</Status>
+ <StatusDescription>Request was successful.</StatusDescription>
+ <ResultCount>1</ResultCount>
+ <TimeStamp>2011-01-14 16:41:38</TimeStamp>
+ </ResponseSummary>
+ <Result>
+ <TransactionID>2983691</TransactionID>
+ <Status>APPROVED</Status>
+ <StatusDescription>Transaction is APPROVED</StatusDescription>
+ <CustomerID></CustomerID>
+ <TransactionType>CREDIT</TransactionType>
+ <FirstName>Longbob</FirstName>
+ <LastName>Longsen</LastName>
+ <Address>1234 My Street</Address>
+ <ZipCode>K1C2N6</ZipCode>
+ <City>Ottawa</City>
+ <State>ON</State>
+ <EmailAddress></EmailAddress>
+ <CreditCardNumber>2224</CreditCardNumber>
+ <ExpireMonth>09</ExpireMonth>
+ <ExpireYear>12</ExpireYear>
+ <Memo>Store Purchase</Memo>
+ <Amount>1.00</Amount>
+ <TransactionDate>2011-01-14</TransactionDate>
+ <PaymentType>CC</PaymentType>
+ <CardType>VI</CardType>
+ <AVSResponseCode>A</AVSResponseCode>
+ <AuthorizationCode>099557</AuthorizationCode>
+ <CVV2ResponseCode>N</CVV2ResponseCode>
+ </Result>
+ </QGWRequest>)
+ end
+
+ # Place raw failed response from gateway here
+ def failed_purchase_response
+ %(<QGWRequest>
+ <ResponseSummary>
+ <RequestType>ProcessSingleTransaction</RequestType>
+ <Status>Success</Status>
+ <StatusDescription>Request was successful.</StatusDescription>
+ <ResultCount>1</ResultCount>
+ <TimeStamp>2011-01-14 16:41:40</TimeStamp>
+ </ResponseSummary>
+ <Result>
+ <TransactionID>2983692</TransactionID>
+ <Status>DECLINED</Status>
+ <StatusDescription>Transaction is DECLINED</StatusDescription>
+ <CustomerID></CustomerID>
+ <TransactionType>CREDIT</TransactionType>
+ <FirstName>Longbob</FirstName>
+ <LastName>Longsen</LastName>
+ <Address>1234 My Street</Address>
+ <ZipCode>K1C2N6</ZipCode>
+ <EmailAddress></EmailAddress>
+ <CreditCardNumber>2224</CreditCardNumber>
+ <ExpireMonth>09</ExpireMonth>
+ <ExpireYear>12</ExpireYear>
+ <Memo>Store Purchase</Memo>
+ <Amount>0.01</Amount>
+ <TransactionDate>2011-01-14</TransactionDate>
+ <FailReason>AUTH DECLINED 200</FailReason>
+ <ErrorCode>200</ErrorCode>
+ <PaymentType>CC</PaymentType>
+ <CardType>VI</CardType>
+ <AVSResponseCode>Y</AVSResponseCode>
+ <CVV2ResponseCode>N</CVV2ResponseCode>
+ </Result>
+ </QGWRequest>)
+ end
+end

0 comments on commit 8e6ce67

Please sign in to comment.