lib/active_merchant/billing/gateways/pay_junction.rb
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
# PayJunction Gateway
#
# This gateway accepts the following arguments:
# :login => your PayJunction username
# :password => your PayJunction pass
#
# Example use:
#
# gateway = ActiveMerchant::Billing::Base.gateway(:pay_junction).new(
# :login => "my_account",
# :password => "my_pass"
# )
#
# # set up credit card obj as in main ActiveMerchant example
# creditcard = ActiveMerchant::Billing::CreditCard.new(
# :type => 'visa',
# :number => '4242424242424242',
# :month => 8,
# :year => 2009,
# :first_name => 'Bob',
# :last_name => 'Bobsen'
# )
#
# # optionally specify address if using AVS
# address = { :address1 => '101 Test Ave', :city => 'Test', :state => 'TS',
# :zip => '10101', :country => 'US' }
#
# # run request
# response = gateway.purchase(1000, creditcard, :address => address) # charge 10 dollars
#
# 1) Check whether the transaction was successful
#
# response.success?
#
# 2) Retrieve the message returned by PayJunction
#
# response.message
#
# 3) Retrieve the unique transaction ID returned by PayJunction
#
# response.authorization
#
# This gateway supports "instant" transactions. These transactions allow you
# to execute an operation on a previously run card without card information
# provided you have the transaction id from a previous transaction with the
# same card. All functions that take a credit card object for this gateway
# can take a transaction id string instead.
#
# Test Transactions
#
# See the source for initialize() for test account information. Note that
# PayJunction does not allow test transactions on your account, so if the
# gateway is running in :test mode your transaction will be run against
# PayJunction's global test account and will not show up in your account.
#
# Transactions ran on this account go through a test processor, so there is no
# need to void or otherwise cancel transactions. However, for further safety,
# please use the special card numbers 4433221111223344 or 4444333322221111 and
# keep transaction amounts below $4.00 when testing.
#
# Also note, transactions ran for an amount between $0.00 and $1.99 will likely
# result in denial. To demonstrate approvals, use amounts between $2.00 and $4.00.
#
# Test transactions can be checked by logging into
# PayJunction Web Login with username 'pj-cm-01' and password 'pj-cm-01p'
#
# Usage Details
#
# Below is a map of values accepted by PayJunction and how you should submit
# each to ActiveMerchant
#
# PayJunction Field ActiveMerchant Use
#
# dc_logon provide as :login value to gateway instantiation
# dc_password provide as :password value to gateway instantiation
#
# dc_name will be retrieved from credit_card.name
# dc_first_name :first_name on CreditCard object instantiation
# dc_last_name :last_name on CreditCard object instantiation
# dc_number :number on CreditCard object instantiation
# dc_expiration_month :month on CreditCard object instantiation
# dc_expiration_year :year on CreditCard object instantiation
# dc_verification_number :verification_value on CC object instantiation
#
# dc_transaction_amount include as argument to method for your transaction type
# dc_transaction_type do nothing, set by your transaction type
# dc_version do nothing, always "1.2"
#
# dc_transaction_id submit as a string in place of CreditCard obj for
# "instant" transactions.
#
# dc_invoice :order_id in options for transaction method
# dc_notes :description in options for transaction method
#
# See example use above for address AVS fields
# See #recurring for periodic transaction fields
class PayJunctionGateway < Gateway
API_VERSION = '1.2'
class_attribute :test_url, :live_url
self.test_url = 'https://www.payjunctionlabs.com/quick_link'
self.live_url = 'https://payjunction.com/quick_link'
TEST_LOGIN = 'pj-ql-01'
TEST_PASSWORD = 'pj-ql-01p'
SUCCESS_CODES = %w[00 85]
SUCCESS_MESSAGE = 'The transaction was approved.'
FAILURE_MESSAGE = 'The transaction was declined.'
DECLINE_CODES = {
'AE' => 'Address verification failed because address did not match.',
'ZE' => 'Address verification failed because zip did not match.',
'XE' => 'Address verification failed because zip and address did not match.',
'YE' => 'Address verification failed because zip and address did not match.',
'OE' => 'Address verification failed because address or zip did not match.',
'UE' => 'Address verification failed because cardholder address unavailable.',
'RE' => 'Address verification failed because address verification system is not working.',
'SE' => 'Address verification failed because address verification system is unavailable.',
'EE' => 'Address verification failed because transaction is not a mail or phone order.',
'GE' => 'Address verification failed because international support is unavailable.',
'CE' => 'Declined because CVV2/CVC2 code did not match.',
'04' => 'Declined. Pick up card.',
'07' => 'Declined. Pick up card (Special Condition).',
'41' => 'Declined. Pick up card (Lost).',
'43' => 'Declined. Pick up card (Stolen).',
'13' => 'Declined because of the amount is invalid.',
'14' => 'Declined because the card number is invalid.',
'80' => 'Declined because of an invalid date.',
'05' => 'Declined. Do not honor.',
'51' => 'Declined because of insufficient funds.',
'N4' => 'Declined because the amount exceeds issuer withdrawal limit.',
'61' => 'Declined because the amount exceeds withdrawal limit.',
'62' => 'Declined because of an invalid service code (restricted).',
'65' => 'Declined because the card activity limit exceeded.',
'93' => 'Declined because there a violation (the transaction could not be completed).',
'06' => 'Declined because address verification failed.',
'54' => 'Declined because the card has expired.',
'15' => 'Declined because there is no such issuer.',
'96' => 'Declined because of a system error.',
'N7' => 'Declined because of a CVV2/CVC2 mismatch.',
'M4' => 'Declined.',
'FE' => 'There was a format error with your Trinity Gateway Service (API) request.',
'LE' => 'Could not log you in (problem with dc_logon and/or dc_password).',
'NL' => 'Aborted because of a system error, please try again later. ',
'AB' => 'Aborted because of an upstream system error, please try again later.'
}
self.supported_cardtypes = %i[visa master american_express discover]
self.supported_countries = ['US']
self.homepage_url = 'http://www.payjunction.com/'
self.display_name = 'PayJunction'
def initialize(options = {})
requires!(options, :login, :password)
super
end
# The first half of the preauth(authorize)/postauth(capture) model.
# Checks to make sure funds are available for a transaction, and returns a
# transaction_id that can be used later to postauthorize (capture) the funds.
def authorize(money, payment_source, options = {})
parameters = {
transaction_amount: amount(money)
}
add_payment_source(parameters, payment_source)
add_address(parameters, options)
add_optional_fields(parameters, options)
commit('AUTHORIZATION', parameters)
end
# A simple sale, capturing funds immediately.
# Execute authorization and capture in a single step.
def purchase(money, payment_source, options = {})
parameters = {
transaction_amount: amount(money)
}
add_payment_source(parameters, payment_source)
add_address(parameters, options)
add_optional_fields(parameters, options)
commit('AUTHORIZATION_CAPTURE', parameters)
end
# The second half of the preauth(authorize)/postauth(capture) model.
# Retrieve funds that have been previously authorized with _authorization_
def capture(money, authorization, options = {})
parameters = {
transaction_id: authorization,
posture: 'capture'
}
add_optional_fields(parameters, options)
commit('update', parameters)
end
# Return money to a card that was previously billed.
# _authorization_ should be the transaction id of the transaction we are returning.
def refund(money, authorization, options = {})
parameters = {
transaction_amount: amount(money),
transaction_id: authorization
}
commit('CREDIT', parameters)
end
def credit(money, authorization, options = {})
ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end
# Cancel a transaction that has been charged but has not yet made it
# through the batch process.
def void(authorization, options = {})
parameters = {
transaction_id: authorization,
posture: 'void'
}
add_optional_fields(parameters, options)
commit('update', parameters)
end
# Set up a sale that will be made on a regular basis for the same amount
# (ex. $20 a month for 12 months)
#
# The parameter :periodicity should be specified as either :monthly, :weekly, or :daily
# The parameter :payments should be the number of payments to be made
#
# gateway.recurring('2000', creditcard, :periodicity => :monthly, :payments => 12)
#
# The optional parameter :starting_at takes a date or time argument or a string in
# YYYYMMDD format and can be used to specify when the first charge will be made.
# If omitted the first charge will be immediate.
def recurring(money, payment_source, options = {})
ActiveMerchant.deprecated RECURRING_DEPRECATION_MESSAGE
requires!(options, %i[periodicity monthly weekly daily], :payments)
periodic_type =
case options[:periodicity]
when :monthly
'month'
when :weekly
'week'
when :daily
'day'
end
if options[:starting_at].nil?
start_date = Time.now.strftime('%Y-%m-%d')
elsif options[:starting_at].is_a?(String)
sa = options[:starting_at]
start_date = "#{sa[0..3]}-#{sa[4..5]}-#{sa[6..7]}"
else
start_date = options[:starting_at].strftime('%Y-%m-%d')
end
parameters = {
transaction_amount: amount(money),
schedule_periodic_type: periodic_type,
schedule_create: 'true',
schedule_limit: options[:payments].to_i > 1 ? options[:payments] : 1,
schedule_periodic_number: 1,
schedule_start: start_date
}
add_payment_source(parameters, payment_source)
add_optional_fields(parameters, options)
add_address(parameters, options)
commit('AUTHORIZATION_CAPTURE', parameters)
end
def test?
(test_login? || super)
end
private
def test_login?
@options[:login] == TEST_LOGIN && @options[:password] == TEST_PASSWORD
end
# add fields depending on payment source selected (cc or transaction id)
def add_payment_source(params, source)
if source.is_a?(String)
add_billing_id(params, source)
else
add_creditcard(params, source)
end
end
# add fields for credit card
def add_creditcard(params, creditcard)
if creditcard.respond_to?(:track_data) && creditcard.track_data.present?
params[:track] = creditcard.track_data
else
params[:name] = creditcard.name
params[:number] = creditcard.number
params[:expiration_month] = creditcard.month
params[:expiration_year] = creditcard.year
params[:verification_number] = creditcard.verification_value if creditcard.verification_value?
end
end
# add field for "instant" transaction, using previous transaction id
def add_billing_id(params, billingid)
params[:transaction_id] = billingid
end
# add address fields if present
def add_address(params, options)
address = options[:billing_address] || options[:address]
if address
params[:address] = address[:address1] unless address[:address1].blank?
params[:city] = address[:city] unless address[:city].blank?
params[:state] = address[:state] unless address[:state].blank?
params[:zipcode] = address[:zip] unless address[:zip].blank?
params[:country] = address[:country] unless address[:country].blank?
end
end
def add_optional_fields(params, options)
params[:notes] = options[:description] unless options[:description].blank?
params[:invoice] = options[:order_id].to_s.gsub(/[^-\/\w.,']/, '') unless options[:order_id].blank?
end
def commit(action, parameters)
url = test? ? self.test_url : self.live_url
response = parse(ssl_post(url, post_data(action, parameters)))
Response.new(
successful?(response),
message_from(response),
response,
test: test?,
authorization: response[:transaction_id] || parameters[:transaction_id]
)
end
def successful?(response)
SUCCESS_CODES.include?(response[:response_code]) || response[:query_status] == true
end
def message_from(response)
if successful?(response)
SUCCESS_MESSAGE
else
DECLINE_CODES[response[:response_code]] || FAILURE_MESSAGE
end
end
def post_data(action, params)
if test?
# test requests must use global test account
params[:logon] = TEST_LOGIN
params[:password] = TEST_PASSWORD
else
params[:logon] = @options[:login]
params[:password] = @options[:password]
end
params[:version] = API_VERSION
params[:transaction_type] = action
params.reject { |_k, v| v.blank? }.collect { |k, v| "dc_#{k}=#{CGI.escape(v.to_s)}" }.join('&')
end
def parse(body)
# PayJunction uses the Field Separator ASCII character to separate key/val
# pairs in the response. The <FS> character's octal value is 034.
#
# Sample response:
#
# transaction_id=44752<FS>response_code=M4<FS>response_message=Declined (INV TEST CARD).
pairs = body.chomp.split("\034")
response = {}
pairs.each do |pair|
key, val = pair.split('=')
response[key[3..-1].to_sym] = val ? normalize(val) : nil
end
response
end
end
end
end