begin
require "vindicia-api"
rescue LoadError
raise "Could not load the vindicia-api gem. Use `gem install vindicia-api` to install it."
end
require 'i18n/core_ext/string/interpolate'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# For more information on the Vindicia Gateway please visit their {website}[http://vindicia.com/]
#
# The login and password are not the username and password you use to
# login to the Vindicia Merchant Portal.
#
# ==== Recurring Billing
#
# AutoBills are an feature of Vindicia's API that allows for creating and managing subscriptions.
#
# For more information about Vindicia's API and various other services visit their {Resource Center}[http://www.vindicia.com/resources/index.html]
class VindiciaGateway < Gateway
self.supported_countries = %w{US CA GB AU MX BR DE KR CN HK}
self.supported_cardtypes = [:visa, :master, :american_express, :discover]
self.homepage_url = 'http://www.vindicia.com/'
self.display_name = 'Vindicia'
class_attribute :test_url, :live_url
self.test_url = "https://soap.prodtest.sj.vindicia.com/soap.pl"
self.live_url = "http://soap.vindicia.com/soap.pl"
# Creates a new VindiciaGateway
#
# The gateway requires that a valid login and password be passed
# in the +options+ hash.
#
# ==== Options
#
# * :login -- Vindicia SOAP login (REQUIRED)
# * :password -- Vindicia SOAP password (REQUIRED)
# * :api_version -- Vindicia API Version - defaults to 3.6 (OPTIONAL)
# * :account_id -- Account Id which all transactions will be run against. (REQUIRED)
# * :transaction_prefix -- Prefix to order id for one-time transactions - defaults to 'X' (OPTIONAL
# * :min_chargeback_probability -- Minimum score for chargebacks - defaults to 65 (OPTIONAL)
# * :cvn_success -- Array of valid CVN Check return values - defaults to [M, P] (OPTIONAL)
# * :avs_success -- Array of valid AVS Check return values - defaults to [X, Y, A, W, Z] (OPTIONAL)
def initialize(options = {})
requires!(options, :login, :password)
super
config = lambda do |config|
config.login = options[:login]
config.password = options[:password]
config.api_version = options[:api_version] || "3.6"
config.endpoint = test? ? self.test_url : self.live_url
config.namespace = "http://soap.vindicia.com"
end
if Vindicia.config.is_configured?
config.call(Vindicia.config)
else
Vindicia.configure(&config)
end
requires!(options, :account_id)
@account_id = options[:account_id]
@transaction_prefix = options[:transaction_prefix] || "X"
@min_chargeback_probability = options[:min_chargeback_probability] || 65
@cvn_success = options[:cvn_success] || %w{M P}
@avs_success = options[:avs_success] || %w{X Y A W Z}
@allowed_authorization_statuses = %w{Authorized}
end
# Perform a purchase, which is essentially an authorization and capture in a single operation.
#
# ==== Parameters
#
# * money -- The amount to be purchased as an Integer value in cents.
# * creditcard -- The CreditCard details for the transaction.
# * options -- A hash of optional parameters.
def purchase(money, creditcard, options = {})
response = authorize(money, creditcard, options)
return response if !response.success? || response.fraud_review?
capture(money, response.authorization, options)
end
# Performs an authorization, which reserves the funds on the customer's credit card, but does not
# charge the card.
#
# ==== Parameters
#
# * money -- The amount to be authorized as an Integer value in cents.
# * creditcard -- The CreditCard details for the transaction.
# * options -- A hash of optional parameters.
def authorize(money, creditcard, options = {})
vindicia_transaction = authorize_transaction(money, creditcard, options)
response = check_transaction(vindicia_transaction)
# if this response is under fraud review because of our AVS/CVV checks void the transaction
if !response.success? && response.fraud_review? && !response.authorization.blank?
void_response = void([vindicia_transaction[:transaction][:merchantTransactionId]], options)
if void_response.success?
return response
else
return void_response
end
end
response
end
# Captures the funds from an authorized transaction.
#
# ==== Parameters
#
# * money -- The amount to be captured as an Integer value in cents.
# * identification -- The authorization returned from the previous authorize request.
def capture(money, identification, options = {})
response = post(Vindicia::Transaction.capture({
:transactions => [{ :merchantTransactionId => identification }]
}))
if response[:return][:returnCode] != '200' || response[:qtyFail].to_i > 0
return fail(response)
end
success(response, identification)
end
# Void a previous transaction
#
# ==== Parameters
#
# * identification - The authorization returned from the previous authorize request.
# * options - Extra options (currently only :ip used)
def void(identification, options = {})
response = post(Vindicia::Transaction.cancel({
:transactions => [{
:account => { :merchantAccountId => @account_id },
:merchantTransactionId => identification,
:sourceIp => options[:ip]
}]
}))
if response[:return][:returnCode] == '200' && response[:qtyFail].to_i == 0
success(response, identification)
else
fail(response)
end
end
# Perform a recurring billing, which is essentially a purchase and autobill setup in a single operation.
#
# ==== Parameters
#
# * money -- The amount to be purchased as an Integer value in cents.
# * creditcard -- The CreditCard details for the transaction.
# * options -- A hash of parameters.
#
# ==== Options
#
# * :product_sku -- The subscription product's sku
# * :autobill_prefix -- Prefix to order id for subscriptions - defaults to 'A' (OPTIONAL)
def recurring(money, creditcard, options={})
options[:recurring] = true
@autobill_prefix = options[:autobill_prefix] || "A"
response = authorize(money, creditcard, options)
return response if !response.success? || response.fraud_review?
capture_resp = capture(money, response.authorization, options)
return capture_resp if !response.success?
# Setting up a recurring AutoBill requires an associated product
requires!(options, :product_sku)
autobill_response = check_subscription(authorize_subscription(options.merge(:product_sku => options[:product_sku])))
if autobill_response.success?
autobill_response
else
# If the AutoBill fails to set-up, void the transaction and return it as the response
void_response = void(capture_resp.authorization, options)
if void_response.success?
return autobill_response
else
return void_response
end
end
end
protected
def post(body)
parse(ssl_post(Vindicia.config.endpoint, body, "Content-Type" => "text/xml"))
end
def parse(response)
# Vindicia always returns in the form of request_type_response => { actual_response }
Hash.from_xml(response)["Envelope"]["Body"].values.first.with_indifferent_access
end
def check_transaction(vindicia_transaction)
if vindicia_transaction[:return][:returnCode] == '200'
status_log = vindicia_transaction[:transaction][:statusLog].first
if status_log[:creditCardStatus]
avs = status_log[:creditCardStatus][:avsCode]
cvn = status_log[:creditCardStatus][:cvnCode]
end
if @allowed_authorization_statuses.include?(status_log[:status]) &&
check_cvn(cvn) && check_avs(avs)
success(vindicia_transaction,
vindicia_transaction[:transaction][:merchantTransactionId],
avs, cvn)
else
# If the transaction is authorized, but it didn't pass our AVS/CVV checks send the authorization along so
# that is gets voided. Otherwise, send no authorization.
fail(vindicia_transaction, avs, cvn, false,
@allowed_authorization_statuses.include?(status_log[:status]) ? vindicia_transaction[:transaction][:merchantTransactionId] : "")
end
else
# 406 = Chargeback risk score is higher than minChargebackProbability, transaction not authorized.
fail(vindicia_transaction, nil, nil, vindicia_transaction[:return][:return_code] == '406')
end
end
def authorize_transaction(money, creditcard, options)
parameters = {
:amount => amount(money),
:currency => options[:currency] || currency(money)
}
add_account_data(parameters, options)
add_customer_data(parameters, options)
add_payment_source(parameters, creditcard, options)
post(Vindicia::Transaction.auth({
:transaction => parameters,
:minChargebackProbability => @min_chargeback_probability
}))
end
def add_account_data(parameters, options)
parameters[:account] = { :merchantAccountId => @account_id }
parameters[:sourceIp] = options[:ip] if options[:ip]
end
def add_customer_data(parameters, options)
parameters[:merchantTransactionId] = transaction_id(options[:order_id])
parameters[:shippingAddress] = convert_am_address_to_vindicia(options[:shipping_address])
# Transaction items must be provided for tax purposes
requires!(options, :line_items)
parameters[:transactionItems] = options[:line_items]
if options[:recurring]
parameters[:nameValues] = [{:name => 'merchantAutoBillIdentifier', :value => autobill_id(options[:order_id])}]
end
end
def add_payment_source(parameters, creditcard, options)
parameters[:sourcePaymentMethod] = {
:type => 'CreditCard',
:creditCard => { :account => creditcard.number, :expirationDate => "%4d%02d" % [creditcard.year, creditcard.month] },
:accountHolderName => creditcard.name,
:nameValues => [{ :name => 'CVN', :value => creditcard.verification_value }],
:billingAddress => convert_am_address_to_vindicia(options[:billing_address] || options[:address]),
:customerSpecifiedType => creditcard.brand.capitalize,
:active => !!options[:recurring]
}
end
def authorize_subscription(options)
parameters = {}
add_account_data(parameters, options)
add_subscription_information(parameters, options)
post(Vindicia::AutoBill.update({
:autobill => parameters,
:validatePaymentMethod => false,
:minChargebackProbability => 100
}))
end
def check_subscription(vindicia_transaction)
if vindicia_transaction[:return][:returnCode] == '200'
if vindicia_transaction[:autobill] && vindicia_transaction[:autobill][:status] == "Active"
success(vindicia_transaction,
vindicia_transaction[:autobill][:merchantAutoBillId])
else
fail(vindicia_transaction)
end
else
fail(vindicia_transaction)
end
end
def add_subscription_information(parameters, options)
requires!(options, :product_sku)
if options[:shipping_address]
parameters[:account][:shipping_address] = options[:shipping_address]
end
parameters[:merchantAutoBillId] = autobill_id(options[:order_id])
parameters[:product] = { :merchantProductId => options[:product_sku] }
end
def check_avs(avs)
avs.blank? || @avs_success.include?(avs)
end
def check_cvn(cvn)
cvn.blank? || @cvn_success.include?(cvn)
end
def success(response, authorization, avs_code = nil, cvn_code = nil)
ActiveMerchant::Billing::Response.new(true, response[:return][:returnString], response,
{ :fraud_review => false, :authorization => authorization, :test => test?,
:avs_result => { :code => avs_code }, :cvv_result => cvn_code })
end
def fail(response, avs_code = nil, cvn_code = nil, fraud_review = false, authorization = "")
ActiveMerchant::Billing::Response.new(false, response[:return][:returnString], response,
{ :fraud_review => fraud_review || !authorization.blank?,
:authorization => authorization, :test => test?,
:avs_result => { :code => avs_code }, :cvv_result => cvn_code })
end
def autobill_id(order_id)
"#{@autobill_prefix}#{order_id}"
end
def transaction_id(order_id)
"#{@transaction_prefix}#{order_id}"
end
# Converts valid ActiveMerchant address hash to proper Vindicia format
def convert_am_address_to_vindicia(address)
return if address.nil?
convs = { :address1 => :addr1, :address2 => :addr2,
:state => :district, :zip => :postalCode }
vindicia_address = {}
address.each do |key, val|
vindicia_address[convs[key] || key] = val
end
vindicia_address
end
end
end
end