begin
require 'tclink'
rescue LoadError
# Falls back to an SSL post to TrustCommerce
end
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# TO USE:
# First, make sure you have everything setup correctly and all of your dependencies in place with:
#
# require 'rubygems'
# require 'active_merchant'
#
# ActiveMerchant expects amounts to be Integer values in cents
#
# tendollar = 1000
#
# Next, create a credit card object using a TC approved test card.
#
# creditcard = ActiveMerchant::Billing::CreditCard.new(
# :number => '4111111111111111',
# :month => 8,
# :year => 2006,
# :first_name => 'Longbob',
# :last_name => 'Longsen'
# )
#
# To finish setting up, create the active_merchant object you will be using, with the TrustCommerce gateway. If you have a
# functional TrustCommerce account, replace login and password with your account info. Otherwise the defaults will work for
# testing.
#
# gateway = ActiveMerchant::Billing::Base.gateway(:trust_commerce).new(:login => "TestMerchant", :password => "password")
#
# Now we are ready to process our transaction
#
# response = gateway.purchase(tendollar, creditcard)
#
# Sending a transaction to TrustCommerce with active_merchant returns a Response object, which consistently allows you to:
#
# 1) Check whether the transaction was successful
#
# response.success?
#
# 2) Retrieve any message returned by TrustCommerce, either a "transaction was successful" note or an explanation of why the
# transaction was rejected.
#
# response.message
#
# 3) Retrieve and store the unique transaction ID returned by Trust Commerece, for use in referencing the transaction in the future.
#
# response.params["transid"]
#
# For higher performance and failover with the TrustCommerceGateway you can install the TCLink library from http://www.trustcommerce.com/tclink.html.
# Follow the instructions available there to get it working on your system. ActiveMerchant will automatically use tclink if available.
#
# The TCLink library has the following added benefits:
# * Good transaction times. Transaction duration under 1.2 seconds are common.
# * Fail-over to geographically distributed servers for extreme reliability
#
# Once it is installed, you should be able to make sure
# that it is visible to your ruby install by opening irb and typing "require 'tclink'", which should return "true".
#
# This should be enough to get you started with Trust Commerce and active_merchant. For further information, review the methods
# below and the rest of active_merchant's documentation, as well as Trust Commerce's user and developer documentation.
class TrustCommerceGateway < Gateway
self.live_url = self.test_url = 'https://vault.trustcommerce.com/trans/'
SUCCESS_TYPES = %w[approved accepted]
DECLINE_CODES = {
'decline' => 'The credit card was declined',
'avs' => 'AVS failed; the address entered does not match the billing address on file at the bank',
'cvv' => 'CVV failed; the number provided is not the correct verification number for the card',
'call' => 'The card must be authorized manually over the phone',
'expiredcard' => 'Issuer was not certified for card verification',
'carderror' => 'Card number is invalid',
'authexpired' => 'Attempt to postauth an expired (more than 14 days old) preauth',
'fraud' => 'CrediGuard fraud score was below requested threshold',
'blacklist' => 'CrediGuard blacklist value was triggered',
'velocity' => 'CrediGuard velocity control value was triggered',
'dailylimit' => 'Daily limit in transaction count or amount as been reached',
'weeklylimit' => 'Weekly limit in transaction count or amount as been reached',
'monthlylimit' => 'Monthly limit in transaction count or amount as been reached'
}
BADDATA_CODES = {
'missingfields' => 'One or more parameters required for this transaction type were not sent',
'extrafields' => 'Parameters not allowed for this transaction type were sent',
'badformat' => 'A field was improperly formatted, such as non-digit characters in a number field',
'badlength' => 'A field was longer or shorter than the server allows',
'merchantcantaccept' => "The merchant can't accept data passed in this field",
'mismatch' => 'Data in one of the offending fields did not cross-check with the other offending field'
}
ERROR_CODES = {
'cantconnect' => "Couldn't connect to the TrustCommerce gateway",
'dnsfailure' => 'The TCLink software was unable to resolve DNS hostnames',
'linkfailure' => 'The connection was established, but was severed before the transaction could complete',
'failtoprocess' => 'The bank servers are offline and unable to authorize transactions'
}
TEST_LOGIN = 'TestMerchant'
TEST_PASSWORD = 'password'
VOIDABLE_ACTIONS = %w(preauth sale postauth credit)
self.money_format = :cents
self.supported_cardtypes = %i[visa master discover american_express diners_club jcb]
self.supported_countries = ['US']
self.homepage_url = 'http://www.trustcommerce.com/'
self.display_name = 'TrustCommerce'
def self.tclink?
defined?(TCLink)
end
# Creates a new TrustCommerceGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * :login -- The TrustCommerce account login.
# * :password -- The TrustCommerce account password.
# * :test => +true+ or +false+ -- Perform test transactions
#
# ==== Test Account Credentials
# * :login -- TestMerchant
# * :password -- password
def initialize(options = {})
requires!(options, :login, :password)
super
end
def tclink?
self.class.tclink?
end
def test?
((@options[:login] == TEST_LOGIN && @options[:password] == TEST_PASSWORD) || super)
end
# authorize() is the first half of the preauth(authorize)/postauth(capture) model. The TC API docs call this
# preauth, we preserve active_merchant's nomenclature of authorize() for consistency with the rest of the library. This
# method simply checks to make sure funds are available for a transaction, and returns a transid that can be used later to
# postauthorize (capture) the funds.
def authorize(money, creditcard_or_billing_id, options = {})
parameters = {
amount: amount(money)
}
add_order_id(parameters, options)
add_aggregator(parameters, options)
add_customer_data(parameters, options)
add_payment_source(parameters, creditcard_or_billing_id)
add_addresses(parameters, options)
add_custom_fields(parameters, options)
commit('preauth', parameters)
end
# purchase() is a simple sale. This is one of the most common types of transactions, and is extremely simple. All that you need
# to process a purchase are an amount in cents or a money object and a creditcard object or billingid string.
def purchase(money, creditcard_or_billing_id, options = {})
parameters = {
amount: amount(money)
}
add_order_id(parameters, options)
add_aggregator(parameters, options)
add_customer_data(parameters, options)
add_payment_source(parameters, creditcard_or_billing_id)
add_addresses(parameters, options)
add_custom_fields(parameters, options)
commit('sale', parameters)
end
# capture() is the second half of the preauth(authorize)/postauth(capture) model. The TC API docs call this
# postauth, we preserve active_merchant's nomenclature of capture() for consistency with the rest of the library. To process
# a postauthorization with TC, you need an amount in cents or a money object, and a TC transid.
def capture(money, authorization, options = {})
transaction_id, = split_authorization(authorization)
parameters = {
amount: amount(money),
transid: transaction_id
}
add_aggregator(parameters, options)
add_custom_fields(parameters, options)
commit('postauth', parameters)
end
# refund() allows you to return money to a card that was previously billed. You need to supply the amount, in cents or a money object,
# that you want to refund, and a TC transid for the transaction that you are refunding.
def refund(money, identification, options = {})
transaction_id, = split_authorization(identification)
parameters = {
amount: amount(money),
transid: transaction_id
}
add_aggregator(parameters, options)
add_custom_fields(parameters, options)
commit('credit', parameters)
end
def credit(money, identification, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
# void() clears an existing authorization and releases the reserved fund
# s back to the cardholder. The TC API refers to this transaction as a
# reversal. After voiding, you will no longer be able to capture funds
# from this authorization. TrustCommerce seems to always return a status
# of "accepted" even if the transid you are trying to deauthorize has
# already been captured. Note: Your account needs to be configured by
# TrustCommerce to allow for reversal transactions before you can use this
# method.
#
# void() is also used to to cancel a capture (postauth), purchase (sale),
# or refund (credit) or a before it is sent for settlement.
#
# NOTE: AMEX preauth's cannot be reversed. If you want to clear it more
# quickly than the automatic expiration (7-10 days), you will have to
# capture it and then immediately issue a credit for the same amount
# which should clear the customers credit card with 48 hours according to
# TC.
def void(authorization, options = {})
transaction_id, original_action = split_authorization(authorization)
action = (VOIDABLE_ACTIONS - ['preauth']).include?(original_action) ? 'void' : 'reversal'
parameters = {
transid: transaction_id
}
add_aggregator(parameters, options)
add_custom_fields(parameters, options)
commit(action, parameters)
end
def verify(credit_card, options = {})
parameters = {}
add_creditcard(parameters, credit_card)
commit('verify', parameters)
end
# recurring() a TrustCommerce account that is activated for Citadel, TrustCommerce's
# hosted customer billing info database.
#
# Recurring billing uses the same TC action as a plain-vanilla 'store', but we have a separate method for clarity. It can be called
# like store, with the addition of a required 'periodicity' parameter:
#
# The parameter :periodicity should be specified as either :bimonthly, :monthly, :biweekly, :weekly, :yearly or :daily
#
# gateway.recurring(tendollar, creditcard, :periodicity => :weekly)
#
# You can optionally specify how long you want payments to continue using 'payments'
def recurring(money, creditcard, options = {})
ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE
requires!(options, %i[periodicity bimonthly monthly biweekly weekly yearly daily])
cycle =
case options[:periodicity]
when :monthly
'1m'
when :bimonthly
'2m'
when :weekly
'1w'
when :biweekly
'2w'
when :yearly
'1y'
when :daily
'1d'
end
parameters = {
amount: amount(money),
cycle: cycle,
verify: options[:verify] || 'y',
billingid: options[:billingid] || nil,
payments: options[:payments] || nil
}
add_creditcard(parameters, creditcard)
commit('store', parameters)
end
# store() requires a TrustCommerce account that is activated for Citadel. You can call it with a credit card and a billing ID
# you would like to use to reference the stored credit card info for future captures. Use 'verify' to specify whether you want
# to simply store the card in the DB, or you want TC to verify the data first.
def store(creditcard, options = {})
parameters = {
verify: options[:verify] || 'y',
billingid: options[:billingid] || options[:billing_id] || nil
}
add_creditcard(parameters, creditcard)
add_addresses(parameters, options)
add_custom_fields(parameters, options)
commit('store', parameters)
end
# To unstore a creditcard stored in Citadel using store() or recurring(), all that is required is the billing id. When you run
# unstore() the information will be removed and a Response object will be returned indicating the success of the action.
def unstore(identification, options = {})
parameters = {
billingid: identification
}
add_custom_fields(parameters, options)
commit('unstore', parameters)
end
def supports_scrubbing
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r((&?cc=)\d*(&?)), '\1[FILTERED]\2').
gsub(%r((&?password=)[^&]+(&?)), '\1[FILTERED]\2').
gsub(%r((&?cvv=)\d*(&?)), '\1[FILTERED]\2').
gsub(%r((&?account=)\d*(&?)), '\1[FILTERED]\2')
end
private
def add_custom_fields(params, options)
options[:custom_fields]&.each do |key, value|
params[key.to_sym] = value
end
end
def add_aggregator(params, options)
if @options[:aggregator_id] || application_id != Gateway.application_id
params[:aggregators] = 1
params[:aggregator1] = @options[:aggregator_id] || application_id
end
end
def add_payment_source(params, source)
if source.is_a?(String)
add_billing_id(params, source)
elsif card_brand(source) == 'check'
add_check(params, source)
else
add_creditcard(params, source)
end
end
def add_check(params, check)
params[:media] = 'ach'
params[:routing] = check.routing_number
params[:account] = check.account_number
params[:savings] = 'y' if check.account_type == 'savings'
params[:name] = check.name
end
def add_creditcard(params, creditcard)
params[:media] = 'cc'
params[:name] = creditcard.name
params[:cc] = creditcard.number
params[:exp] = expdate(creditcard)
params[:cvv] = creditcard.verification_value if creditcard.verification_value?
end
def add_order_id(params, options)
params[:ticket] = options[:order_id] unless options[:order_id].blank?
end
def add_billing_id(params, billingid)
params[:billingid] = billingid
end
def add_customer_data(params, options)
params[:email] = options[:email] unless options[:email].blank?
params[:ip] = options[:ip] unless options[:ip].blank?
end
def add_addresses(params, options)
address = options[:billing_address] || options[:address]
if address
params[:address1] = address[:address1] unless address[:address1].blank?
params[:address2] = address[:address2] unless address[:address2].blank?
params[:city] = address[:city] unless address[:city].blank?
params[:state] = address[:state] unless address[:state].blank?
params[:zip] = address[:zip] unless address[:zip].blank?
params[:country] = address[:country] unless address[:country].blank?
params[:avs] = 'n'
end
if shipping_address = options[:shipping_address]
params[:shipto_name] = shipping_address[:name] unless shipping_address[:name].blank?
params[:shipto_address1] = shipping_address[:address1] unless shipping_address[:address1].blank?
params[:shipto_address2] = shipping_address[:address2] unless shipping_address[:address2].blank?
params[:shipto_city] = shipping_address[:city] unless shipping_address[:city].blank?
params[:shipto_state] = shipping_address[:state] unless shipping_address[:state].blank?
params[:shipto_zip] = shipping_address[:zip] unless shipping_address[:zip].blank?
params[:shipto_country] = shipping_address[:country] unless shipping_address[:country].blank?
end
end
def clean_and_stringify_params(parameters)
# TCLink wants us to send a hash with string keys, and activemerchant pushes everything around with
# symbol keys. Before sending our input to TCLink, we convert all our keys to strings and dump the symbol keys.
# We also remove any pairs with nil values, as these confuse TCLink.
parameters.keys.reverse_each do |key|
parameters[key.to_s] = parameters[key] if parameters[key]
parameters.delete(key)
end
end
def post_data(parameters)
parameters.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
def commit(action, parameters)
parameters[:custid] = @options[:login]
parameters[:password] = @options[:password]
parameters[:demo] = test? ? 'y' : 'n'
parameters[:action] = action
clean_and_stringify_params(parameters)
data = if tclink?
TCLink.send(parameters)
else
parse(ssl_post(self.live_url, post_data(parameters)))
end
# to be considered successful, transaction status must be either "approved" or "accepted"
success = SUCCESS_TYPES.include?(data['status'])
message = message_from(data)
Response.new(
success,
message,
data,
test: test?,
authorization: authorization_from(action, data),
cvv_result: data['cvv'],
avs_result: { code: data['avs'] }
)
end
def parse(body)
results = {}
body.split(/\n/).each do |pair|
key, val = pair.split(/=/)
results[key] = val
end
results
end
def message_from(data)
case data['status']
when 'decline'
return DECLINE_CODES[data['declinetype']]
when 'baddata'
return BADDATA_CODES[data['error']]
when 'error'
return ERROR_CODES[data['errortype']]
else
return 'The transaction was successful'
end
end
def authorization_from(action, data)
case action
when 'store'
data['billingid']
when *VOIDABLE_ACTIONS
"#{data['transid']}|#{action}"
else
data['transid']
end
end
def split_authorization(authorization)
authorization&.split('|')
end
end
end
end