lib/active_merchant/billing/gateways/card_connect.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardConnectGateway < Gateway
self.test_url = 'https://fts-uat.cardconnect.com/cardconnect/rest/'
self.live_url = 'https://fts.cardconnect.com/cardconnect/rest/'
self.supported_countries = ['US']
self.default_currency = 'USD'
self.supported_cardtypes = %i[visa master american_express discover]
self.homepage_url = 'https://cardconnect.com/'
self.display_name = 'Card Connect'
STANDARD_ERROR_CODE_MAPPING = {
'11' => STANDARD_ERROR_CODE[:card_declined],
'12' => STANDARD_ERROR_CODE[:incorrect_number],
'13' => STANDARD_ERROR_CODE[:incorrect_cvc],
'14' => STANDARD_ERROR_CODE[:incorrect_cvc],
'15' => STANDARD_ERROR_CODE[:invalid_expiry_date],
'16' => STANDARD_ERROR_CODE[:expired_card],
'17' => STANDARD_ERROR_CODE[:incorrect_zip],
'21' => STANDARD_ERROR_CODE[:config_error],
'22' => STANDARD_ERROR_CODE[:config_error],
'23' => STANDARD_ERROR_CODE[:config_error],
'24' => STANDARD_ERROR_CODE[:processing_error],
'25' => STANDARD_ERROR_CODE[:processing_error],
'27' => STANDARD_ERROR_CODE[:processing_error],
'28' => STANDARD_ERROR_CODE[:processing_error],
'29' => STANDARD_ERROR_CODE[:processing_error],
'31' => STANDARD_ERROR_CODE[:processing_error],
'32' => STANDARD_ERROR_CODE[:processing_error],
'33' => STANDARD_ERROR_CODE[:card_declined],
'34' => STANDARD_ERROR_CODE[:card_declined],
'35' => STANDARD_ERROR_CODE[:incorrect_zip],
'36' => STANDARD_ERROR_CODE[:processing_error],
'37' => STANDARD_ERROR_CODE[:incorrect_cvc],
'41' => STANDARD_ERROR_CODE[:processing_error],
'42' => STANDARD_ERROR_CODE[:processing_error],
'43' => STANDARD_ERROR_CODE[:processing_error],
'44' => STANDARD_ERROR_CODE[:config_error],
'61' => STANDARD_ERROR_CODE[:processing_error],
'62' => STANDARD_ERROR_CODE[:processing_error],
'63' => STANDARD_ERROR_CODE[:processing_error],
'64' => STANDARD_ERROR_CODE[:config_error],
'65' => STANDARD_ERROR_CODE[:processing_error],
'66' => STANDARD_ERROR_CODE[:processing_error],
'91' => STANDARD_ERROR_CODE[:processing_error],
'92' => STANDARD_ERROR_CODE[:processing_error],
'93' => STANDARD_ERROR_CODE[:processing_error],
'94' => STANDARD_ERROR_CODE[:processing_error],
'95' => STANDARD_ERROR_CODE[:config_error],
'96' => STANDARD_ERROR_CODE[:processing_error],
'NU' => STANDARD_ERROR_CODE[:card_declined],
'N3' => STANDARD_ERROR_CODE[:card_declined],
'NJ' => STANDARD_ERROR_CODE[:card_declined],
'51' => STANDARD_ERROR_CODE[:card_declined],
'C2' => STANDARD_ERROR_CODE[:incorrect_cvc],
'54' => STANDARD_ERROR_CODE[:expired_card],
'05' => STANDARD_ERROR_CODE[:card_declined],
'03' => STANDARD_ERROR_CODE[:config_error],
'60' => STANDARD_ERROR_CODE[:pickup_card]
}
SCHEDULED_PAYMENT_TYPES = %w(recurring installment)
def initialize(options = {})
requires!(options, :merchant_id, :username, :password)
require_valid_domain!(options, :domain)
super
end
def require_valid_domain!(options, param)
if options[param]
raise ArgumentError.new('not a valid cardconnect domain') unless /https:\/\/\D*cardconnect.com/ =~ options[param]
end
end
def purchase(money, payment, options = {})
if options[:po_number]
MultiResponse.run do |r|
r.process { authorize(money, payment, options) }
r.process { capture(money, r.authorization, options) }
end
else
post = {}
add_invoice(post, options)
add_money(post, money)
add_payment(post, payment)
add_currency(post, money, options)
add_address(post, options)
add_customer_data(post, options)
add_three_ds_mpi_data(post, options)
add_additional_data(post, options)
add_stored_credential(post, options)
post[:capture] = 'Y'
commit('auth', post)
end
end
def authorize(money, payment, options = {})
post = {}
add_money(post, money)
add_currency(post, money, options)
add_invoice(post, options)
add_payment(post, payment)
add_address(post, options)
add_customer_data(post, options)
add_three_ds_mpi_data(post, options)
add_additional_data(post, options)
add_stored_credential(post, options)
commit('auth', post)
end
def capture(money, authorization, options = {})
post = {}
add_money(post, money)
add_reference(post, authorization)
add_additional_data(post, options)
commit('capture', post)
end
def refund(money, authorization, options = {})
post = {}
add_money(post, money)
add_reference(post, authorization)
commit('refund', post)
end
def void(authorization, options = {})
post = {}
add_reference(post, authorization)
commit('void', post)
end
def verify(credit_card, options = {})
authorize(0, credit_card, options)
end
def store(payment, options = {})
post = {}
add_payment(post, payment)
add_address(post, options)
add_customer_data(post, options)
commit('profile', post)
end
def unstore(authorization, options = {})
account_id, profile_id = authorization.split('|')
commit(
'profile',
{},
verb: :delete,
path: "/#{profile_id}/#{account_id}/#{@options[:merchant_id]}"
)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r(("cvv2\\":\\")\d*), '\1[FILTERED]').
gsub(%r(("merchid\\":\\")\d*), '\1[FILTERED]').
gsub(%r((&?"account\\":\\")\d*), '\1[FILTERED]').
gsub(%r((&?"token\\":\\")\d*), '\1[FILTERED]')
end
private
def add_customer_data(post, options)
post[:email] = options[:email] if options[:email]
end
def add_address(post, options)
if address = options[:billing_address] || options[:address]
post[:address] = address[:address1] if address[:address1]
post[:address2] = address[:address2] if address[:address2]
post[:city] = address[:city] if address[:city]
post[:region] = address[:state] if address[:state]
post[:country] = address[:country] if address[:country]
post[:postal] = address[:zip] if address[:zip]
post[:phone] = address[:phone] if address[:phone]
end
end
def add_money(post, money)
post[:amount] = amount(money)
end
def add_currency(post, money, options)
post[:currency] = (options[:currency] || currency(money))
end
def add_invoice(post, options)
post[:orderid] = options[:order_id]
post[:ecomind] = if options[:ecomind]
options[:ecomind].capitalize
else
(options[:recurring] ? 'R' : 'E')
end
end
def add_payment(post, payment)
if payment.is_a?(String)
account_id, profile_id = payment.split('|')
post[:profile] = profile_id
post[:acctid] = account_id
else
post[:name] = payment.name
if card_brand(payment) == 'check'
add_echeck(post, payment)
else
post[:account] = payment.number
post[:expiry] = expdate(payment)
post[:cvv2] = payment.verification_value
end
end
end
def add_echeck(post, payment)
post[:accttype] = 'ECHK'
post[:account] = payment.account_number
post[:bankaba] = payment.routing_number
end
def add_reference(post, authorization)
post[:retref] = authorization
end
def add_additional_data(post, options)
post[:ponumber] = options[:po_number]
post[:taxamnt] = options[:tax_amount] if options[:tax_amount]
post[:frtamnt] = options[:freight_amount] if options[:freight_amount]
post[:dutyamnt] = options[:duty_amount] if options[:duty_amount]
post[:orderdate] = options[:order_date] if options[:order_date]
post[:shipfromzip] = options[:ship_from_zip] if options[:ship_from_zip]
if (shipping_address = options[:shipping_address])
post[:shiptozip] = shipping_address[:zip]
post[:shiptocountry] = shipping_address[:country]
end
if options[:items]
post[:items] = options[:items].map do |item|
updated = {}
item.each_pair do |k, v|
updated.merge!(k.to_s.delete('_') => v)
end
updated
end
end
post[:userfields] = options[:user_fields] if options[:user_fields]
end
def add_three_ds_mpi_data(post, options)
return unless three_d_secure = options[:three_d_secure]
post[:secureflag] = three_d_secure[:eci]
post[:securevalue] = three_d_secure[:cavv]
post[:securedstid] = three_d_secure[:ds_transaction_id]
end
def add_stored_credential(post, options)
return unless stored_credential = options[:stored_credential]
post[:cof] = stored_credential[:initiator] == 'merchant' ? 'M' : 'C'
post[:cofscheduled] = SCHEDULED_PAYMENT_TYPES.include?(stored_credential[:reason_type]) ? 'Y' : 'N'
post[:cofpermission] = stored_credential[:initial_transaction] ? 'Y' : 'N'
end
def headers
{
'Authorization' => 'Basic ' + Base64.strict_encode64("#{@options[:username]}:#{@options[:password]}"),
'Content-Type' => 'application/json'
}
end
def expdate(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end
def parse(body)
JSON.parse(body)
end
def url(action, path)
if test?
test_url + action + path
else
(@options[:domain] || live_url) + action + path
end
end
def commit(action, parameters, verb: :put, path: '')
parameters[:frontendid] = application_id
parameters[:merchid] = @options[:merchant_id]
url = url(action, path)
response = parse(ssl_request(verb, url, post_data(parameters), headers))
Response.new(
success_from(response),
message_from(response),
response,
authorization: authorization_from(response),
avs_result: AVSResult.new(code: response['avsresp']),
cvv_result: CVVResult.new(response['cvvresp']),
test: test?,
error_code: error_code_from(response)
)
rescue ResponseError => e
return Response.new(false, 'Unable to authenticate. Please check your credentials.', {}, test: test?) if e.response.code == '401'
raise
end
def success_from(response)
response['respstat'] == 'A'
end
def message_from(response)
response['setlstat'] ? "#{response['resptext']} #{response['setlstat']}" : response['resptext']
end
def authorization_from(response)
if response['profileid']
"#{response['acctid']}|#{response['profileid']}"
else
response['retref']
end
end
def post_data(parameters = {})
parameters.to_json
end
def error_code_from(response)
STANDARD_ERROR_CODE_MAPPING[response['respcode']] unless success_from(response)
end
end
end
end