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 = ['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 = [: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 # 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, [: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') 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) authorization = data['transid'] authorization = "#{authorization}|#{action}" if authorization && VOIDABLE_ACTIONS.include?(action) authorization end def split_authorization(authorization) authorization&.split('|') end end end end