require 'rexml/document'
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# Initialization Options
# :login Your store number
# :pem The text of your linkpoint PEM file. Note
# this is not the path to file, but its
# contents. If you are only using one PEM
# file on your site you can declare it
# globally and then you won't need to
# include this option
#
#
# A valid store number is required. Unfortunately, with LinkPoint
# YOU CAN'T JUST USE ANY OLD STORE NUMBER. Also, you can't just
# generate your own PEM file. You'll need to use a special PEM file
# provided by LinkPoint.
#
# Go to http://www.linkpoint.com/support/sup_teststore.asp to set up
# a test account and obtain your PEM file.
#
# Declaring PEM file Globally
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
#
# Valid Order Options
# :result =>
# LIVE Production mode
# GOOD Approved response in test mode
# DECLINE Declined response in test mode
# DUPLICATE Duplicate response in test mode
#
# :ponumber Order number
#
# :transactionorigin => Source of the transaction
# ECI Email or Internet
# MAIL Mail order
# MOTO Mail order/Telephone
# TELEPHONE Telephone
# RETAIL Face-to-face
#
# :ordertype =>
# SALE Real live sale
# PREAUTH Authorize only
# POSTAUTH Forced Ticket or Ticket Only transaction
# VOID
# CREDIT
# CALCSHIPPING For shipping charges calculations
# CALCTAX For sales tax calculations
#
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# MONTHLY
# BIMONTHLY
# WEEKLY
# BIWEEKLY
# YEARLY
# DAILY
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
#
# For reference:
#
# https://www.linkpointcentral.com/lpc/docs/Help/APIHelp/lpintguide.htm
#
# Entities = {
# :payment => [:subtotal, :tax, :vattax, :shipping, :chargetotal],
# :billing => [:name, :address1, :address2, :city, :state, :zip, :country, :email, :phone, :fax, :addrnum],
# :shipping => [:name, :address1, :address2, :city, :state, :zip, :country, :weight, :items, :carrier, :total],
# :creditcard => [:cardnumber, :cardexpmonth, :cardexpyear, :cvmvalue, :track],
# :telecheck => [:routing, :account, :checknumber, :bankname, :bankstate, :dl, :dlstate, :void, :accounttype, :ssn],
# :transactiondetails => [:transactionorigin, :oid, :ponumber, :taxexempt, :terminaltype, :ip, :reference_number, :recurring, :tdate],
# :periodic => [:action, :installments, :threshold, :startdate, :periodicity, :comments],
# :notes => [:comments, :referred]
# :items => [:item => [:price, :quantity, :description, :id, :options => [:option => [:name, :value]]]]
# }
#
#
# LinkPoint's Items entity is an optional entity that can be attached to orders.
# It is entered as :line_items to be consistent with the CyberSource implementation
#
# The line_item hash goes in the options hash and should look like
#
# :line_items => [
# {
# :id => '123456',
# :description => 'Logo T-Shirt',
# :price => '12.00',
# :quantity => '1',
# :options => [
# {
# :name => 'Color',
# :value => 'Red'
# },
# {
# :name => 'Size',
# :value => 'XL'
# }
# ]
# },
# {
# :id => '111',
# :description => 'keychain',
# :price => '3.00',
# :quantity => '1'
# }
# ]
# This functionality is only supported by this particular gateway may
# be changed at any time
#
class LinkpointGateway < Gateway
# Your global PEM file. This will be assigned to you by linkpoint
#
# Example:
#
# ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' )
#
cattr_accessor :pem_file
self.test_url = 'https://staging.linkpt.net:1129/'
self.live_url = 'https://secure.linkpt.net:1129/'
self.supported_countries = ['US']
self.supported_cardtypes = %i[visa master american_express discover jcb diners_club]
self.homepage_url = 'http://www.linkpoint.com/'
self.display_name = 'LinkPoint'
def initialize(options = {})
requires!(options, :login)
@options = {
result: 'LIVE',
pem: LinkpointGateway.pem_file
}.update(options)
raise ArgumentError, "You need to pass in your pem file using the :pem parameter or set it globally using ActiveMerchant::Billing::LinkpointGateway.pem_file = File.read( File.dirname(__FILE__) + '/../mycert.pem' ) or similar" if @options[:pem].blank?
@options[:pem].strip!
end
# Send a purchase request with periodic options
# Recurring Options
# :action =>
# SUBMIT
# MODIFY
# CANCEL
#
# :installments Identifies how many recurring payments to charge the customer
# :startdate Date to begin charging the recurring payments. Format: YYYYMMDD or "immediate"
# :periodicity =>
# :monthly
# :bimonthly
# :weekly
# :biweekly
# :yearly
# :daily
# :threshold Tells how many times to retry the transaction (if it fails) before contacting the merchant.
# :comments Uh... comments
#
def recurring(money, creditcard, options={})
ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE
requires!(options, %i[periodicity bimonthly monthly biweekly weekly yearly daily], :installments, :order_id)
options.update(
ordertype: 'SALE',
action: options[:action] || 'SUBMIT',
installments: options[:installments] || 12,
startdate: options[:startdate] || 'immediate',
periodicity: options[:periodicity].to_s || 'monthly',
comments: options[:comments] || nil,
threshold: options[:threshold] || 3
)
commit(money, creditcard, options)
end
# Buy the thing
def purchase(money, creditcard, options={})
requires!(options, :order_id)
options.update(
ordertype: 'SALE'
)
commit(money, creditcard, options)
end
#
# Authorize the transaction
#
# Reserves the funds on the customer's credit card, but does not charge the card.
#
def authorize(money, creditcard, options = {})
requires!(options, :order_id)
options.update(
ordertype: 'PREAUTH'
)
commit(money, creditcard, options)
end
#
# Post an authorization.
#
# Captures the funds from an authorized transaction.
# Order_id must be a valid order id from a prior authorized transaction.
#
def capture(money, authorization, options = {})
options.update(
order_id: authorization,
ordertype: 'POSTAUTH'
)
commit(money, nil, options)
end
# Void a previous transaction
def void(identification, options = {})
options.update(
order_id: identification,
ordertype: 'VOID'
)
commit(nil, nil, options)
end
#
# Refund an order
#
# identification must be a valid order id previously submitted by SALE
#
def refund(money, identification, options = {})
options.update(
ordertype: 'CREDIT',
order_id: identification
)
commit(money, nil, options)
end
def credit(money, identification, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, identification, options)
end
def supports_scrubbing
true
end
def scrub(transcript)
transcript.
gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
gsub(%r(()\d+())i, '\1[FILTERED]\2').
gsub(%r(()\d+())i, '\1[FILTERED]\2')
end
private
# Commit the transaction by posting the XML file to the LinkPoint server
def commit(money, creditcard, options = {})
response = parse(ssl_post(test? ? self.test_url : self.live_url, post_data(money, creditcard, options)))
Response.new(successful?(response), response[:message], response,
test: test?,
authorization: response[:ordernum],
avs_result: { code: response[:avs].to_s[2, 1] },
cvv_result: response[:avs].to_s[3, 1]
)
end
def successful?(response)
response[:approved] == 'APPROVED'
end
# Build the XML file
def post_data(money, creditcard, options)
params = parameters(money, creditcard, options)
xml = REXML::Document.new
order = xml.add_element('order')
# Merchant Info
merchantinfo = order.add_element('merchantinfo')
merchantinfo.add_element('configfile').text = @options[:login]
# Loop over the params hash to construct the XML string
for key, value in params
elem = order.add_element(key.to_s)
if key == :items
build_items(elem, value)
else
for k, _ in params[key]
elem.add_element(k.to_s).text = params[key][k].to_s if params[key][k]
end
end
# Linkpoint doesn't understand empty elements:
order.delete(elem) if elem.size == 0
end
return xml.to_s
end
# adds LinkPoint's Items entity to the XML. Called from post_data
def build_items(element, items)
for item in items
item_element = element.add_element('item')
for key, value in item
if key == :options
options_element = item_element.add_element('options')
for option in value
opt_element = options_element.add_element('option')
opt_element.add_element('name').text = option[:name] unless option[:name].blank?
opt_element.add_element('value').text = option[:value] unless option[:value].blank?
end
else
item_element.add_element(key.to_s).text = item[key].to_s unless item[key].blank?
end
end
end
end
# Set up the parameters hash just once so we don't have to do it
# for every action.
def parameters(money, creditcard, options = {})
params = {
payment: {
subtotal: amount(options[:subtotal]),
tax: amount(options[:tax]),
vattax: amount(options[:vattax]),
shipping: amount(options[:shipping]),
chargetotal: amount(money)
},
transactiondetails: {
transactionorigin: options[:transactionorigin] || 'ECI',
oid: options[:order_id],
ponumber: options[:ponumber],
taxexempt: options[:taxexempt],
terminaltype: options[:terminaltype],
ip: options[:ip],
reference_number: options[:reference_number],
recurring: options[:recurring] || 'NO', # DO NOT USE if you are using the periodic billing option.
tdate: options[:tdate]
},
orderoptions: {
ordertype: options[:ordertype],
result: @options[:result]
},
periodic: {
action: options[:action],
installments: options[:installments],
threshold: options[:threshold],
startdate: options[:startdate],
periodicity: options[:periodicity],
comments: options[:comments]
},
telecheck: {
routing: options[:telecheck_routing],
account: options[:telecheck_account],
checknumber: options[:telecheck_checknumber],
bankname: options[:telecheck_bankname],
dl: options[:telecheck_dl],
dlstate: options[:telecheck_dlstate],
void: options[:telecheck_void],
accounttype: options[:telecheck_accounttype],
ssn: options[:telecheck_ssn]
}
}
if creditcard
params[:creditcard] = {
cardnumber: creditcard.number,
cardexpmonth: creditcard.month,
cardexpyear: format_creditcard_expiry_year(creditcard.year),
track: nil
}
if creditcard.verification_value?
params[:creditcard][:cvmvalue] = creditcard.verification_value
params[:creditcard][:cvmindicator] = 'provided'
else
params[:creditcard][:cvmindicator] = 'not_provided'
end
end
if billing_address = options[:billing_address] || options[:address]
params[:billing] = {}
params[:billing][:name] = billing_address[:name] || (creditcard ? creditcard.name : nil)
params[:billing][:address1] = billing_address[:address1] unless billing_address[:address1].blank?
params[:billing][:address2] = billing_address[:address2] unless billing_address[:address2].blank?
params[:billing][:city] = billing_address[:city] unless billing_address[:city].blank?
params[:billing][:state] = billing_address[:state] unless billing_address[:state].blank?
params[:billing][:zip] = billing_address[:zip] unless billing_address[:zip].blank?
params[:billing][:country] = billing_address[:country] unless billing_address[:country].blank?
params[:billing][:company] = billing_address[:company] unless billing_address[:company].blank?
params[:billing][:phone] = billing_address[:phone] unless billing_address[:phone].blank?
params[:billing][:email] = options[:email] unless options[:email].blank?
end
if shipping_address = options[:shipping_address]
params[:shipping] = {}
params[:shipping][:name] = shipping_address[:name] || (creditcard ? creditcard.name : nil)
params[:shipping][:address1] = shipping_address[:address1] unless shipping_address[:address1].blank?
params[:shipping][:address2] = shipping_address[:address2] unless shipping_address[:address2].blank?
params[:shipping][:city] = shipping_address[:city] unless shipping_address[:city].blank?
params[:shipping][:state] = shipping_address[:state] unless shipping_address[:state].blank?
params[:shipping][:zip] = shipping_address[:zip] unless shipping_address[:zip].blank?
params[:shipping][:country] = shipping_address[:country] unless shipping_address[:country].blank?
end
params[:items] = options[:line_items] if options[:line_items]
return params
end
def parse(xml)
# For reference, a typical response...
#
#
#
#
#
# This is a test transaction and will not show up in the Reports
#
# Thu Feb 2 15:40:21 2006
#
#
# APPROVED
#
response = {message: 'Global Error Receipt', complete: false}
xml = REXML::Document.new("#{xml}")
xml.root&.elements&.each do |node|
response[node.name.downcase.sub(/^r_/, '').to_sym] = normalize(node.text)
end
response
end
def format_creditcard_expiry_year(year)
sprintf('%.4i', year)[-2..-1]
end
end
end
end