Skip to content

Commit

Permalink
Add Gateway for Moneris US
Browse files Browse the repository at this point in the history
Fix unit tests for Moneris US XML

Moneris US cannot void a preauthorization
Bring this in line with the way Moneris Canada does things.
  • Loading branch information
Michael Wood committed Mar 23, 2012
1 parent dd91818 commit 2faffc1
Show file tree
Hide file tree
Showing 6 changed files with 474 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -111,6 +111,7 @@ The [ActiveMerchant Wiki](http://github.com/Shopify/active_merchant/wikis) conta
* [MerchantWare](http://merchantwarehouse.com/merchantware) - US
* [Modern Payments](http://www.modpay.com) - US
* [Moneris](http://www.moneris.com/) - CA
* [Moneris US](http://www.monerisusa.com/) - US
* [NABTransact](http://www.nab.com.au/nabtransact/) - AU
* [Netaxept](http://www.betalingsterminal.no/Netthandel-forside) - NO, DK, SE, FI
* [NetRegistry](http://www.netregistry.com.au) - AU
Expand Down
211 changes: 211 additions & 0 deletions lib/active_merchant/billing/gateways/moneris_us.rb
@@ -0,0 +1,211 @@
require 'rexml/document'

module ActiveMerchant #:nodoc:
module Billing #:nodoc:

# To learn more about the Moneris (US) gateway, please contact
# ussales@moneris.com for a copy of their integration guide. For
# information on remote testing, please see "Test Environment Penny Value
# Response Table", and "Test Environment eFraud (AVS and CVD) Penny
# Response Values", available at Moneris' {eSelect Plus Documentation
# Centre}[https://www3.moneris.com/connect/en/documents/index.html].
class MonerisUsGateway < Gateway
TEST_URL = 'https://esplusqa.moneris.com/gateway_us/servlet/MpgRequest'
LIVE_URL = 'https://esplus.moneris.com/gateway_us/servlet/MpgRequest'

self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :diners_club, :discover]
self.homepage_url = 'http://www.monerisusa.com/'
self.display_name = 'Moneris (US)'

# login is your Store ID
# password is your API Token
def initialize(options = {})
requires!(options, :login, :password)
@options = { :crypt_type => 7 }.update(options)
super
end

# Referred to as "PreAuth" in the Moneris integration guide, this action
# verifies and locks funds on a customer's card, which then must be
# captured at a later date.
#
# Pass in +order_id+ and optionally a +customer+ parameter.
def authorize(money, creditcard, options = {})
debit_commit 'us_preauth', money, creditcard, options
end

# This action verifies funding on a customer's card, and readies them for
# deposit in a merchant's account.
#
# Pass in <tt>order_id</tt> and optionally a <tt>customer</tt> parameter
def purchase(money, creditcard, options = {})
debit_commit 'us_purchase', money, creditcard, options
end

# This method retrieves locked funds from a customer's account (from a
# PreAuth) and prepares them for deposit in a merchant's account.
#
# Note: Moneris requires both the order_id and the transaction number of
# the original authorization. To maintain the same interface as the other
# gateways the two numbers are concatenated together with a ; separator as
# the authorization number returned by authorization
def capture(money, authorization, options = {})
commit 'us_completion', crediting_params(authorization, :comp_amount => amount(money))
end

# Voiding requires the original transaction ID and order ID of some open
# transaction. Closed transactions must be refunded. Note that the only
# methods which may be voided are +capture+ and +purchase+.
#
# Concatenate your transaction number and order_id by using a semicolon
# (';'). This is to keep the Moneris interface consistent with other
# gateways. (See +capture+ for details.)
def void(authorization, options = {})
commit 'us_purchasecorrection', crediting_params(authorization)
end

# Performs a refund. This method requires that the original transaction
# number and order number be included. Concatenate your transaction
# number and order_id by using a semicolon (';'). This is to keep the
# Moneris interface consistent with other gateways. (See +capture+ for
# details.)
def credit(money, authorization, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end

def refund(money, authorization, options = {})
commit 'us_refund', crediting_params(authorization, :amount => amount(money))
end

def test?
@options[:test] || super
end
private # :nodoc: all

def expdate(creditcard)
sprintf("%.4i", creditcard.year)[-2..-1] + sprintf("%.2i", creditcard.month)
end

def debit_commit(commit_type, money, creditcard, options)
requires!(options, :order_id)
commit(commit_type, debit_params(money, creditcard, options))
end

# Common params used amongst the +purchase+ and +authorization+ methods
def debit_params(money, creditcard, options = {})
{
:order_id => options[:order_id],
:cust_id => options[:customer],
:amount => amount(money),
:pan => creditcard.number,
:expdate => expdate(creditcard),
:crypt_type => options[:crypt_type] || @options[:crypt_type]
}
end

# Common params used amongst the +credit+, +void+ and +capture+ methods
def crediting_params(authorization, options = {})
{
:txn_number => split_authorization(authorization).first,
:order_id => split_authorization(authorization).last,
:crypt_type => options[:crypt_type] || @options[:crypt_type]
}.merge(options)
end

# Splits an +authorization+ param and retrives the order id and
# transaction number in that order.
def split_authorization(authorization)
if authorization.nil? || authorization.empty? || authorization !~ /;/
raise ArgumentError, 'You must include a valid authorization code (e.g. "1234;567")'
else
authorization.split(';')
end
end

def commit(action, parameters = {})
response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, post_data(action, parameters)))

Response.new(successful?(response), message_from(response[:message]), response,
:test => test?,
:authorization => authorization_from(response)
)
end

# Generates a Moneris authorization string of the form 'trans_id;receipt_id'.
def authorization_from(response = {})
if response[:trans_id] && response[:receipt_id]
"#{response[:trans_id]};#{response[:receipt_id]}"
end
end

# Tests for a successful response from Moneris' servers
def successful?(response)
response[:response_code] &&
response[:complete] &&
(0..49).include?(response[:response_code].to_i)
end

def parse(xml)
response = { :message => "Global Error Receipt", :complete => false }
hashify_xml!(xml, response)
response
end

def hashify_xml!(xml, response)
xml = REXML::Document.new(xml)
return if xml.root.nil?
xml.elements.each('//receipt/*') do |node|
response[node.name.underscore.to_sym] = normalize(node.text)
end
end

def post_data(action, parameters = {})
xml = REXML::Document.new
root = xml.add_element("request")
root.add_element("store_id").text = options[:login]
root.add_element("api_token").text = options[:password]
transaction = root.add_element(action)

# Must add the elements in the correct order
actions[action].each do |key|
transaction.add_element(key.to_s).text = parameters[key] unless parameters[key].blank?
end

xml.to_s
end

def message_from(message)
return 'Unspecified error' if message.blank?
message.gsub(/[^\w]/, ' ').split.join(" ").capitalize
end

# Make a Ruby type out of the response string
def normalize(field)
case field
when "true" then true
when "false" then false
when '', "null" then nil
else field
end
end

def actions
{
"us_purchase" => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type],
"us_preauth" => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type],
"us_refund" => [:order_id, :amount, :txn_number, :crypt_type],
"us_ind_refund" => [:order_id, :cust_id, :amount, :pan, :expdate, :crypt_type],
"us_completion" => [:order_id, :comp_amount, :txn_number, :crypt_type],
"us_purchasecorrection" => [:order_id, :txn_number, :crypt_type],
"us_cavv_purchase" => [:order_id, :cust_id, :amount, :pan, :expdate, :cavv],
"us_cavv_preauth" => [:order_id, :cust_id, :amount, :pan, :expdate, :cavv],
"us_batchcloseall" => [],
"us_opentotals" => [:ecr_number],
"us_batchclose" => [:ecr_number]
}
end
end
end
end
4 changes: 4 additions & 0 deletions test/fixtures.yml
Expand Up @@ -168,6 +168,10 @@ moneris:
login: store1
password: yesguy

moneris_us:
login: monusqa002
password: qatoken

#Working credentials, no need to replace
nab_transact:
login: ABC0001
Expand Down
84 changes: 84 additions & 0 deletions test/remote/gateways/remote_moneris_us_test.rb
@@ -0,0 +1,84 @@
require 'test_helper'

class MonerisUsRemoteTest < Test::Unit::TestCase
def setup
Base.mode = :test

@gateway = MonerisUsGateway.new(fixtures(:moneris_us))
@amount = 100
@credit_card = credit_card('4242424242424242')
@options = {
:order_id => generate_unique_id,
:billing_address => address,
:description => 'Store Purchase'
}
end

def test_successful_purchase
assert response = @gateway.purchase(@amount, @credit_card, @options)
assert_success response
assert_equal 'Approved', response.message
assert_false response.authorization.blank?
end

def test_successful_authorization
response = @gateway.authorize(@amount, @credit_card, @options)
assert_success response
assert_false response.authorization.blank?
end

def test_failed_authorization
response = @gateway.authorize(105, @credit_card, @options)
assert_failure response
end

def test_successful_authorization_and_capture
response = @gateway.authorize(@amount, @credit_card, @options)
assert_success response
assert response.authorization

response = @gateway.capture(@amount, response.authorization)
assert_success response
end

def test_successful_authorization_and_void
response = @gateway.authorize(@amount, @credit_card, @options)
assert_success response
assert response.authorization

# Moneris cannot void a preauthorization
# You must capture the auth transaction with an amount of $0.00
void = @gateway.capture(0, response.authorization)
assert_success void
end

def test_successful_purchase_and_void
purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase

void = @gateway.void(purchase.authorization)
assert_success void
end

def test_failed_purchase_and_void
purchase = @gateway.purchase(101, @credit_card, @options)
assert_failure purchase

void = @gateway.void(purchase.authorization)
assert_failure void
end

def test_successful_purchase_and_credit
purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase

credit = @gateway.credit(@amount, purchase.authorization)
assert_success credit
end

def test_failed_purchase_from_error
assert response = @gateway.purchase(150, @credit_card, @options)
assert_failure response
assert_equal 'Declined', response.message
end
end
3 changes: 2 additions & 1 deletion test/unit/base_test.rb
Expand Up @@ -11,7 +11,8 @@ def teardown

def test_should_return_a_new_gateway_specified_by_symbol_name
assert_equal BogusGateway, Base.gateway(:bogus)
assert_equal MonerisGateway, Base.gateway(:moneris)
assert_equal MonerisGateway, Base.gateway(:moneris)
assert_equal MonerisUsGateway, Base.gateway(:moneris_us)
assert_equal AuthorizeNetGateway, Base.gateway(:authorize_net)
assert_equal UsaEpayGateway, Base.gateway(:usa_epay)
assert_equal LinkpointGateway, Base.gateway(:linkpoint)
Expand Down

0 comments on commit 2faffc1

Please sign in to comment.