module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# === EVO Canada payment gateway.
#
# EVO returns two different identifiers for most transactions, the
# +authcode+ and the +transactionid+. Since +transactionid+ is used more
# often (i.e. for {#capture}, {#refund}, {#void} and {#update}) we store it in the
# Response#authorization attribute. The +authcode+ from the merchant
# account is accessible via {Response#params}.
#
# Two different but related response messages are also returned from EVO.
# The message indicated by EVO's response_code parameter is returned as
# {Response#message} (Those messages can be seen in the {MESSAGES} hash.)
# The other, shorter message is available via {Response#params}.
#
# It's recommended to save the contents of the {Response#params} in your
# transaction log for future reference.
#
# === Sample Use
#
# gateway = ActiveMerchant::Billing::EvoCaGateway.new(username: 'demo', password: 'password')
#
# response = gateway.authorize(1000, credit_card, options)
#
# puts response.authorization # the transactionid
# puts response.params['authcode'] # the authcode from the merchant account
# puts response.message # the 'pretty' response message
# puts response.params['responsetext'] # the 'terse' response message
#
# gateway.capture(1000, response.authorization)
# gateway.update(response.authorization, shipping_carrier: 'fedex')
# gateway.refund(500, response.authorization)
#
class EvoCaGateway < Gateway
self.test_url = 'https://secure.evoepay.com/api/transact.php'
self.live_url = 'https://secure.evoepay.com/api/transact.php'
self.supported_countries = ['CA']
self.supported_cardtypes = %i[visa master american_express jcb discover]
self.money_format = :dollars
self.homepage_url = 'http://www.evocanada.com/'
self.display_name = 'EVO Canada'
APPROVED, DECLINED, ERROR = 1, 2, 3
MESSAGES = {
100 => 'Transaction was approved',
200 => 'Transaction was declined by processor',
201 => 'Do not honor',
202 => 'Insufficient funds',
203 => 'Over limit',
204 => 'Transaction not allowed',
220 => 'Incorrect payment data',
221 => 'No such card issuer',
222 => 'No card number on file with issuer',
223 => 'Expired card',
224 => 'Invalid expiration date',
225 => 'Invalid card security code',
240 => 'Call issuer for futher information',
250 => 'Pick up card',
251 => 'Lost card',
252 => 'Stolen card',
253 => 'Fraudulant card',
260 => 'Declined with further instructions available',
261 => 'Declined - stop all recurring payments',
262 => 'Declined - stop this recurring program',
263 => 'Declined - updated cardholder data available',
264 => 'Declined - retry in a few days',
300 => 'Transaction was rejected by gateway',
400 => 'Transaction error returned by processor',
410 => 'Invalid merchant configuration',
411 => 'Merchant account is inactive',
420 => 'Communication error',
421 => 'Communication error with issuer',
430 => 'Duplicate transaction at processor',
440 => 'Processor format error',
441 => 'Invalid transaction information',
460 => 'Processor feature not available',
461 => 'Unsupported card type'
}
# This gateway requires that a valid username and password be passed
# in the +options+ hash.
#
# === Required Options
#
# * :username
# * :password
def initialize(options = {})
requires!(options, :username, :password)
super
end
# Transaction sales are submitted and immediately flagged for settlement.
# These transactions will automatically be settled.
#
# Payment source can be either a {CreditCard} or {Check}.
#
# === Additional Options
# In addition to the standard options, this gateway supports
#
# * :tracking_number - Shipping tracking number
# * :shipping_carrier - ups/fedex/dhl/usps
# * :po_number - Purchase order
# * :tax - Tax amount
# * :shipping - Shipping cost
def purchase(money, credit_card_or_check, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card_or_check)
add_address(post, options)
add_customer_data(post, options)
commit('sale', money, post)
end
# Transaction authorizations are authorized immediately but are not
# flagged for settlement. These transactions must be flagged for
# settlement using the _capture_ transaction type. Authorizations
# typically remain activate for three to seven business days.
#
# Payment source must be a {CreditCard}.
def authorize(money, credit_card, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card)
add_address(post, options)
add_customer_data(post, options)
commit('auth', money, post)
end
# Transaction captures flag existing _authorizations_ for settlement. Only
# authorizations can be captured. Captures can be submitted for an amount
# equal to or less than the original authorization.
#
# The authorization parameter is the transaction ID, retrieved
# from Response#authorization. See EvoCaGateway#purchase for the
# options.
def capture(money, authorization, options = {})
post = {
amount: amount(money),
transactionid: authorization
}
add_order(post, options)
commit('capture', money, post)
end
# Transaction refunds will reverse a previously settled transaction. If
# the transaction has not been settled, it must be _voided_ instead of
# refunded.
#
# The identification parameter is the transaction ID, retrieved
# from {Response#authorization}.
def refund(money, identification)
post = { transactionid: identification }
commit('refund', money, post)
end
# Transaction credits apply a negative amount to the cardholder's card.
# In most situations credits are disabled as transaction refunds should
# be used instead.
#
# Note that this is different from a {#refund} (which is usually what
# you'll be looking for).
def credit(money, credit_card, options = {})
post = {}
add_invoice(post, options)
add_order(post, options)
add_paymentmethod(post, credit_card)
add_address(post, options)
add_customer_data(post, options)
commit('credit', money, post)
end
# Transaction voids will cancel an existing sale or captured
# authorization. In addition, non-captured authorizations can be voided to
# prevent any future capture. Voids can only occur if the transaction has
# not been settled.
#
# The identification parameter is the transaction ID, retrieved
# from {Response#authorization}.
def void(identification)
post = { transactionid: identification }
commit('void', nil, post)
end
# Transaction updates can be used to update previous transactions with
# specific order information, such as a tracking number and shipping
# carrier. See EvoCaGateway#purchase for options.
#
# The identification parameter is the transaction ID, retrieved
# from {Response#authorization}.
def update(identification, options)
post = { transactionid: identification }
add_order(post, options)
commit('update', nil, post)
end
private
def add_customer_data(post, options)
post[:email] = options[:email]
post[:ipaddress] = options[:ip]
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:firstname] = address[:first_name]
post[:lastname] = address[:last_name]
post[:address1] = address[:address1]
post[:address2] = address[:address2]
post[:company] = address[:company]
post[:phone] = address[:phone]
post[:city] = address[:city]
post[:state] = address[:state]
post[:zip] = address[:zip]
post[:country] = address[:country]
end
if address = options[:shipping_address]
post[:shipping_firstname] = address[:first_name]
post[:shipping_lastname] = address[:last_name]
post[:shipping_address1] = address[:address1]
post[:shipping_address2] = address[:address2]
post[:shipping_company] = address[:company]
post[:shipping_zip] = address[:zip]
post[:shipping_city] = address[:city]
post[:shipping_state] = address[:state]
post[:shipping_country] = address[:country]
end
end
def add_order(post, options)
post[:orderid] = options[:order_id]
post[:tracking_number] = options[:tracking_number]
post[:shipping_carrier] = options[:shipping_carrier]
end
def add_invoice(post, options)
post[:orderdescription] = options[:description]
post[:ponumber] = options[:po_number]
post[:shipping] = amount(options[:shipping])
post[:tax] = amount(options[:tax])
end
def add_paymentmethod(post, payment)
if card_brand(payment) == 'check'
post[:payment] = 'check'
post[:checkname] = payment.name
post[:checkaba] = payment.routing_number
post[:checkaccount] = payment.account_number
post[:account_holder_type] = payment.account_holder_type
post[:account_type] = payment.account_type
else
post[:payment] = 'creditcard'
post[:ccnumber] = payment.number
post[:ccexp] = "#{format(payment.month, :two_digits)}#{format(payment.year, :two_digits)}"
post[:cvv] = payment.verification_value
end
end
def parse(body)
fields = {}
CGI::parse(body).each do |k, v|
fields[k.to_s] = v.kind_of?(Array) ? v[0] : v
end
fields
end
def success?(response)
response['response'].to_i == APPROVED
end
def commit(action, money, parameters)
parameters[:amount] = amount(money) unless action == 'void'
data = ssl_post self.live_url, post_data(action, parameters)
response = parse(data)
message = message_from(response)
Response.new(success?(response), message, response,
test: test?,
authorization: response['transactionid'],
avs_result: { code: response['avsresponse'] },
cvv_result: response['cvvresponse'])
end
def message_from(response)
MESSAGES.fetch(response['response_code'].to_i, false) || response['message']
end
def post_data(action, parameters = {})
post = { type: action }
if test?
post[:username] = 'demo'
post[:password] = 'password'
else
post[:username] = options[:username]
post[:password] = options[:password]
end
post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" unless value.nil? }.compact.join('&')
end
end
end
end