module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CashnetGateway < Gateway
include Empty
self.live_url = 'https://commerce.cashnet.com/'
self.test_url = 'https://train.cashnet.com/'
self.supported_countries = ['US']
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
self.homepage_url = 'https://transactcampus.com'
self.display_name = 'Cashnet'
self.money_format = :dollars
self.max_retries = 0
# Creates a new CashnetGateway
#
# ==== Options
#
# * :merchant -- Gateway Merchant (REQUIRED)
# * :operator -- Operator (REQUIRED)
# * :password -- Password (REQUIRED)
# * :merchant_gateway_name -- Site name (REQUIRED)
# * :station -- Station (defaults to "WEB")
# * :custcode -- Customer code (defaults to
# "ActiveMerchant/#{ActiveMerchant::VERSION}")
# * :default_item_code -- Default item code (defaults to "FEE",
# can be overridden on a per-transaction basis with options[:item_code])
def initialize(options = {})
requires!(
options,
:merchant,
:operator,
:password,
:merchant_gateway_name
)
options[:default_item_code] ||= 'FEE'
super
end
def purchase(money, payment_object, options = {})
post = {}
add_creditcard(post, payment_object)
add_invoice(post, money, options)
add_address(post, options)
add_customer_data(post, options)
commit('SALE', money, post)
end
def refund(money, identification, options = {})
post = {}
post[:origtx] = identification
add_invoice(post, money, options)
add_customer_data(post, options)
commit('REFUND', money, post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r{(password=)[^&]+}, '\1[FILTERED]').
gsub(%r{(cardno=)[^&]+}, '\1[FILTERED]').
gsub(%r{(cid=)[^&]+}, '\1[FILTERED]')
end
private
def commit(action, money, fields)
url = (test? ? test_url : live_url) + CGI.escape(@options[:merchant_gateway_name])
raw_response = ssl_post(url, post_data(action, fields))
parsed_response = parse(raw_response)
return unparsable_response(raw_response) unless parsed_response
success = success?(parsed_response)
Response.new(
success,
CASHNET_CODES[parsed_response[:result]],
parsed_response,
test: test?,
authorization: (success ? parsed_response[:tx] : '')
)
end
def success?(response)
response[:result] == '0'
end
def post_data(action, parameters = {})
post = {}
post[:command] = action
post[:merchant] = @options[:merchant]
post[:operator] = @options[:operator]
post[:password] = @options[:password]
post[:station] = (@options[:station] || 'WEB')
post[:custcode] = (@options[:custcode] || "ActiveMerchant/#{ActiveMerchant::VERSION}")
post.merge(parameters).collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
def add_creditcard(post, creditcard)
post[:cardno] = creditcard.number
post[:cid] = creditcard.verification_value
post[:expdate] = expdate(creditcard)
post[:card_name_g] = creditcard.name
post[:fname] = creditcard.first_name
post[:lname] = creditcard.last_name
end
def add_invoice(post, money, options)
post[:order_number] = options[:order_id] if options[:order_id].present?
if options[:item_codes].present?
codes_and_amounts = options[:item_codes].transform_keys { |key| key.to_s.delete('_') }
codes_and_amounts.each do |key, value|
post[key] = value if key.start_with?('itemcode')
post[key] = amount(value.to_i) if key.start_with?('amount')
end
else
post[:itemcode] = (options[:item_code] || @options[:default_item_code])
post[:amount] = amount(money.to_i)
end
end
def add_address(post, options)
if address = (options[:shipping_address] || options[:billing_address] || options[:address])
post[:addr_g] = String(address[:address1]) + ',' + String(address[:address2])
post[:city_g] = address[:city]
post[:state_g] = address[:state]
post[:zip_g] = address[:zip]
end
end
def add_customer_data(post, options)
post[:email_g] = options[:email]
post[:custcode] = options[:custcode] unless empty?(options[:custcode])
end
def expdate(creditcard)
year = format(creditcard.year, :two_digits)
month = format(creditcard.month, :two_digits)
"#{month}#{year}"
end
def parse(body)
match = body.match(/(.*)<\/cngateway>/)
return nil unless match
Hash[CGI::parse(match[1]).map { |k, v| [k.to_sym, v.first] }]
end
def handle_response(response)
if (200...300).cover?(response.code.to_i)
return response.body
elsif response.code.to_i == 302
return ssl_get(URI.parse(response['location']))
end
raise ResponseError.new(response)
end
def unparsable_response(raw_response)
message = 'Unparsable response received from Cashnet. Please contact Cashnet if you continue to receive this message.'
message += " (The raw response returned by the API was #{raw_response.inspect})"
return Response.new(false, message)
end
CASHNET_CODES = {
'0' => 'Success',
'1' => 'Invalid customer code, no customer code specified',
'2' => 'Invalid operator code, no operator specified',
'3' => 'Invalid workstation code, no station specified',
'4' => 'Invalid item code, no code specified',
'5' => 'Negative amount is not allowed',
'6' => 'Invalid credit card number, no credit card number provided',
'7' => 'Invalid expiration date, no expiration date provided',
'8' => 'Please only provide either ACH or credit card information',
'9' => 'Invalid ACH account number, no account number provided',
'10' => 'Invalid routing/transit number, no routing/transit number provided',
'11' => 'Invalid account type, no account type provided',
'12' => 'Invalid check digit for routing/transit number',
'13' => 'No ACH merchant account set up for the location of the station being used',
'21' => 'Invalid merchant code, no merchant code provided',
'22' => 'Invalid client code, no client code provided',
'23' => 'Invalid password, no password provided',
'24' => 'Invalid transaction type, no transaction type provided',
'25' => 'Invalid amount, amount not provided',
'26' => 'Invalid payment code provided',
'27' => 'Invalid version number, version not found',
'31' => 'Application amount exceeds account balance',
'150' => 'Invalid payment information, no payment information provided',
'200' => 'Invalid command',
'201' => 'Customer not on file',
'205' => 'Invalid operator or password',
'206' => 'Operator is not authorized for this function',
'208' => 'Customer/PIN authentication unsuccessful',
'209' => 'Credit card error',
'211' => 'Credit card error',
'212' => 'Customer/PIN not on file',
'213' => 'Customer information not on file',
'215' => 'Old PIN does not validate ',
'221' => 'Invalid credit card processor type specified in location or payment code',
'222' => 'Credit card processor error',
'230' => 'Host Error (USE VOID OR REVERSAL TO REFUND UNSETTLED TRANSACTIONS)',
'280' => 'SmartPay transaction not posted',
'301' => 'Original transaction not found for this customer',
'302' => 'Amount to refund exceeds original payment amount or is missing',
'304' => 'Original credit card payment not found or corrupted',
'305' => 'Refund amounts should be expressed as positive amounts',
'306' => 'Original ACH payment not found',
'307' => 'Original electronic payment not found',
'308' => 'Invalid original transaction number, original transaction number not found',
'310' => 'Refund amount exceeds amount still available for a refund',
'321' => 'Store has not been implemented',
'501' => 'Unable to roll over batch',
'502' => 'Batch not found',
'503' => 'Batch information not available',
'650' => 'Invalid quick code',
'651' => 'Transaction amount does not match amount specified in quick code',
'652' => 'Invalid item code in the detail of the quick code',
'701' => 'This website has been disabled. Please contact the system administrator.',
'702' => 'Improper merchant code. Please contact the system administrator.',
'703' => 'This site is temporarily down for maintenance. We regret the inconvenience. Please try again later.',
'704' => 'Duplicate item violation. Please contact the system administrator.',
'705' => 'An invalid reference type has been passed into the system. Please contact the system administrator',
'706' => 'Items violating unique selection have been passed in. Please contact the system administrator.',
'999' => 'An unexpected error has occurred. Please consult the event log.'
}
end
end
end