lib/active_merchant/billing/gateways/cardknox.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class CardknoxGateway < Gateway
self.live_url = 'https://x1.cardknox.com/gateway'
self.supported_countries = %w[US CA GB]
self.default_currency = 'USD'
self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
self.homepage_url = 'https://www.cardknox.com/'
self.display_name = 'Cardknox'
COMMANDS = {
credit_card: {
purchase: 'cc:sale',
authorization: 'cc:authonly',
capture: 'cc:capture',
refund: 'cc:refund',
void: 'cc:void',
save: 'cc:save'
},
check: {
purchase: 'check:sale',
refund: 'check:refund',
void: 'check:void',
save: 'check:save'
}
}
def initialize(options = {})
requires!(options, :api_key)
super
end
# There are three sources for doing a purchase transation:
# - credit card
# - check
# - cardknox token, which is returned in the the authorization string "ref_num;token;command"
def purchase(amount, source, options = {})
post = {}
add_amount(post, amount, options)
add_invoice(post, options)
add_source(post, source)
add_address(post, source, options)
add_customer_data(post, options)
add_custom_fields(post, options)
commit(:purchase, source_type(source), post)
end
def authorize(amount, source, options = {})
post = {}
add_amount(post, amount)
add_invoice(post, options)
add_source(post, source)
add_address(post, source, options)
add_customer_data(post, options)
add_custom_fields(post, options)
commit(:authorization, source_type(source), post)
end
def capture(amount, authorization, options = {})
post = {}
add_reference(post, authorization)
add_amount(post, amount)
commit(:capture, source_type(authorization), post)
end
def refund(amount, authorization, options = {})
post = {}
add_reference(post, authorization)
add_amount(post, amount)
commit(:refund, source_type(authorization), post)
end
def void(authorization, options = {})
post = {}
add_reference(post, authorization)
commit(:void, source_type(authorization), post)
end
def verify(credit_card, options = {})
MultiResponse.run(:use_first_response) do |r|
r.process { authorize(100, credit_card, options) }
r.process(:ignore_result) { void(r.authorization, options) }
end
end
def store(source, options = {})
post = {}
add_source(post, source)
add_address(post, source, options)
add_invoice(post, options)
add_customer_data(post, options)
add_custom_fields(post, options)
commit(:save, source_type(source), post)
end
def supports_scrubbing?
true
end
def scrub(transcript)
transcript.
gsub(%r((xCardNum=)\d+), '\1[FILTERED]').
gsub(%r((xCVV=)\d+), '\1[FILTERED]').
gsub(%r((xAccount=)\d+), '\1[FILTERED]').
gsub(%r((xRouting=)\d+), '\1[FILTERED]').
gsub(%r((xKey=)\w+), '\1[FILTERED]')
end
private
def split_authorization(authorization)
authorization.split(';')
end
def add_reference(post, reference)
reference, = split_authorization(reference)
post[:Refnum] = reference
end
def source_type(source)
if source.respond_to?(:brand)
:credit_card
elsif source.respond_to?(:routing_number)
:check
elsif source.kind_of?(String)
source_type_from(source)
else
raise ArgumentError, "Unknown source #{source.inspect}"
end
end
def source_type_from(authorization)
_, _, source_type = split_authorization(authorization)
(source_type || 'credit_card').to_sym
end
def add_source(post, source)
if source.respond_to?(:brand)
add_credit_card(post, source)
elsif source.respond_to?(:routing_number)
add_check(post, source)
elsif source.kind_of?(String)
add_cardknox_token(post, source)
else
raise ArgumentError, "Invalid payment source #{source.inspect}"
end
end
# Subtotal + Tax + Tip = Amount.
def add_amount(post, money, options = {})
post[:Tip] = amount(options[:tip])
post[:Amount] = amount(money)
end
def expdate(credit_card)
year = format(credit_card.year, :two_digits)
month = format(credit_card.month, :two_digits)
"#{month}#{year}"
end
def add_customer_data(post, options)
address = options[:billing_address] || {}
post[:Street] = address[:address1]
post[:Zip] = address[:zip]
post[:PONum] = options[:po_number]
post[:Fax] = options[:fax]
post[:Email] = options[:email]
post[:IP] = options[:ip]
end
def add_address(post, source, options)
add_address_for_type(:billing, post, source, options[:billing_address]) if options[:billing_address]
add_address_for_type(:shipping, post, source, options[:shipping_address]) if options[:shipping_address]
end
def add_address_for_type(type, post, source, address)
prefix = address_key_prefix(type)
if source.respond_to?(:first_name)
post[address_key(prefix, 'FirstName')] = source.first_name
post[address_key(prefix, 'LastName')] = source.last_name
else
post[address_key(prefix, 'FirstName')] = address[:first_name]
post[address_key(prefix, 'LastName')] = address[:last_name]
end
post[address_key(prefix, 'MiddleName')] = address[:middle_name]
post[address_key(prefix, 'Company')] = address[:company]
post[address_key(prefix, 'Street')] = address[:address1]
post[address_key(prefix, 'Street2')] = address[:address2]
post[address_key(prefix, 'City')] = address[:city]
post[address_key(prefix, 'State')] = address[:state]
post[address_key(prefix, 'Zip')] = address[:zip]
post[address_key(prefix, 'Country')] = address[:country]
post[address_key(prefix, 'Phone')] = address[:phone]
post[address_key(prefix, 'Mobile')] = address[:mobile]
end
def address_key_prefix(type)
case type
when :shipping then 'Ship'
when :billing then 'Bill'
else
raise ArgumentError, "Unknown address key prefix: #{type}"
end
end
def address_key(prefix, key)
"#{prefix}#{key}".to_sym
end
def add_invoice(post, options)
post[:Invoice] = options[:invoice]
post[:OrderID] = options[:order_id]
post[:Comments] = options[:comments]
post[:Description] = options[:description]
post[:Tax] = amount(options[:tax])
end
def add_custom_fields(post, options)
options.keys.grep(/^custom(?:[01]\d|20)$/) do |key|
post[key.to_s.capitalize] = options[key]
end
end
def add_credit_card(post, credit_card)
if credit_card.track_data.present?
post[:Magstripe] = credit_card.track_data
post[:Cardpresent] = true
else
post[:CardNum] = credit_card.number
post[:CVV] = credit_card.verification_value
post[:Exp] = expdate(credit_card)
post[:Name] = credit_card.name
post[:CardPresent] = true if credit_card.manual_entry
end
end
def add_check(post, check)
post[:Routing] = check.routing_number
post[:Account] = check.account_number
post[:Name] = check.name
post[:CheckNum] = check.number
end
def add_cardknox_token(post, authorization)
_, token, = split_authorization(authorization)
post[:Token] = token
end
def parse(body)
fields = {}
for line in body.split('&')
key, value = *line.scan(%r{^(\w+)\=(.*)$}).flatten
fields[key] = CGI.unescape(value.to_s)
end
{
result: fields['xResult'],
status: fields['xStatus'],
error: fields['xError'],
auth_code: fields['xAuthCode'],
ref_num: fields['xRefNum'],
current_ref_num: fields['xRefNumCurrent'],
token: fields['xToken'],
batch: fields['xBatch'],
avs_result: fields['xAvsResult'],
avs_result_code: fields['xAvsResultCode'],
cvv_result: fields['xCvvResult'],
cvv_result_code: fields['xCvvResultCode'],
remaining_balance: fields['xRemainingBalance'],
amount: fields['xAuthAmount'],
masked_card_num: fields['xMaskedCardNumber'],
masked_account_number: fields['MaskedAccountNumber']
}.delete_if { |_k, v| v.nil? }
end
def commit(action, source_type, parameters)
response = parse(ssl_post(live_url, post_data(COMMANDS[source_type][action], parameters)))
Response.new(
(response[:status] == 'Approved'),
message_from(response),
response,
authorization: authorization_from(response, source_type),
avs_result: { code: response[:avs_result_code] },
cvv_result: response[:cvv_result_code]
)
end
def message_from(response)
if response[:status] == 'Approved'
'Success'
elsif response[:error].blank?
'Unspecified error'
else
response[:error]
end
end
def authorization_from(response, source_type)
"#{response[:ref_num]};#{response[:token]};#{source_type}"
end
def post_data(command, parameters = {})
initial_parameters = {
Key: @options[:api_key],
Version: '4.5.4',
SoftwareName: 'Active Merchant',
SoftwareVersion: ActiveMerchant::VERSION.to_s,
Command: command
}
seed = SecureRandom.hex(32).upcase
hash = Digest::SHA1.hexdigest("#{initial_parameters[:command]}:#{@options[:pin]}:#{parameters[:amount]}:#{parameters[:invoice]}:#{seed}")
initial_parameters[:Hash] = "s/#{seed}/#{hash}/n" unless @options[:pin].blank?
parameters = initial_parameters.merge(parameters)
parameters.reject { |_k, v| v.blank? }.collect { |key, value| "x#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
end
end
end