lib/active_merchant/billing/gateways/beanstream/beanstream_core.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module BeanstreamCore
include Empty
RECURRING_URL = 'https://www.beanstream.com/scripts/recurring_billing.asp'
SECURE_PROFILE_URL = 'https://www.beanstream.com/scripts/payment_profile.asp'
SP_SERVICE_VERSION = '1.1'
TRANSACTIONS = {
authorization: 'PA',
purchase: 'P',
capture: 'PAC',
refund: 'R',
void: 'VP',
check_purchase: 'D',
check_refund: 'C',
void_purchase: 'VP',
void_refund: 'VR'
}
PROFILE_OPERATIONS = {
new: 'N',
modify: 'M'
}
CVD_CODES = {
'1' => 'M',
'2' => 'N',
'3' => 'I',
'4' => 'S',
'5' => 'U',
'6' => 'P'
}
AVS_CODES = {
'0' => 'R',
'5' => 'I',
'9' => 'I'
}
PERIODS = {
days: 'D',
weeks: 'W',
months: 'M',
years: 'Y'
}
PERIODICITIES = {
daily: [:days, 1],
weekly: [:weeks, 1],
biweekly: [:weeks, 2],
monthly: [:months, 1],
bimonthly: [:months, 2],
yearly: [:years, 1]
}
RECURRING_OPERATION = {
update: 'M',
cancel: 'C'
}
STATES = {
'ALBERTA' => 'AB',
'BRITISH COLUMBIA' => 'BC',
'MANITOBA' => 'MB',
'NEW BRUNSWICK' => 'NB',
'NEWFOUNDLAND AND LABRADOR' => 'NL',
'NOVA SCOTIA' => 'NS',
'ONTARIO' => 'ON',
'PRINCE EDWARD ISLAND' => 'PE',
'QUEBEC' => 'QC',
'SASKATCHEWAN' => 'SK',
'NORTHWEST TERRITORIES' => 'NT',
'NUNAVUT' => 'NU',
'YUKON' => 'YT',
'ALABAMA' => 'AL',
'ALASKA' => 'AK',
'ARIZONA' => 'AZ',
'ARKANSAS' => 'AR',
'CALIFORNIA' => 'CA',
'COLORADO' => 'CO',
'CONNECTICUT' => 'CT',
'DELAWARE' => 'DE',
'FLORIDA' => 'FL',
'GEORGIA' => 'GA',
'HAWAII' => 'HI',
'IDAHO' => 'ID',
'ILLINOIS' => 'IL',
'INDIANA' => 'IN',
'IOWA' => 'IA',
'KANSAS' => 'KS',
'KENTUCKY' => 'KY',
'LOUISIANA' => 'LA',
'MAINE' => 'ME',
'MARYLAND' => 'MD',
'MASSACHUSETTS' => 'MA',
'MICHIGAN' => 'MI',
'MINNESOTA' => 'MN',
'MISSISSIPPI' => 'MS',
'MISSOURI' => 'MO',
'MONTANA' => 'MT',
'NEBRASKA' => 'NE',
'NEVADA' => 'NV',
'NEW HAMPSHIRE' => 'NH',
'NEW JERSEY' => 'NJ',
'NEW MEXICO' => 'NM',
'NEW YORK' => 'NY',
'NORTH CAROLINA' => 'NC',
'NORTH DAKOTA' => 'ND',
'OHIO' => 'OH',
'OKLAHOMA' => 'OK',
'OREGON' => 'OR',
'PENNSYLVANIA' => 'PA',
'RHODE ISLAND' => 'RI',
'SOUTH CAROLINA' => 'SC',
'SOUTH DAKOTA' => 'SD',
'TENNESSEE' => 'TN',
'TEXAS' => 'TX',
'UTAH' => 'UT',
'VERMONT' => 'VT',
'VIRGINIA' => 'VA',
'WASHINGTON' => 'WA',
'WEST VIRGINIA' => 'WV',
'WISCONSIN' => 'WI',
'WYOMING' => 'WY'
}
def self.included(base)
base.default_currency = 'CAD'
# The countries the gateway supports merchants from as 2 digit ISO country codes
base.supported_countries = %w[CA US]
# The card types supported by the payment gateway
base.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
# The homepage URL of the gateway
base.homepage_url = 'http://www.beanstream.com/'
base.live_url = 'https://api.na.bambora.com/scripts/process_transaction.asp'
# The name of the gateway
base.display_name = 'Beanstream.com'
end
# Only <tt>:login</tt> is required by default,
# which is the merchant's merchant ID. If you'd like to perform void,
# capture or refund transactions then you'll also need to add a username
# and password to your account under administration -> account settings ->
# order settings -> Use username/password validation
def initialize(options = {})
requires!(options, :login)
super
end
def capture(money, authorization, options = {})
reference, = split_auth(authorization)
post = {}
add_amount(post, money)
add_reference(post, reference)
add_transaction_type(post, :capture)
add_recurring_payment(post, options)
commit(post)
end
def refund(money, source, options = {})
post = {}
reference, _, type = split_auth(source)
add_reference(post, reference)
add_transaction_type(post, refund_action(type))
add_amount(post, money)
commit(post)
end
def credit(money, source, options = {})
ActiveMerchant.deprecated Gateway::CREDIT_DEPRECATION_MESSAGE
refund(money, source, options)
end
private
def purchase_action(source)
if source.is_a?(Check)
:check_purchase
else
:purchase
end
end
def add_customer_ip(post, options)
post[:customerIp] = options[:ip] if options[:ip]
end
def void_action(original_transaction_type)
original_transaction_type == TRANSACTIONS[:refund] ? :void_refund : :void_purchase
end
def refund_action(type)
type == TRANSACTIONS[:check_purchase] ? :check_refund : :refund
end
def secure_profile_action(type)
PROFILE_OPERATIONS[type] || PROFILE_OPERATIONS[:new]
end
def split_auth(string)
string.split(';')
end
def add_amount(post, money)
post[:trnAmount] = amount(money)
end
def add_original_amount(post, amount)
post[:trnAmount] = amount
end
def add_reference(post, reference)
post[:adjId] = reference
end
def add_address(post, options)
post[:ordEmailAddress] = options[:email] if options[:email]
post[:shipEmailAddress] = options[:shipping_email] || options[:email] if options[:email]
prepare_address_for_non_american_countries(options)
if billing_address = options[:billing_address] || options[:address]
post[:ordName] = billing_address[:name]
post[:ordPhoneNumber] = billing_address[:phone] || billing_address[:phone_number]
post[:ordAddress1] = billing_address[:address1]
post[:ordAddress2] = billing_address[:address2]
post[:ordCity] = billing_address[:city]
post[:ordProvince] = state_for(billing_address)
post[:ordPostalCode] = billing_address[:zip]
post[:ordCountry] = billing_address[:country]
end
if shipping_address = options[:shipping_address]
post[:shipName] = shipping_address[:name]
post[:shipPhoneNumber] = shipping_address[:phone]
post[:shipAddress1] = shipping_address[:address1]
post[:shipAddress2] = shipping_address[:address2]
post[:shipCity] = shipping_address[:city]
post[:shipProvince] = state_for(shipping_address)
post[:shipPostalCode] = shipping_address[:zip]
post[:shipCountry] = shipping_address[:country]
post[:shippingMethod] = shipping_address[:shipping_method]
post[:deliveryEstimate] = shipping_address[:delivery_estimate]
end
end
def state_for(address)
STATES[address[:state].upcase] || address[:state] if address[:state]
end
def prepare_address_for_non_american_countries(options)
[options[:billing_address], options[:shipping_address]].compact.each do |address|
next if empty?(address[:country])
unless %w[US CA].include?(address[:country])
address[:state] = '--'
address[:zip] = '000000' unless address[:zip]
end
end
end
def add_recurring_payment(post, options)
post[:recurringPayment] = 1 if options[:recurring].to_s == 'true'
end
def add_invoice(post, options)
post[:trnOrderNumber] = options[:order_id]
post[:trnComments] = options[:description]
post[:ordItemPrice] = amount(options[:subtotal])
post[:ordShippingPrice] = amount(options[:shipping])
post[:ordTax1Price] = amount(options[:tax1] || options[:tax])
post[:ordTax2Price] = amount(options[:tax2])
post[:ref1] = options[:custom]
end
def add_credit_card(post, credit_card)
if credit_card
post[:trnCardOwner] = credit_card.name
post[:trnCardNumber] = credit_card.number
post[:trnExpMonth] = format(credit_card.month, :two_digits)
post[:trnExpYear] = format(credit_card.year, :two_digits)
post[:trnCardCvd] = credit_card.verification_value
if credit_card.is_a?(NetworkTokenizationCreditCard)
post[:"3DSecureXID"] = credit_card.transaction_id
post[:"3DSecureECI"] = credit_card.eci
post[:"3DSecureCAVV"] = credit_card.payment_cryptogram
end
end
end
def add_check(post, check)
# The institution number of the consumer’s financial institution. Required for Canadian dollar EFT transactions.
post[:institutionNumber] = check.institution_number
# The bank transit number of the consumer’s bank account. Required for Canadian dollar EFT transactions.
post[:transitNumber] = check.transit_number
# The routing number of the consumer’s bank account. Required for US dollar EFT transactions.
post[:routingNumber] = check.routing_number
# The account number of the consumer’s bank account. Required for both Canadian and US dollar EFT transactions.
post[:accountNumber] = check.account_number
end
def add_secure_profile_variables(post, options = {})
post[:serviceVersion] = SP_SERVICE_VERSION
post[:responseFormat] = 'QS'
post[:cardValidation] = (options[:cardValidation].to_i == 1) || '0'
post[:operationType] = options[:operationType] || options[:operation] || secure_profile_action(:new)
post[:customerCode] = options[:billing_id] || options[:vault_id] || false
post[:status] = options[:status]
billing_address = options[:billing_address] || options[:address]
post[:trnCardOwner] = billing_address ? billing_address[:name] : nil
end
def add_recurring_amount(post, money)
post[:amount] = amount(money)
end
def add_recurring_invoice(post, options)
post[:rbApplyTax1] = options[:apply_tax1]
post[:rbApplyTax2] = options[:apply_tax2]
end
def add_recurring_operation_type(post, operation)
post[:operationType] = RECURRING_OPERATION[operation]
end
def add_recurring_service(post, options)
post[:serviceVersion] = '1.0'
post[:merchantId] = @options[:login]
post[:passCode] = @options[:recurring_api_key]
post[:rbAccountId] = options[:account_id]
end
def add_recurring_type(post, options)
# XXX requires!
post[:trnRecurring] = 1
period, increment = interval(options)
post[:rbBillingPeriod] = PERIODS[period]
post[:rbBillingIncrement] = increment
if options.include? :start_date
post[:rbCharge] = 0
post[:rbFirstBilling] = options[:start_date].strftime('%m%d%Y')
end
if count = options[:occurrences] || options[:payments]
post[:rbExpiry] = (options[:start_date] || Date.current).advance(period => count).strftime('%m%d%Y')
end
end
def interval(options)
if options.include? :periodicity
requires!(options, [:periodicity, *PERIODICITIES.keys])
PERIODICITIES[options[:periodicity]]
elsif options.include? :interval
interval = options[:interval]
if interval.respond_to? :parts
parts = interval.parts
raise ArgumentError.new("Cannot recur with mixed interval (#{interval}). Use only one of: days, weeks, months or years") if parts.length > 1
parts.first
elsif interval.kind_of? Hash
requires!(interval, :unit)
unit, length = interval.values_at(:unit, :length)
length ||= 1
[unit, length]
end
end
end
def parse(body)
results = {}
body&.split(/&/)&.each do |pair|
key, val = pair.split(/\=/)
results[key.to_sym] = val.nil? ? nil : CGI.unescape(val)
end
# Clean up the message text if there is any
if results[:messageText]
results[:messageText].gsub!(/<LI>/, '')
results[:messageText].gsub!(/(\.)?<br>/, '. ')
results[:messageText].strip!
end
results
end
def recurring_parse(data)
REXML::Document.new(data).root.elements.to_a.inject({}) do |response, element|
response[element.name.to_sym] = element.text
response
end
end
def commit(params, use_profile_api = false)
post(post_data(params, use_profile_api), use_profile_api)
end
def recurring_commit(params)
recurring_post(post_data(params, false))
end
def post(data, use_profile_api = nil)
response = parse(ssl_post((use_profile_api ? SECURE_PROFILE_URL : self.live_url), data))
response[:customer_vault_id] = response[:customerCode] if response[:customerCode]
build_response(
success?(response),
message_from(response),
response,
test: test? || response[:authCode] == 'TEST',
authorization: authorization_from(response),
cvv_result: CVD_CODES[response[:cvdId]],
avs_result: { code: AVS_CODES.include?(response[:avsId]) ? AVS_CODES[response[:avsId]] : response[:avsId] }
)
end
def recurring_post(data)
response = recurring_parse(ssl_post(RECURRING_URL, data))
build_response(recurring_success?(response), recurring_message_from(response), response)
end
def authorization_from(response)
"#{response[:trnId]};#{response[:trnAmount]};#{response[:trnType]}"
end
def message_from(response)
response[:messageText] || response[:responseMessage]
end
def recurring_message_from(response)
response[:message]
end
def recurring_success?(response)
response[:code] == '1'
end
def add_source(post, source)
if source.is_a?(String) || source.is_a?(Integer)
post[:customerCode] = source
else
card_brand(source) == 'check' ? add_check(post, source) : add_credit_card(post, source)
end
end
def add_transaction_type(post, action)
post[:trnType] = TRANSACTIONS[action]
end
def post_data(params, use_profile_api)
params[:requestType] = 'BACKEND'
if use_profile_api
params[:merchantId] = @options[:login]
params[:passCode] = @options[:secure_profile_api_key]
else
params[:username] = @options[:user] if @options[:user]
params[:password] = @options[:password] if @options[:password]
params[:merchant_id] = @options[:login]
params[:passcode] = @options[:api_key]
end
params[:vbvEnabled] = '0'
params[:scEnabled] = '0'
params.reject { |_k, v| v.blank? }.collect { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
end
end
end
end