module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on the Metrics Global Payment Gateway, visit the {Metrics Global website}[www.metricsglobal.com].
# Further documentation on AVS and CVV response codes are available under the support section of the Metrics Global
# control panel.
#
# === Metrics Global Payment Gateway Authentication
#
# The login and password for the gateway are the same as the username and password used to log in to the Metrics Global
# control panel. Contact Metrics Global support to receive credentials for the control panel.
#
# === Demo Account
#
# There is a public demo account available with the following credentials:
#
# Login: demo
# Password: password
class MetricsGlobalGateway < Gateway
API_VERSION = '3.1'
class_attribute :test_url, :live_url
self.test_url = 'https://secure.metricsglobalgateway.com/gateway/transact.dll?testing=true'
self.live_url = 'https://secure.metricsglobalgateway.com/gateway/transact.dll'
class_attribute :duplicate_window
APPROVED, DECLINED, ERROR, FRAUD_REVIEW = 1, 2, 3, 4
RESPONSE_CODE, RESPONSE_REASON_CODE, RESPONSE_REASON_TEXT = 0, 2, 3
AVS_RESULT_CODE, TRANSACTION_ID, CARD_CODE_RESPONSE_CODE = 5, 6, 38
self.supported_countries = ['US']
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
self.homepage_url = 'http://www.metricsglobal.com'
self.display_name = 'Metrics Global'
CARD_CODE_ERRORS = %w(N S)
AVS_ERRORS = %w(A E N R W Z)
AVS_REASON_CODES = %w(27 45)
# Creates a new MetricsGlobalGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * :login -- The username required to access the Metrics Global control panel. (REQUIRED)
# * :password -- The password required to access the Metrics Global control panel. (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, :password)
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 = {})
post = {}
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
add_duplicate_window(post)
commit('AUTH_ONLY', money, post)
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 = {})
post = {}
add_invoice(post, options)
add_creditcard(post, creditcard)
add_address(post, options)
add_customer_data(post, options)
add_duplicate_window(post)
commit('AUTH_CAPTURE', money, post)
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 = {})
post = {:trans_id => authorization}
add_customer_data(post, options)
commit('PRIOR_AUTH_CAPTURE', money, post)
end
# Void a previous transaction
#
# ==== Parameters
#
# * authorization - The authorization returned from the previous authorize request.
def void(authorization, options = {})
post = {:trans_id => authorization}
add_duplicate_window(post)
commit('VOID', nil, post)
end
# Refund a transaction.
#
# This transaction 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 refund is being issued.
# * options -- A hash of parameters.
#
# ==== Options
#
# * :card_number -- The credit card number the refund is being issued to. (REQUIRED)
# * :first_name -- The first name of the account being refunded.
# * :last_name -- The last name of the account being refunded.
# * :zip -- The postal code of the account being refunded.
def refund(money, identification, options = {})
requires!(options, :card_number)
post = { :trans_id => identification,
:card_num => options[:card_number]}
post[:first_name] = options[:first_name] if options[:first_name]
post[:last_name] = options[:last_name] if options[:last_name]
post[:zip] = options[:zip] if options[:zip]
add_invoice(post, options)
add_duplicate_window(post)
commit('CREDIT', money, post)
end
def credit(money, identification, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
private
def commit(action, money, parameters)
parameters[:amount] = amount(money) unless action == 'VOID'
# Only activate the test_request when the :test option is passed in
parameters[:test_request] = @options[:test] ? 'TRUE' : 'FALSE'
url = test? ? self.test_url : self.live_url
data = ssl_post url, post_data(action, parameters)
response = parse(data)
message = message_from(response)
# Return the response. The authorization can be taken out of the transaction_id
# Test Mode on/off is something we have to parse from the response text.
# It usually looks something like this
#
# (TESTMODE) Successful Sale
test_mode = test? || message =~ /TESTMODE/
Response.new(success?(response), message, response,
:test => test_mode,
:authorization => response[:transaction_id],
:fraud_review => fraud_review?(response),
:avs_result => { :code => response[:avs_result_code] },
:cvv_result => response[:card_code]
)
end
def success?(response)
response[:response_code] == APPROVED
end
def fraud_review?(response)
response[:response_code] == FRAUD_REVIEW
end
def parse(body)
fields = split(body)
results = {
:response_code => fields[RESPONSE_CODE].to_i,
:response_reason_code => fields[RESPONSE_REASON_CODE],
:response_reason_text => fields[RESPONSE_REASON_TEXT],
:avs_result_code => fields[AVS_RESULT_CODE],
:transaction_id => fields[TRANSACTION_ID],
:card_code => fields[CARD_CODE_RESPONSE_CODE]
}
results
end
def post_data(action, parameters = {})
post = {}
post[:version] = API_VERSION
post[:login] = @options[:login]
post[:tran_key] = @options[:password]
post[:relay_response] = 'FALSE'
post[:type] = action
post[:delim_data] = 'TRUE'
post[:delim_char] = ','
post[:encap_char] = '$'
post[:solution_ID] = application_id if application_id
request = post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&')
request
end
def add_invoice(post, options)
post[:invoice_num] = options[:order_id]
post[:description] = options[:description]
end
def add_creditcard(post, creditcard)
post[:card_num] = creditcard.number
post[:card_code] = creditcard.verification_value if creditcard.verification_value?
post[:exp_date] = expdate(creditcard)
post[:first_name] = creditcard.first_name
post[:last_name] = creditcard.last_name
end
def add_customer_data(post, options)
if options.has_key? :email
post[:email] = options[:email]
post[:email_customer] = false
end
post[:cust_id] = options[:customer] if options.has_key? :customer
post[:customer_ip] = options[:ip] if options.has_key? :ip
end
# x_duplicate_window won't be sent by default, because sending it changes the response.
# "If this field is present in the request with or without a value, an enhanced duplicate transaction response will be sent."
def add_duplicate_window(post)
post[:duplicate_window] = duplicate_window unless duplicate_window.nil?
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:address] = address[:address1].to_s
post[:company] = address[:company].to_s
post[:phone] = address[:phone].to_s
post[:zip] = address[:zip].to_s
post[:city] = address[:city].to_s
post[:country] = address[:country].to_s
post[:state] = address[:state].blank? ? 'n/a' : address[:state]
end
if address = options[:shipping_address]
post[:ship_to_first_name] = address[:first_name].to_s
post[:ship_to_last_name] = address[:last_name].to_s
post[:ship_to_address] = address[:address1].to_s
post[:ship_to_company] = address[:company].to_s
post[:ship_to_phone] = address[:phone].to_s
post[:ship_to_zip] = address[:zip].to_s
post[:ship_to_city] = address[:city].to_s
post[:ship_to_country] = address[:country].to_s
post[:ship_to_state] = address[:state].blank? ? 'n/a' : address[:state]
end
end
def message_from(results)
if results[:response_code] == DECLINED
return CVVResult.messages[results[:card_code]] if CARD_CODE_ERRORS.include?(results[:card_code])
return AVSResult.messages[results[:avs_result_code]] if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code])
end
(results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '')
end
def split(response)
response[1..-2].split(/\$,\$/)
end
end
end
end