Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add First Data e4 gateway support #473

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions lib/active_merchant/billing/gateways/firstdata_e4.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,223 @@
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
class FirstdataE4Gateway < Gateway
self.test_url = "https://api.demo.globalgatewaye4.firstdata.com/transaction"
self.live_url = "https://api.globalgatewaye4.firstdata.com/transaction"

TRANSACTIONS = { :sale => "00",
:authorization => "01",
:capture => "32",
:void => "33",
:credit => "34" }


POST_HEADERS = { 'Accepts' => 'application/xml',
'Content-Type' => 'application/xml'
}

SUCCESS = "true"

SENSITIVE_FIELDS = [ :verification_str2, :expiry_date, :card_number ]

self.supported_cardtypes = [:visa, :master, :american_express, :jcb, :discover]
self.supported_countries = ['CA', 'US']
self.default_currency = 'USD'
self.homepage_url = 'http://www.firstdata.com'
self.display_name = 'FirstData E4'

def initialize(options = {})
requires!(options, :login, :password)
@options = options

super
end

def test?
@test_mode || Base.gateway_mode == :test
end

def authorize(money, credit_card, options = {})
commit(:authorization, build_sale_or_authorization_request(money, credit_card, options))
end

def purchase(money, credit_card, options = {})
commit(:sale, build_sale_or_authorization_request(money, credit_card, options))
end

def capture(money, authorization, options = {})
commit(:capture, build_capture_or_credit_request(money, authorization, options))
end

# Void is not supported, because we would have to put the amount in the authorization string.
#def void(authorization, options = {})
# commit(:void, build_capture_or_credit_request(money_from_authorization(authorization), authorization, options))
#end

def credit(money, authorization, options = {})
deprecated CREDIT_DEPRECATION_MESSAGE
refund(money, authorization, options)
end

def refund(money, authorization, options = {})
commit(:credit, build_capture_or_credit_request(money, authorization, options))
end

private
def build_request(action, body)
xml = Builder::XmlMarkup.new

xml.instruct!
xml.tag! 'Transaction' do
add_credentials(xml)
add_transaction_type(xml, action)
xml << body
end

xml.target!
end

def build_sale_or_authorization_request(money, credit_card, options)
xml = Builder::XmlMarkup.new

add_amount(xml, money)
add_credit_card(xml, credit_card)
add_customer_data(xml, options)
add_invoice(xml, options)

xml.target!
end

def build_capture_or_credit_request(money, identification, options)
xml = Builder::XmlMarkup.new

add_identification(xml, identification)
add_amount(xml, money)
add_customer_data(xml, options)

xml.target!
end

def add_credentials(xml)
xml.tag! 'ExactID', @options[:login]
xml.tag! 'Password', @options[:password]
end

def add_transaction_type(xml, action)
xml.tag! 'Transaction_Type', TRANSACTIONS[action]
end

def add_identification(xml, identification)
authorization_num, transaction_tag, amount = identification.split(';')

xml.tag! 'Authorization_Num', authorization_num
xml.tag! 'Transaction_Tag', transaction_tag
end

def add_amount(xml, money)
xml.tag! 'DollarAmount', amount(money)
end

def add_credit_card(xml, credit_card)
xml.tag! 'Card_Number', credit_card.number
xml.tag! 'Expiry_Date', expdate(credit_card)
xml.tag! 'CardHoldersName', credit_card.name
xml.tag! 'CardType', credit_card.brand

if credit_card.verification_value?
xml.tag! 'CVD_Presence_Ind', '1'
xml.tag! 'VerificationStr2', credit_card.verification_value
end
end

def add_customer_data(xml, options)
xml.tag! 'Customer_Ref', options[:customer] if options[:customer]
xml.tag! 'Client_IP', options[:ip] if options[:ip]
xml.tag! 'Client_Email', options[:email] if options[:email]
end

def add_address(xml, options)
if address = options[:billing_address] || options[:address]
xml.tag! 'ZipCode', address[:zip]
end
end

def add_invoice(xml, options)
xml.tag! 'Reference_No', options[:order_id]
xml.tag! 'Reference_3', options[:description] if options[:description]
end

def expdate(credit_card)
"#{format(credit_card.month, :two_digits)}#{format(credit_card.year, :two_digits)}"
end

def commit(action, request)
url = test? ? self.test_url : self.live_url
begin
req = build_request(action, request)
#response = parse(ssl_post(url, build_request(action, request), POST_HEADERS))
raw_response = ssl_post(url, req, POST_HEADERS)
response = parse(raw_response)
rescue ResponseError => e
response = parse_error(e.response)
end

Response.new(successful?(response), message_from(response), response,
:test => test?,
:authorization => authorization_from(response),
:avs_result => { :code => response[:avs] },
:cvv_result => response[:cvv2]
)
end

def successful?(response)
response[:transaction_approved] == SUCCESS
end

def authorization_from(response)
if response[:authorization_num] && response[:transaction_tag]
"#{response[:authorization_num]};#{response[:transaction_tag]}"
else
''
end
end

def message_from(response)
if response[:faultcode] && response[:faultstring]
response[:faultstring]
elsif response[:error_number] != '0'
response[:error_description]
else
result = response[:exact_message] || ''
result << " - #{response[:bank_message]}" unless response[:bank_message].blank?
result
end
end

def parse_error(error)
response = {}
response[:transaction_approved] = 'false'
response[:error_number] = error.code
response[:error_description] = error.body
response
end

def parse(xml)
response = {}
xml = REXML::Document.new(xml)

if root = REXML::XPath.first(xml, "//TransactionResult")
parse_elements(response, root)
end

response.delete_if{ |k,v| SENSITIVE_FIELDS.include?(k) }
end

def parse_elements(response, root)
root.elements.to_a.each do |node|
response[node.name.gsub(/EXact/, 'Exact').underscore.to_sym] = (node.text || '').strip
end
end
end
end
end

13 changes: 6 additions & 7 deletions lib/active_merchant/billing/gateways/itransact.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ def capture(money, authorization, options = {})
# This will reverse a previously run transaction which *has* *not* settled. # This will reverse a previously run transaction which *has* *not* settled.
# #
# ==== Parameters # ==== Parameters
# * <tt>money</tt> - This parameter is ignored -- the PaymentClearing gateway does not allow partial voids.
# * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request # * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request
# * <tt>options</tt> - A Hash of options, all are optional # * <tt>options</tt> - A Hash of options, all are optional
# #
Expand All @@ -209,15 +208,15 @@ def capture(money, authorization, options = {})
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
# #
# ==== Examples # ==== Examples
# response = gateway.void(nil, '9999999999', # response = gateway.void('9999999999',
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true, # :send_customer_email => true,
# :send_merchant_email => true, # :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'], # :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true # :test_mode => true
# ) # )
# #
def void(money, authorization, options = {}) def void(authorization, options = {})
payload = Nokogiri::XML::Builder.new do |xml| payload = Nokogiri::XML::Builder.new do |xml|
xml.VoidTransaction { xml.VoidTransaction {
xml.OperationXID(authorization) xml.OperationXID(authorization)
Expand All @@ -232,7 +231,7 @@ def void(money, authorization, options = {})
# This will reverse a previously run transaction which *has* settled. # This will reverse a previously run transaction which *has* settled.
# #
# ==== Parameters # ==== Parameters
# * <tt>money</tt> - The amount to be credited. Should be <tt>nil</tt> or an Integer amount in cents # * <tt>money</tt> - The amount to be credited. Should be an Integer amount in cents
# * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request # * <tt>authorization</tt> - The authorization returned from the previous capture or purchase request
# * <tt>options</tt> - A Hash of options, all are optional # * <tt>options</tt> - A Hash of options, all are optional
# #
Expand All @@ -245,17 +244,17 @@ def void(money, authorization, options = {})
# * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # * <tt>:test_mode</tt> - <tt>true</tt> or <tt>false</tt>. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'.
# #
# ==== Examples # ==== Examples
# response = gateway.credit(nil, '9999999999', # response = gateway.refund(555, '9999999999',
# :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}],
# :send_customer_email => true, # :send_customer_email => true,
# :send_merchant_email => true, # :send_merchant_email => true,
# :email_text => ['line1', 'line2', 'line3'], # :email_text => ['line1', 'line2', 'line3'],
# :test_mode => true # :test_mode => true
# ) # )
# #
def credit(money, authorization, options = {}) def refund(money, authorization, options = {})
payload = Nokogiri::XML::Builder.new do |xml| payload = Nokogiri::XML::Builder.new do |xml|
xml.TranCreditTransaction { xml.TranCredTransaction {
xml.OperationXID(authorization) xml.OperationXID(authorization)
add_invoice(xml, money, options) add_invoice(xml, money, options)
add_transaction_control(xml, options) add_transaction_control(xml, options)
Expand Down
76 changes: 76 additions & 0 deletions test/remote/gateways/remote_firstdata_e4_test.rb
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,76 @@
require 'test_helper'

class RemoteFirstdataE4Test < Test::Unit::TestCase

def setup

@gateway = FirstdataE4Gateway.new(fixtures(:firstdata_e4))
@credit_card = credit_card
@bad_credit_card = credit_card('4111111111111113')
@amount = 100
@options = {
:order_id => '1',
:billing_address => address,
:description => 'Store Purchase'
}
end

def test_successful_purchase
assert response = @gateway.purchase(@amount, @credit_card, @options)
assert_match /Transaction Normal/, response.message
assert_success response
end

def test_unsuccessful_purchase
# ask for error 13 response (Amount Error) via dollar amount 5,000 + error
@amount = 501300
assert response = @gateway.purchase(@amount, @credit_card, @options )
assert_match /Transaction Normal/, response.message
assert_failure response
end

def test_bad_creditcard_number
assert response = @gateway.purchase(@amount, @bad_credit_card, @options)
assert_match /Invalid Credit Card/, response.message
assert_failure response
end


def test_trans_error
# ask for error 42 (unable to send trans) as the cents bit...
@amount = 500042
assert response = @gateway.purchase(@amount, @credit_card, @options )
assert_match /Unable to Send Transaction/, response.message # 42 is 'unable to send trans'
assert_failure response
end

def test_purchase_and_credit
assert purchase = @gateway.purchase(@amount, @credit_card, @options)
assert_success purchase
assert purchase.authorization
assert credit = @gateway.refund(@amount, purchase.authorization)
assert_success credit
end

def test_authorize_and_capture
assert auth = @gateway.authorize(@amount, @credit_card, @options)
assert_success auth
assert auth.authorization
assert capture = @gateway.capture(@amount, auth.authorization)
assert_success capture
end

def test_failed_capture
assert response = @gateway.capture(@amount, 'ET838747474;frob')
assert_failure response
assert_match /Invalid Authorization Number/i, response.message
end

def test_invalid_login
gateway = FirstdataE4Gateway.new( :login => "NotARealUser",
:password => "NotARealPassword" )
assert response = gateway.purchase(@amount, @credit_card, @options)
assert_equal "Unauthorized Request (bad or missing credentials).", response.message
assert_failure response
end
end
20 changes: 13 additions & 7 deletions test/remote/gateways/remote_itransact_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -56,18 +56,24 @@ def test_authorize_and_void
assert_success response assert_success response
assert_nil response.message assert_nil response.message
assert response.authorization assert response.authorization
assert capture = @gateway.void(nil, response.authorization) assert capture = @gateway.void(response.authorization)
assert_success capture assert_success capture
end end


def test_credit def test_void
assert credit = @gateway.void(nil, '9999999999') assert void = @gateway.void('9999999999')
assert_success credit assert_success void
end end


def test_credit_partial # As of Sep 19, 2012, iTransact REQUIRES the total amount for the refund.
assert credit = @gateway.void(5.55, '9999999999') # def test_refund
assert_success credit # assert refund = @gateway.refund(nil, '9999999999')
# assert_success refund
# end

def test_refund_partial
assert refund = @gateway.refund(555, '9999999999') # $5.55 in cents
assert_success refund
end end


def test_invalid_login def test_invalid_login
Expand Down
Loading