#!ruby19 # encoding: utf-8 module ActiveMerchant #:nodoc: module Billing #:nodoc: class SkipJackGateway < Gateway API_VERSION = '?.?' LIVE_HOST = "https://www.skipjackic.com" TEST_HOST = "https://developer.skipjackic.com" BASIC_PATH = "/scripts/evolvcc.dll" ADVANCED_PATH = "/evolvcc/evolvcc.aspx" ACTIONS = { :authorization => 'AuthorizeAPI', :change_status => 'SJAPI_TransactionChangeStatusRequest', :get_status => 'SJAPI_TransactionStatusRequest' } SUCCESS_MESSAGE = 'The transaction was successful.' MONETARY_CHANGE_STATUSES = ['AUTHORIZE', 'AUTHORIZE ADDITIONAL', 'CREDIT', 'SPLITSETTLE'] CARD_CODE_ERRORS = %w( N S "" ) CARD_CODE_MESSAGES = { "M" => "Card verification number matched", "N" => "Card verification number didn't match", "P" => "Card verification number was not processed", "S" => "Card verification number should be on card but was not indicated", "U" => "Issuer was not certified for card verification", "" => "Transaction failed because incorrect card verification number was entered or no number was entered" } AVS_ERRORS = %w( A B C E I N O P R W Z ) AVS_MESSAGES = { "A" => "Street address matches billing information, zip/postal code does not", "B" => "Street address match for international transaction. Postal code not verified due to incompatible formats", "C" => "Street address and postal code not verified for internation transaction due to incompatible formats", "D" => "Street address and postal code match for international transaction", "E" => "Address verification service error", "I" => "Address information not verified by international issuer", "M" => "Street address and postal code match for international transaction", "N" => "Neither street address nor zip/postal match billing information", "O" => "Non-US issuer does not participate", "P" => "Postal codes match for international transaction but street address not verified due to incompatible formats", "P" => "Address verification not applicable for this transaction", "R" => "Payment gateway was unavailable or timed out", "S" => "Address verification service not supported by issuer", "U" => "Address information is unavailable", "W" => "9-digit zip/postal code matches billing information, street address does not", "X" => "Street address and 9-digit zip/postal code matches billing information", "Y" => "Street address and 5-digit zip/postal code matches billing information", "Z" => "5-digit zip/postal code matches billing information, street address does not", } CHANGE_STATUS_ERROR_MESSAGES = { '0' => 'Success', '-1' => 'Invalid Command', '-2' => 'Parameter Missing', '-3' => 'Failed retrieving response', '-4' => 'Invalid Status', '-5' => 'Failed reading security flags', '-6' => 'Developer serial number not found', '-7' => 'Invalid Serial Number' } TRANSACTION_CURRENT_STATUS = { '0' => 'Idle', '1' => 'Authorized', '2' => 'Denied', '3' => 'Settled', '4' => 'Credited', '5' => 'Deleted', '6' => 'Archived', '7' => 'Pre-Authorized', '8' => 'Split Settled' } TRANSACTION_PENDING_STATUS = { '0' => 'Idle', '1' => 'Pending Credit', '2' => 'Pending Settlement', '3' => 'Pending Authorization', '4' => 'Pending Manual Settlement', '5' => 'Pending Recurring' } RETURN_CODE_MESSAGES = { '-1' => 'Data was not by received intact by Skipjack Transaction Network.', '0' => 'Communication Failure. Error in Request and Response at IP level.', '1' => 'Valid Data. Authorization request was valid.', '-35' => 'Invalid credit card number. Retry with correct credit card number.', '-37' => 'Merchant Processor Unavailable. Skipjack is unable to communicate with payment Processor. Retry', '-39' => 'Length or value of HTML Serial. Number Invalid serial number. Check HTML Serial Number length and that it is a correct/valid number. Confirm you are sending to the correct environment (Development or Production)', '-51' => 'The value or length for billing zip code is incorrect.', '-52' => 'The value or length for shipping zip code is incorrect.', '-53' => 'The value or length for credit card expiration month is incorrect.', '-54' => 'The value or length of the month or year of the credit card account number was incorrect.', '-55' => 'The value or length or billing street address is incorrect.', '-56' => 'The value or length of the shipping address is incorrect.', '-57' => 'The length of the transaction amount must be at least 3 digits long (excluding the decimal place).', '-58' => 'Length or value in Merchant Name Merchant Name associated with Skipjack account is misconfigured or invalid', '-59' => 'Length or value in Merchant Address Merchant Address associated with Skipjack account is misconfigured or invalid', '-60' => 'Length or value in Merchant State Merchant State associated with Skipjack account is misconfigured or invalid', '-61' => 'The value or length for shipping state/province is empty.', '-62' => 'The value for length orderstring is empty.', '-64' => 'The value for the phone number is incorrect.', '-65' => 'The value or length for billing name is empty.', '-66' => 'The value or length for billing e-mail is empty.', '-67' => 'The value or length for billing street address is empty.', '-68' => 'The value or length for billing city is empty.', '-69' => 'The value or length for billing state is empty.', '-70' => 'Empty zipcode Zip Code field is empty.', '-71' => 'Empty ordernumber Ordernumber field is empty.', '-72' => 'Empty accountnumber Accountnumber field is empty', '-73' => 'Empty month Month field is empty.', '-74' => 'Empty year Year field is empty.', '-75' => 'Empty serialnumber Serialnumber field is empty.', '-76' => 'Empty transactionamount Transaction amount field is empty.', '-77' => 'Empty orderstring Orderstring field is empty.', '-78' => 'Empty shiptophone Shiptophone field is empty.', '-79' => 'The value or length for billing name is empty.', '-80' => 'Length shipto name Error in the length or value of shiptophone.', '-81' => 'Length or value of Customer location', '-82' => 'The value or length for billing state is empty.', '-83' => 'The value or length for shipping phone is empty.', '-84' => 'There is already an existing pending transaction in the register sharing the posted Order Number.', '-85' => 'Airline leg info invalid Airline leg field value is invalid or empty.', '-86' => 'Airline ticket info invalid Airline ticket info field is invalid or empty', '-87' => 'Point of Sale check routing number must be 9 numeric digits Point of Sale check routing number is invalid or empty.', '-88' => 'Point of Sale check account number missing or invalid Point of Sale check account number is invalid or empty.', '-89' => 'Point of Sale check MICR missing or invalid Point of Sale check MICR invalid or empty.', '-90' => 'Point of Sale check number missing or invalid Point of Sale check number invalid or empty.', '-91' => 'CVV2 Invalid or empty "Make CVV a required field feature" enabled (New feature 01 April 2006) in the Merchant Account Setup interface but no CVV code was sent in the transaction data.', '-92' => 'Approval Code Invalid Approval Code Invalid. Approval Code is a 6 digit code.', '-93' => 'Blind Credits Request Refused "Allow Blind Credits" option must be enabled on the Skipjack Merchant Account.', '-94' => 'Blind Credits Failed', '-95' => 'Voice Authorization Request Refused Voice Authorization option must be enabled on the Skipjack Merchant Account.', '-96' => 'Voice Authorizations Failed', '-97' => 'Fraud Rejection Violates Velocity Settling.', '-98' => 'Invalid Discount Amount', '-99' => 'POS PIN Debit Pin Block Debit-specific', '-100' => 'POS PIN Debit Invalid Key Serial Number Debit-specific', '-101' => 'Invalid Authentication Data Data for Verified by Visa/MC Secure Code is invalid.', '-102' => 'Authentication Data Not Allowed', '-103' => 'POS Check Invalid Birth Date POS check dateofbirth variable contains a birth date in an incorrect format. Use MM/DD/YYYY format for this variable.', '-104' => 'POS Check Invalid Identification Type POS check identificationtype variable contains a identification type value which is invalid. Use the single digit value where Social Security Number=1, Drivers License=2 for this variable.', '-105' => 'Invalid trackdata Track Data is in invalid format.', '-106' => 'POS Check Invalid Account Type', '-107' => 'POS PIN Debit Invalid Sequence Number', '-108' => 'Invalid Transaction ID', '-109' => 'Invalid From Account Type', '-110' => 'Pos Error Invalid To Account Type', '-112' => 'Pos Error Invalid Auth Option', '-113' => 'Pos Error Transaction Failed', '-114' => 'Pos Error Invalid Incoming Eci', '-115' => 'POS Check Invalid Check Type', '-116' => 'POS Check Invalid Lane Number POS Check lane or cash register number is invalid. Use a valid lane or cash register number that has been configured in the Skipjack Merchant Account.', '-117' => 'POS Check Invalid Cashier Number' } self.supported_countries = ['US', 'CA'] self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover, :diners_club] self.homepage_url = 'http://www.skipjack.com/' self.display_name = 'SkipJack' # Creates a new SkipJackGateway # # The gateway requires that a valid login and password be passed # in the +options+ hash. # # ==== Options # # * :login -- The SkipJack Merchant Serial Number. # * :password -- The SkipJack Developer Serial Number. # * :test => +true+ or +false+ -- Use the test or live SkipJack url. # * :advanced => +true+ or +false+ -- Set to true if you're using an advanced processor # See the SkipJack Integration Guide for details. (default: +false+) def initialize(options = {}) requires!(options, :login, :password) @options = options super end def test? @options[:test] || super end def authorize(money, creditcard, options = {}) requires!(options, :order_id, :email) post = {} add_invoice(post, options) add_creditcard(post, creditcard) add_address(post, options) add_customer_data(post, options) commit(:authorization, money, post) end def purchase(money, creditcard, options = {}) post = {} authorization = authorize(money, creditcard, options) if authorization.success? capture(money, authorization.authorization) else authorization end end # Captures the funds from an authorized transaction. # # ==== Parameters # # * money -- The amount to be capture as an Integer in cents. # * authorization -- The authorization returned from the previous authorize request. # * options -- A hash of optional parameters. # # ==== Options # # * :force_settlement -- Force the settlement to occur as soon as possible. This option is not supported by other gateways. See the SkipJack API reference for more details def capture(money, authorization, options = {}) post = { } add_status_action(post, 'SETTLE') add_forced_settlement(post, options) add_transaction_id(post, authorization) commit(:change_status, money, post) end def void(authorization, options = {}) post = {} add_status_action(post, 'DELETE') add_forced_settlement(post, options) add_transaction_id(post, authorization) commit(:change_status, nil, post) end def refund(money, identification, options = {}) post = {} add_status_action(post, 'CREDIT') add_forced_settlement(post, options) add_transaction_id(post, identification) commit(:change_status, money, post) end def credit(money, identification, options = {}) deprecated CREDIT_DEPRECATION_MESSAGE refund(money, identification, options) end def status(order_id) commit(:get_status, nil, :szOrderNumber => order_id) end private def advanced? @options[:advanced] end def add_forced_settlement(post, options) post[:szForceSettlement] = options[:force_settlment] ? 1 : 0 end def add_status_action(post, action) post[:szDesiredStatus] = action end def commit(action, money, parameters) response = parse( ssl_post( url_for(action), post_data(action, money, parameters) ), action ) # Pass along the original transaction id in the case an update transaction Response.new(response[:success], message_from(response, action), response, :test => test?, :authorization => response[:szTransactionFileName] || parameters[:szTransactionId], :avs_result => { :code => response[:szAVSResponseCode] }, :cvv_result => response[:szCVV2ResponseCode] ) end def url_for(action) result = test? ? TEST_HOST : LIVE_HOST result += advanced? && action == :authorization ? ADVANCED_PATH : BASIC_PATH result += "?#{ACTIONS[action]}" end def add_credentials(params, action) if action == :authorization params[:SerialNumber] = @options[:login] params[:DeveloperSerialNumber] = @options[:password] else params[:szSerialNumber] = @options[:login] params[:szDeveloperSerialNumber] = @options[:password] end end def add_amount(params, action, money) if action == :authorization params[:TransactionAmount] = amount(money) else params[:szAmount] = amount(money) if MONETARY_CHANGE_STATUSES.include?(params[:szDesiredStatus]) end end def parse(body, action) case action when :authorization parse_authorization_response(body) when :get_status parse_status_response(body, [ :SerialNumber, :TransactionAmount, :TransactionStatusCode, :TransactionStatusMessage, :OrderNumber, :TransactionDateTime, :TransactionID, :ApprovalCode, :BatchNumber ]) else parse_status_response(body, [ :SerialNumber, :TransactionAmount, :DesiredStatus, :StatusResponse, :StatusResponseMessage, :OrderNumber, :AuditID ]) end end def split_lines(body) body.split(/[\r\n]+/) end def split_line(line) line.split(/","/).collect { |key| key.sub(/"*([^"]*)"*/, '\1').strip; } end def authorize_response_map(body) lines = split_lines(body) keys, values = split_line(lines[0]), split_line(lines[1]) Hash[*(keys.zip(values).flatten)].symbolize_keys end def parse_authorization_response(body) result = authorize_response_map(body) result[:success] = (result[:szIsApproved] == '1') result end def parse_status_response(body, response_keys) lines = split_lines(body) keys = [ :szSerialNumber, :szErrorCode, :szNumberRecords] values = split_line(lines[0])[0..2] result = Hash[*(keys.zip(values).flatten)] result[:szErrorMessage] = '' result[:success] = (result[:szErrorCode] == '0') if result[:success] lines[1..-1].each do |line| values = split_line(line) response_keys.each_with_index do |key, index| result[key] = values[index] end end else result[:szErrorMessage] = lines[1] end result end def post_data(action, money, params = {}) add_credentials(params, action) add_amount(params, action, money) sorted_params = params.to_a.sort{|a,b| a.to_s <=> b.to_s}.reverse sorted_params.collect { |key, value| "#{key.to_s}=#{CGI.escape(value.to_s)}" }.join("&") end def add_transaction_id(post, transaction_id) post[:szTransactionId] = transaction_id end def add_invoice(post, options) post[:OrderNumber] = sanitize_order_id(options[:order_id]) post[:CustomerCode] = options[:customer].to_s.slice(0, 17) post[:InvoiceNumber] = options[:invoice] post[:OrderDescription] = options[:description] if order_items = options[:items] post[:OrderString] = order_items.collect { |item| "#{item[:sku]}~#{item[:description].tr('~','-')}~#{item[:declared_value]}~#{item[:quantity]}~#{item[:taxable]}~~~~~~~~#{item[:tax_rate]}~||"}.join else post[:OrderString] = '1~None~0.00~0~N~||' end end def add_creditcard(post, creditcard) post[:AccountNumber] = creditcard.number post[:Month] = creditcard.month post[:Year] = creditcard.year post[:CVV2] = creditcard.verification_value if creditcard.verification_value? post[:SJName] = creditcard.name end def add_customer_data(post, options) post[:Email] = options[:email] end def add_address(post, options) if address = options[:billing_address] || options[:address] post[:StreetAddress] = address[:address1] post[:StreetAddress2] = address[:address2] post[:City] = address[:city] post[:State] = address[:state] post[:ZipCode] = address[:zip] post[:Country] = address[:country] post[:Phone] = address[:phone] post[:Fax] = address[:fax] end if address = options[:shipping_address] post[:ShipToName] = address[:name] post[:ShipToStreetAddress] = address[:address1] post[:ShipToStreetAddress2] = address[:address2] post[:ShipToCity] = address[:city] post[:ShipToState] = address[:state] post[:ShipToZipCode] = address[:zip] post[:ShipToCountry] = address[:country] post[:ShipToPhone] = address[:phone] post[:ShipToFax] = address[:fax] end # The phone number for the shipping address is required # Use the billing address phone number if a shipping address # phone number wasn't provided post[:ShipToPhone] = post[:Phone] if post[:ShipToPhone].blank? end def message_from(response, action) case action when :authorization message_from_authorization(response) when :get_status message_from_status(response) else message_from_status(response) end end def message_from_authorization(response) if response[:success] return SUCCESS_MESSAGE else return CARD_CODE_MESSAGES[response[:szCVV2ResponseCode]] if CARD_CODE_ERRORS.include?(response[:szCVV2ResponseCode]) return AVS_MESSAGES[response[:szAVSResponseMessage]] if AVS_ERRORS.include?(response[:szAVSResponseCode]) return RETURN_CODE_MESSAGES[response[:szReturnCode]] if response[:szReturnCode] != '1' return response[:szAuthorizationDeclinedMessage] end end def message_from_status(response) response[:success] ? SUCCESS_MESSAGE : response[:szErrorMessage] end def sanitize_order_id(value) value.to_s.gsub(/[^\w.]/, '') end end end end