From bfb946c76fcd2d8801728fd31af77f8059a25a17 Mon Sep 17 00:00:00 2001
From: Christopher James Huff <cjameshuff@gmail.com>
Date: Mon, 28 Sep 2009 11:47:47 -0400
Subject: [PATCH] Added basic support for PayLeap gateway
---
lib/active_merchant/billing/gateways/payleap.rb | 228 +++++++++++++++++++++++
test/remote/gateways/remote_payleap_test.rb | 81 ++++++++
test/unit/gateways/payleap_test.rb | 85 +++++++++
3 files changed, 394 insertions(+), 0 deletions(-)
create mode 100644 lib/active_merchant/billing/gateways/payleap.rb
create mode 100644 test/remote/gateways/remote_payleap_test.rb
create mode 100644 test/unit/gateways/payleap_test.rb
diff --git a/lib/active_merchant/billing/gateways/payleap.rb b/lib/active_merchant/billing/gateways/payleap.rb
new file mode 100644
index 0000000..51fb082
--- /dev/null
+++ b/lib/active_merchant/billing/gateways/payleap.rb
@@ -0,0 +1,228 @@
+
+require "rexml/document"
+
+# http://payleap.com/forum/
+# http://www.payleap.com/merchants-faq.html
+# AVSResult: https://www.wellsfargo.com/downloads/pdf/biz/merchant/visa_avs.pdf
+# CVVResult: http://www.bbbonline.org/eExport/doc/MerchantGuide_cvv2.pdf
+
+module ActiveMerchant #:nodoc:
+ module Billing #:nodoc:
+ class PayLeapGateway < Gateway
+ TEST_URL = 'http://test.payleap.com/SmartPayments/transact.asmx/ProcessCreditCard'
+ LIVE_URL = 'https://secure.payleap.com/SmartPayments/transact.asmx/ProcessCreditCard'
+
+ # The countries the gateway supports merchants from as 2 digit ISO country codes
+ self.supported_countries = ['US']
+
+ self.money_format = :dollars # float dollars with two decimal places
+
+ # The card types supported by the payment gateway
+ self.supported_cardtypes = [:visa, :master, :american_express, :discover, :jcb, :diners_club]
+ # also: Wright Express, Carte Blanche
+
+ # The homepage URL of the gateway
+ self.homepage_url = 'http://www.payleap.com/'
+
+ # The name of the gateway
+ self.display_name = 'PayLeap'
+
+ HOST_ERROR = -100
+ APPROVED = 0
+
+ CARD_CODE_ERRORS = %w[N S]
+ AVS_ERRORS = %w[A E N R W Z]
+
+
+ def initialize(options = {})
+ requires!(options, :login, :password)
+ @options = options
+ super
+ end
+
+
+ def authorize(money, creditcard, options = {})
+ post = {}
+ add_invoice(post, options) # InvNum
+ add_creditcard(post, creditcard) # CardNum, CVNum, ExpDate, NameOnCard
+ add_address(post, creditcard, options) # Street, Zip
+ add_customer_data(post, options)
+
+ commit('Auth', money, post) # Amount, TransType, UserName, Password
+
+ # TODO: ExtData?
+ end
+
+ def purchase(money, creditcard, options = {})# TODO Sale
+ post = {}
+ add_invoice(post, options)
+ add_creditcard(post, creditcard)
+ add_address(post, creditcard, options)
+ add_customer_data(post, options)
+
+ commit('Sale', money, post)
+ end
+
+ def capture(money, auth, options = {})
+ post = {:AuthCode => auth[:AuthCode], :PNRef => auth[:PNRef], :CardNum => auth[:CardNum]}
+ # Contrary to the documentation, PNRef and the last 4 digits of the card
+ # number must be included, so auth is a hash containing these.
+ # Also, PayLeap is evidently set up in such a way that Force is used for what
+ # Capture is generally intended for, performing a ForceCapture in this context.
+ commit('Force', money, post)
+ end
+
+ def void(auth, options = {})
+ post = {:AuthCode => auth[:AuthCode], :PNRef => auth[:PNRef]}
+ commit('Void', nil, post)
+ end
+
+ def credit(money, auth, options = {})
+ #requires!(options, :card_number)
+
+# post = {:AuthCode => auth[:AuthCode], :PNRef => auth[:PNRef], :CardNum => auth[:CardNum]}
+ post = {
+ :AuthCode => auth[:AuthCode], :PNRef => auth[:PNRef],
+ :CardNum => options[:card].number, :ExpDate => expdate(options[:card])
+ }
+ #add_invoice(post, options)
+
+ commit('Return', money, post)
+ end
+
+
+ private
+ # return credit card expiration date in MMYY format
+ def expdate(creditcard)
+ month = sprintf("%.2i", creditcard.month)
+ year = sprintf("%.4i", creditcard.year)
+ "#{month}#{year[2, 2]}"
+ end
+
+ def add_customer_data(post, options)
+ if(post[:ExtData] == nil) then post[:ExtData] = "" end
+ if(options[:customer] != nil) then post[:ExtData] += "<CustomerID>#{options[:customer]}</CustomerID>" end
+ end
+
+ def add_address(post, creditcard, options)
+ address = options[:billing_address] || options[:address]
+ if(address)
+ post[:Street] = "#{address[:address1]} #{address[:address2]} #{address[:city]}, #{address[:state]}"
+ post[:Zip] = address[:zip].to_s
+ end # else error?
+ # TODO: Add address to ExtData as well?
+ end
+
+ def add_invoice(post, options)
+ post[:InvNum] = options[:invoice]
+ # TODO: :invoice or :order_id?
+ # TODO: more invoice detail in ExtData?
+ end
+
+ def add_creditcard(post, creditcard)
+ post[:CardNum] = creditcard.number
+ if(creditcard.verification_value?)
+ post[:CVNum] = creditcard.verification_value
+ end
+ post[:ExpDate] = expdate(creditcard)
+ post[:NameOnCard] = creditcard.first_name + " " + creditcard.last_name
+ end
+
+ # Parse response data into response hash
+ def parse(body)
+ response = {}
+ xml = REXML::Document.new(body)
+ if(!xml.root.nil?)
+ puts "##### Got response:"
+ puts "##### XML:"
+ puts body
+ puts "##### end XML"
+ xml.root.elements.each do |node|
+ response[node.name.to_sym] = node.text
+ puts "\t#{node.name}: #{node.text}"
+ end
+ puts "##### end response"
+ else
+ puts "##### Error: Empty response. Body:"
+ puts body
+ puts "##### end body"
+ end
+ return response
+ # Response keys:
+ # "Result", "RespMSG", "Message", "Message1", "Message2"
+ # "PNRef", "HostCode", "HostURL", "ReceiptURL"
+ # "AuthCode", "GetAVSResult", "GetAVSResultTXT"
+ # "GetStreetMatchTXT", "GetZipMatchTXT"
+ # "GetCVResult", "GetCVResultTXT"
+ # "GetGetOrigResult", "GetCommercialCard", "WorkingKey", "KeyPointer"
+ # "InvNum", "CardType", "ExtData"
+ end
+
+ # Extract informational message from response.
+ def message_from(response)
+ if(response[:Result].to_i != APPROVED)
+ if(CARD_CODE_ERRORS.include?(response[:GetCVResult]))
+ return CVVResult.messages[response[:GetCVResult]]
+ elsif(AVS_ERRORS.include?(response[:GetAVSResult]))
+ return AVSResult.messages[response[:GetAVSResult]]
+ end
+ end
+
+ respMsg = ""
+ if(!response[:Message].nil?) then respMsg += response[:Message]; end
+ if(!response[:Message1].nil?) then respMsg += response[:Message1]; end
+ if(!response[:Message2].nil?) then respMsg += response[:Message2]; end
+ respMsg
+ end
+
+ def commit(action, money, parameters)
+ parameters[:TransType] = action
+ if(action != 'Void')
+ parameters[:Amount] = sprintf("%07.2f", money.to_f/100)
+ end
+
+ url = (test?)? TEST_URL : LIVE_URL
+ puts "Using URL: #{url}"
+ puts "Start of POST data:"
+ puts post_data(action, parameters)
+ puts "End of POST data:"
+ data = ssl_post(url, post_data(action, parameters))
+ response = parse(data)
+ message = message_from(response)
+
+ Response.new((response[:Result].to_i == APPROVED), message, response, {
+ :authorization => {
+ :AuthCode => response[:AuthCode],
+ :CardNum => (parameters[:CardNum] != nil)? parameters[:CardNum][-4, 4] : nil,
+ :PNRef => response[:PNRef]
+ },
+ :avs_result => {:code => response[:GetAVSResult]},
+ :cvv_result => response[:GetCVResult],
+ :test => @options[:test]
+ })
+ end
+
+ def post_data(action, parameters = {})
+ post = {} # evidently these fields are not optional, only the values are
+ post[:UserName] = @options[:login]
+ post[:Password] = @options[:password]
+ post[:TransType] = ""
+ post[:CardNum] = ""
+ post[:ExpDate] = ""
+ post[:MagData] = ""
+ post[:NameOnCard] = ""
+ post[:Amount] = ""
+ post[:InvNum] = ""
+ post[:PNRef] = ""
+ post[:Zip] = ""
+ post[:Street] = ""
+ post[:CVNum] = ""
+ post[:ExtData] = ""
+ # use PostData class?
+ request = post.merge(parameters).map {|key, value| "#{key}=#{CGI.escape(value.to_s)}"}.join("&")
+ request
+ end
+ end # class PayLeapGateway
+ end # module Billing
+end # module ActiveMerchant
+
diff --git a/test/remote/gateways/remote_payleap_test.rb b/test/remote/gateways/remote_payleap_test.rb
new file mode 100644
index 0000000..0b59601
--- /dev/null
+++ b/test/remote/gateways/remote_payleap_test.rb
@@ -0,0 +1,81 @@
+require 'test_helper'
+
+# Test cards:
+# MasterCard: 5000300020003003
+# Visa: 4005550000000019
+# Discover: 60011111111111117
+# Diners: 36999999999999
+# AMEX: 374255312721002
+
+class RemotePayLeapTest < Test::Unit::TestCase
+
+ def setup
+ ActiveMerchant::Billing::Base.mode = :test
+ @gateway = PayLeapGateway.new(fixtures(:payleap))
+
+ @amount = 104
+ @credit_card = ActiveMerchant::Billing::CreditCard.new(
+ :type => "american_express",
+ :number => "374255312721002",
+ :verification_value => "123",
+ :month => "10",
+ :year => "2009",
+ :first_name => "John",
+ :last_name => "Doe"
+ )
+
+ @declined_card = ActiveMerchant::Billing::CreditCard.new(
+ :type => "american_express",
+ :number => "374255312721003",
+ :verification_value => "123",
+ :month => "10",
+ :year => "2009",
+ :first_name => "John",
+ :last_name => "Doe"
+ )
+
+ @options = {
+ :order_id => '1',
+ :billing_address => address,
+ :description => 'Store Purchase'
+ }
+ end
+
+ def test_successful_purchase
+ assert response = @gateway.purchase(@amount, @credit_card, @options)
+ assert_success response
+ assert_equal 'Approved', response.message
+ end
+
+ def test_unsuccessful_purchase
+ assert response = @gateway.purchase(@amount, @declined_card, @options)
+ assert_failure response
+# assert_equal 'REPLACE WITH FAILED PURCHASE MESSAGE', response.message
+ end
+
+ def test_authorize_and_capture
+ amount = @amount
+ assert auth = @gateway.authorize(amount, @credit_card, @options)
+ assert_success auth
+ assert_equal 'Approved', auth.message
+ assert auth.authorization
+ assert capture = @gateway.capture(amount, auth.authorization)
+ assert_success capture
+ end
+
+ def test_failed_capture
+ assert response = @gateway.capture(@amount, '')
+ assert_failure response
+# assert_equal 'REPLACE WITH GATEWAY FAILURE MESSAGE', response.message
+ end
+
+ def test_invalid_login
+ gateway = PayLeapGateway.new(
+ :login => '',
+ :password => ''
+ )
+ assert response = gateway.purchase(@amount, @credit_card, @options)
+ assert_failure response
+# assert_equal 'REPLACE WITH FAILURE MESSAGE', response.message
+ end
+end
diff --git a/test/unit/gateways/payleap_test.rb b/test/unit/gateways/payleap_test.rb
new file mode 100644
index 0000000..7ebcf11
--- /dev/null
+++ b/test/unit/gateways/payleap_test.rb
@@ -0,0 +1,85 @@
+require 'test_helper'
+
+class PayLeapTest < Test::Unit::TestCase
+ def setup
+ @gateway = PayLeapGateway.new(
+ :login => 'login',
+ :password => 'password',
+ :test => true
+ )
+
+ @credit_card = ActiveMerchant::Billing::CreditCard.new(
+ :type => "american_express",
+ :number => "374255312721002",
+ :verification_value => "123",
+ :month => "10",
+ :year => "2009",
+ :first_name => "John",
+ :last_name => "Doe"
+ )
+
+ @amount = 100
+
+ @options = {
+ :order_id => '1',
+ :billing_address => address,
+ :description => 'Store Purchase'
+ }
+ end
+
+ def test_successful_purchase
+ @gateway.expects(:ssl_post).returns(successful_purchase_response)
+
+ assert response = @gateway.purchase(@amount, @credit_card, @options)
+ assert_instance_of Response, response
+ assert_success response
+
+ # Replace with authorization number from the successful response
+ assert_equal({:PNRef => "331", :CardNum => "1002", :AuthCode => "562"}, response.authorization)
+ assert response.test?
+ end
+
+ def test_unsuccessful_request
+ @gateway.expects(:ssl_post).returns(failed_purchase_response)
+
+ assert response = @gateway.purchase(@amount, @credit_card, @options)
+ assert_failure response
+ assert response.test?
+ end
+
+ private
+ # Place raw successful response from gateway here
+ def successful_purchase_response
+ %q<<?xml version="1.0" encoding="utf-8"?>
+<Response xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://TPISoft.com/SmartPayments/">
+ <Result>0</Result>
+ <RespMSG>Approved</RespMSG>
+ <Message>Approved</Message>
+ <AuthCode>562</AuthCode>
+ <PNRef>331</PNRef>
+ <HostCode />
+ <GetAVSResult>0</GetAVSResult>
+ <GetAVSResultTXT>Issuer did not perform AVS</GetAVSResultTXT>
+ <GetStreetMatchTXT>Service Not Requested</GetStreetMatchTXT>
+ <GetZipMatchTXT>Service Not Requested</GetZipMatchTXT>
+ <GetCVResult>U</GetCVResult>
+ <GetCVResultTXT>Service Not Requested</GetCVResultTXT>
+ <GetCommercialCard>False</GetCommercialCard>
+ <ExtData>CardType=AMEX</ExtData>
+</Response>>
+ end
+
+ # Place raw failed response from gateway here
+ def failed_purchase_response
+ %q<<?xml version="1.0" encoding="utf-8"?>
+<Response xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://TPISoft.com/SmartPayments/">
+ <Result>113</Result>
+ <RespMSG>Cannot Exceed Sales Cap</RespMSG>
+ <Message>Requested Refund Exceeds Available Refund Amount</Message>
+ <AuthCode>Cannot_Exceed_Sales_Cap</AuthCode>
+ <PNRef>329</PNRef>
+ <GetCommercialCard>False</GetCommercialCard>
+ <ExtData>CardType=AMEX</ExtData>
+</Response>>
+ end
+end
--
1.6.4.2