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