module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class LitleGateway < Gateway
# Specific to Litle options:
# * :merchant_id - Merchant Id assigned by Litle
# * :user - Username assigned by Litle
# * :password - Password assigned by Litle
# * :version - The version of the api you are using (eg, '8.10')
# * :proxy_addr - Proxy address - nil if not needed
# * :proxy_port - Proxy port - nil if not needed
# * :url - URL assigned by Litle (for testing, use the sandbox)
#
# Standard Active Merchant options
# * :order_id - The order number
# * :ip - The IP address of the customer making the purchase
# * :customer - The name, customer number, or other information that identifies the customer
# * :invoice - The invoice number
# * :merchant - The name or description of the merchant offering the product
# * :description - A description of the transaction
# * :email - The email address of the customer
# * :currency - The currency of the transaction. Only important when you are using a currency that is not the default with a gateway that supports multiple currencies.
# * :billing_address - A hash containing the billing address of the customer.
# * :shipping_address - A hash containing the shipping address of the customer.
#
# The :billing_address, and :shipping_address hashes can have the following keys:
#
# * :name - The full name of the customer.
# * :company - The company name of the customer.
# * :address1 - The primary street address of the customer.
# * :address2 - Additional line of address information.
# * :city - The city of the customer.
# * :state - The state of the customer. The 2 digit code for US and Canadian addresses. The full name of the state or province for foreign addresses.
# * :country - The [ISO 3166-1-alpha-2 code](http://www.iso.org/iso/country_codes/iso_3166_code_lists/english_country_names_and_code_elements.htm) for the customer.
# * :zip - The zip or postal code of the customer.
# * :phone - The phone number of the customer.
self.test_url = 'https://www.testlitle.com/sandbox/communicator/online'
self.live_url = 'https://payments.litle.com/vap/communicator/online'
LITLE_SCHEMA_VERSION = '8.13'
# The countries the gateway supports merchants from as 2 digit ISO country codes
self.supported_countries = ['US']
# The card types supported by the payment gateway
self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb]
# The homepage URL of the gateway
self.homepage_url = 'http://www.litle.com/'
# The name of the gateway
self.display_name = 'Litle & Co.'
self.default_currency = 'USD'
def initialize(options = {})
begin
require 'LitleOnline'
rescue LoadError
raise "Could not load the LitleOnline gem (>= 08.13.2). Use `gem install LitleOnline` to install it."
end
@litle = LitleOnline::LitleOnlineRequest.new
options[:version] ||= LITLE_SCHEMA_VERSION
options[:merchant] ||= 'Default Report Group'
options[:user] ||= options[:login]
requires!(options, :merchant_id, :user, :password, :merchant, :version)
super
end
def authorize(money, creditcard_or_token, options = {})
to_pass = build_authorize_request(money, creditcard_or_token, options)
build_response(:authorization, @litle.authorization(to_pass))
end
def purchase(money, creditcard_or_token, options = {})
to_pass = build_purchase_request(money, creditcard_or_token, options)
build_response(:sale, @litle.sale(to_pass))
end
def capture(money, authorization, options = {})
transaction_id, kind = split_authorization(authorization)
to_pass = create_capture_hash(money, transaction_id, options)
build_response(:capture, @litle.capture(to_pass))
end
# Note: Litle requires that authorization requests be voided via auth_reversal
# and other requests via void. To maintain the same interface as the other
# gateways the transaction_id and the kind of transaction are concatenated
# together with a ; separator (e.g. 1234;authorization)
#
# A partial auth_reversal can be accomplished by passing :amount as an option
def void(identification, options = {})
transaction_id, kind = split_authorization(identification)
if(kind == 'authorization')
to_pass = create_auth_reversal_hash(transaction_id, options[:amount], options)
build_response(:authReversal, @litle.auth_reversal(to_pass))
else
to_pass = create_void_hash(transaction_id, options)
build_response(:void, @litle.void(to_pass))
end
end
def credit(money, identification_or_token, options = {})
to_pass = build_credit_request(money, identification_or_token, options)
build_response(:credit, @litle.credit(to_pass))
end
def store(creditcard, options = {})
to_pass = create_token_hash(creditcard, options)
build_response(:registerToken, @litle.register_token_request(to_pass), %w(000 801 802))
end
private
CARD_TYPE = {
'visa' => 'VI',
'master' => 'MC',
'american_express' => 'AX',
'discover' => 'DI',
'jcb' => 'DI',
'diners_club' => 'DI'
}
AVS_RESPONSE_CODE = {
'00' => 'Y',
'01' => 'X',
'02' => 'D',
'10' => 'Z',
'11' => 'W',
'12' => 'A',
'13' => 'A',
'14' => 'P',
'20' => 'N',
'30' => 'S',
'31' => 'R',
'32' => 'U',
'33' => 'R',
'34' => 'I',
'40' => 'E'
}
def url
return @options[:url] if @options[:url].present?
test? ? self.test_url : self.live_url
end
def build_response(kind, litle_response, valid_responses=%w(000))
response = Hash.from_xml(litle_response.raw_xml.to_s)['litleOnlineResponse']
if response['response'] == "0"
detail = response["#{kind}Response"]
fraud = fraud_result(detail)
Response.new(
valid_responses.include?(detail['response']),
detail['message'],
{ :litleOnlineResponse => response },
:authorization => authorization_from(detail, kind),
:avs_result => { :code => fraud['avs'] },
:cvv_result => fraud['cvv'],
:test => test?
)
else
Response.new(false, response['message'], :litleOnlineResponse => response, :test => test?)
end
end
# Generates an authorization string of the appropriate id and the kind of transaction
# See #void for how the kind is used
def authorization_from(litle_response, kind)
case kind
when :registerToken
authorization = litle_response['litleToken']
else
authorization = [litle_response['litleTxnId'], kind.to_s].join(";")
end
end
def split_authorization(authorization)
transaction_id, kind = authorization.to_s.split(';')
[transaction_id, kind]
end
def build_authorize_request(money, creditcard_or_token, options)
payment_method = build_payment_method(creditcard_or_token, options)
hash = create_hash(money, options)
add_creditcard_or_cardtoken_hash(hash, payment_method)
hash
end
def build_purchase_request(money, creditcard_or_token, options)
payment_method = build_payment_method(creditcard_or_token, options)
hash = create_hash(money, options)
add_creditcard_or_cardtoken_hash(hash, payment_method)
hash
end
def build_credit_request(money, identification_or_token, options)
payment_method = build_payment_method(identification_or_token, options)
hash = create_hash(money, options)
add_identification_or_cardtoken_hash(hash, payment_method)
unless payment_method.is_a?(LitleCardToken)
hash['orderSource'] = nil
hash['orderId'] = nil
end
hash
end
def build_payment_method(payment_method, options)
result = payment_method
# Build instance of the LitleCardToken class for internal use if this is a token request.
if payment_method.is_a?(String) && options.has_key?(:token)
result = LitleCardToken.new(:token => payment_method)
result.month = options[:token][:month]
result.year = options[:token][:year]
result.verification_value = options[:token][:verification_value]
result.brand = options[:token][:brand]
end
result
end
def add_creditcard_or_cardtoken_hash(hash, creditcard_or_cardtoken)
if creditcard_or_cardtoken.is_a?(LitleCardToken)
add_cardtoken_hash(hash, creditcard_or_cardtoken)
else
add_creditcard_hash(hash, creditcard_or_cardtoken)
end
end
def add_identification_or_cardtoken_hash(hash, identification_or_cardtoken)
if identification_or_cardtoken.is_a?(LitleCardToken)
add_cardtoken_hash(hash, identification_or_cardtoken)
else
transaction_id, kind = split_authorization(identification_or_cardtoken)
hash['litleTxnId'] = transaction_id
end
end
def add_cardtoken_hash(hash, cardtoken)
token_info = {}
token_info['litleToken'] = cardtoken.token
token_info['expDate'] = cardtoken.exp_date if cardtoken.exp_date?
token_info['cardValidationNum'] = cardtoken.verification_value unless cardtoken.verification_value.blank?
token_info['type'] = cardtoken.type unless cardtoken.type.blank?
hash['token'] = token_info
hash
end
def add_creditcard_hash(hash, creditcard)
cc_type = CARD_TYPE[creditcard.brand]
exp_date_yr = creditcard.year.to_s[2..3]
exp_date_mo = '%02d' % creditcard.month.to_i
exp_date = exp_date_mo + exp_date_yr
card_info = {
'type' => cc_type,
'number' => creditcard.number,
'expDate' => exp_date,
'cardValidationNum' => creditcard.verification_value
}
hash['card'] = card_info
hash
end
def create_capture_hash(money, authorization, options)
hash = create_hash(money, options)
hash['litleTxnId'] = authorization
hash
end
def create_token_hash(creditcard, options)
hash = create_hash(0, options)
hash['accountNumber'] = creditcard.number
hash
end
def create_void_hash(identification, options)
hash = create_hash(nil, options)
hash['litleTxnId'] = identification
hash
end
def create_auth_reversal_hash(identification, money, options)
hash = create_hash(money, options)
hash['litleTxnId'] = identification
hash
end
def create_hash(money, options)
fraud_check_type = {}
if options[:ip]
fraud_check_type['customerIpAddress'] = options[:ip]
end
enhanced_data = {}
if options[:invoice]
enhanced_data['invoiceReferenceNumber'] = options[:invoice]
end
if options[:description]
enhanced_data['customerReference'] = options[:description]
end
if options[:billing_address]
bill_to_address = {
'name' => options[:billing_address][:name],
'companyName' => options[:billing_address][:company],
'addressLine1' => options[:billing_address][:address1],
'addressLine2' => options[:billing_address][:address2],
'city' => options[:billing_address][:city],
'state' => options[:billing_address][:state],
'zip' => options[:billing_address][:zip],
'country' => options[:billing_address][:country],
'email' => options[:email],
'phone' => options[:billing_address][:phone]
}
end
if options[:shipping_address]
ship_to_address = {
'name' => options[:shipping_address][:name],
'companyName' => options[:shipping_address][:company],
'addressLine1' => options[:shipping_address][:address1],
'addressLine2' => options[:shipping_address][:address2],
'city' => options[:shipping_address][:city],
'state' => options[:shipping_address][:state],
'zip' => options[:shipping_address][:zip],
'country' => options[:shipping_address][:country],
'email' => options[:email],
'phone' => options[:shipping_address][:phone]
}
end
hash = {
'billToAddress' => bill_to_address,
'shipToAddress' => ship_to_address,
'orderId' => (options[:order_id] || @options[:order_id]),
'customerId' => options[:customer],
'reportGroup' => (options[:merchant] || @options[:merchant]),
'merchantId' => (options[:merchant_id] || @options[:merchant_id]),
'orderSource' => (options[:order_source] || 'ecommerce'),
'enhancedData' => enhanced_data,
'fraudCheckType' => fraud_check_type,
'user' => (options[:user] || @options[:user]),
'password' => (options[:password] || @options[:password]),
'version' => (options[:version] || @options[:version]),
'url' => (options[:url] || url),
'proxy_addr' => (options[:proxy_addr] || @options[:proxy_addr]),
'proxy_port' => (options[:proxy_port] || @options[:proxy_port]),
'id' => (options[:id] || options[:order_id] || @options[:order_id])
}
if (!money.nil? && money.to_s.length > 0)
hash.merge!({ 'amount' => money })
end
hash
end
def fraud_result(authorization_response)
if result = authorization_response['fraudResult']
if result.key?('cardValidationResult')
cvv_to_pass = result['cardValidationResult'].blank? ? "P" : result['cardValidationResult']
end
avs_to_pass = AVS_RESPONSE_CODE[result['avsResult']] unless result['avsResult'].blank?
end
{ 'cvv' => cvv_to_pass, 'avs' => avs_to_pass }
end
# A +LitleCardToken+ object represents a tokenized credit card, and is capable of validating the various
# data associated with these.
#
# == Example Usage
# token = LitleCardToken.new(
# :token => '1234567890123456',
# :month => '9',
# :year => '2010',
# :brand => 'visa',
# :verification_value => '123'
# )
#
# token.valid? # => true
# cc.exp_date # => 0910
#
class LitleCardToken
include Validateable
# Returns or sets the token. (required)
#
# @return [String]
attr_accessor :token
# Returns or sets the expiry month for the card associated with token. (optional)
#
# @return [Integer]
attr_accessor :month
# Returns or sets the expiry year for the card associated with token. (optional)
#
# @return [Integer]
attr_accessor :year
# Returns or sets the card verification value. (optional)
#
# @return [String] the verification value
attr_accessor :verification_value
# Returns or sets the credit card brand. (optional)
#
# Valid card types are
#
# * +'visa'+
# * +'master'+
# * +'discover'+
# * +'american_express'+
# * +'diners_club'+
# * +'jcb'+
# * +'switch'+
# * +'solo'+
# * +'dankort'+
# * +'maestro'+
# * +'forbrugsforeningen'+
# * +'laser'+
#
# @return (String) the credit card brand
attr_accessor :brand
# Returns the Litle credit card type identifier.
#
# @return (String) the credit card type identifier
def type
CARD_TYPE[brand] unless brand.blank?
end
# Returns true if the expiration date is set.
#
# @return (Boolean)
def exp_date?
!month.to_i.zero? && !year.to_i.zero?
end
# Returns the card token expiration date in MMYY format.
#
# @return (String) the expiration date in MMYY format
def exp_date
result = ''
if exp_date?
exp_date_yr = year.to_s[2..3]
exp_date_mo = '%02d' % month.to_i
result = exp_date_mo + exp_date_yr
end
result
end
# Validates the card token details.
#
# Any validation errors are added to the {#errors} attribute.
def validate
validate_card_token
validate_expiration_date
validate_card_brand
end
def check?
false
end
private
CARD_TYPE = {
'visa' => 'VI',
'master' => 'MC',
'american_express' => 'AX',
'discover' => 'DI',
'jcb' => 'DI',
'diners_club' => 'DI'
}
def before_validate #:nodoc:
self.month = month.to_i
self.year = year.to_i
end
# Litle XML Reference Guide 1.8.2
#
# The length of the original card number is reflected in the token, so a
# submitted 16-digit number results in a 16-digit token. Also, all tokens
# use only numeric characters, so you do not have to change your
# systems to accept alpha-numeric characters.
#
# The credit card token numbers themselves have two parts.
# The last four digits match the last four digits of the card number.
# The remaining digits (length can vary based upon original card number
# length) are a randomly generated.
def validate_card_token #:nodoc:
if token.to_s.length < 12 || token.to_s.match(/\A\d+\Z/).nil?
errors.add :token, "is not a valid card token"
end
end
def validate_expiration_date #:nodoc:
if !month.to_i.zero? || !year.to_i.zero?
errors.add :month, "is not a valid month" unless valid_month?(month)
errors.add :year, "is not a valid year" unless valid_expiry_year?(year)
end
end
def validate_card_brand #:nodoc:
errors.add :brand, "is invalid" unless brand.blank? || CreditCard.card_companies.keys.include?(brand)
end
def valid_month?(month)
(1..12).include?(month.to_i)
end
def valid_expiry_year?(year)
year.to_s =~ /\A\d{4}\Z/ && year.to_i > 1987
end
end
end
end
end