Permalink
Browse files

PaymentExpress: Add support for optional fields: ClientType and TxnData

Even though not documented for PxPost (it is documented for
WebService), the ClientType element on transactions can affect
the way PaymentExpress submits transaction to the acquirers.
This commit gives control over that element through the
`:client_type` element in the options hash.

For completeness, access has also been given to the remaining
optional elements: `TxnData1`, `TxnData2`, and `TxnData3`, which
can be set via `:txn_data1`, `:txn_data2`, and `:txn_data2`,
respectively, in the options hash.
  • Loading branch information...
1 parent 4e3a864 commit b6cd25dd282e76d34add44de101171d32ef731e4 @moklett moklett committed Oct 27, 2012
Showing with 196 additions and 1 deletion.
  1. +61 −0 lib/active_merchant/billing/gateways/payment_express.rb
  2. +135 −1 test/unit/gateways/payment_express_test.rb
@@ -140,6 +140,7 @@ def build_purchase_or_authorization_request(money, payment_source, options)
add_amount(result, money, options)
add_invoice(result, options)
add_address_verification_data(result, options)
+ add_optional_elements(result, options)
result
end
@@ -149,6 +150,7 @@ def build_capture_or_credit_request(money, identification, options)
add_amount(result, money, options)
add_invoice(result, options)
add_reference(result, identification)
+ add_optional_elements(result, options)
result
end
@@ -157,6 +159,7 @@ def build_token_request(credit_card, options)
add_credit_card(result, credit_card)
add_amount(result, 100, options) #need to make an auth request for $1
add_token_request(result, options)
+ add_optional_elements(result, options)
result
end
@@ -223,6 +226,52 @@ def add_address_verification_data(xml, options)
xml.add_element("AvsPostCode").text = address[:zip]
end
+ # The options hash may contain optional data which will be passed
+ # through the the specialized optional fields at PaymentExpress
+ # as follows:
+ #
+ # {
+ # :client_type => :web, # Possible values are: :web, :ivr, :moto, :unattended, :internet, or :recurring
+ # :txn_data1 => "String up to 255 characters",
+ # :txn_data2 => "String up to 255 characters",
+ # :txn_data3 => "String up to 255 characters"
+ # }
+ #
+ # +:client_type+, while not documented for PxPost, will be sent as
+ # the +ClientType+ XML element as described in the documentation for
+ # the PaymentExpress WebService: http://www.paymentexpress.com/Technical_Resources/Ecommerce_NonHosted/WebService#clientType
+ # (PaymentExpress have confirmed that this value works the same in PxPost).
+ # The value sent for +:client_type+ will be normalized and sent
+ # as one of the explicit values allowed by PxPost:
+ #
+ # :web => "Web"
+ # :ivr => "IVR"
+ # :moto => "MOTO"
+ # :unattended => "Unattended"
+ # :internet => "Internet"
+ # :recurring => "Recurring"
+ #
+ # If you set the +:client_type+ to any value not listed above,
+ # the ClientType element WILL NOT BE INCLUDED at all in the
+ # POST data.
+ #
+ # +:txn_data1+, +:txn_data2+, and +:txn_data3+ will be sent as
+ # +TxnData1+, +TxnData2+, and +TxnData3+, respectively, and are
+ # free form fields of the merchant's choosing, as documented here:
+ # http://www.paymentexpress.com/technical_resources/ecommerce_nonhosted/pxpost.html#txndata
+ #
+ # These optional elements are added to all transaction types:
+ # +purchase+, +authorize+, +capture+, +refund+, +store+
+ def add_optional_elements(xml, options)
+ if client_type = normalized_client_type(options[:client_type])
+ xml.add_element("ClientType").text = client_type
+ end
+
+ xml.add_element("TxnData1").text = options[:txn_data1].to_s.slice(0,255) unless options[:txn_data1].blank?
+ xml.add_element("TxnData2").text = options[:txn_data2].to_s.slice(0,255) unless options[:txn_data2].blank?
+ xml.add_element("TxnData3").text = options[:txn_data3].to_s.slice(0,255) unless options[:txn_data3].blank?
+ end
+
def new_transaction
REXML::Document.new.add_element("Txn")
end
@@ -266,6 +315,18 @@ def parse(xml_string)
def format_date(month, year)
"#{format(month, :two_digits)}#{format(year, :two_digits)}"
end
+
+ def normalized_client_type(client_type_from_options)
+ case client_type_from_options.to_s.downcase
+ when 'web' then "Web"
+ when 'ivr' then "IVR"
+ when 'moto' then "MOTO"
+ when 'unattended' then "Unattended"
+ when 'internet' then "Internet"
+ when 'recurring' then "Recurring"
+ else nil
+ end
+ end
end
class PaymentExpressResponse < Response
@@ -1,6 +1,8 @@
require 'test_helper'
class PaymentExpressTest < Test::Unit::TestCase
+ include CommStub
+
def setup
@gateway = PaymentExpressGateway.new(
@@ -147,9 +149,141 @@ def test_cvv_result_not_supported
response = @gateway.purchase(@amount, @visa, @options)
assert_nil response.cvv_result['code']
end
-
+
+ def test_expect_no_optional_fields_by_default
+ perform_each_transaction_type_with_request_body_assertions do |body|
+ assert_no_match(/<ClientType>/, body)
+ assert_no_match(/<TxnData1>/, body)
+ assert_no_match(/<TxnData2>/, body)
+ assert_no_match(/<TxnData3>/, body)
+ end
+ end
+
+ def test_pass_optional_txn_data
+ options = {
+ :txn_data1 => "Transaction Data 1",
+ :txn_data2 => "Transaction Data 2",
+ :txn_data3 => "Transaction Data 3"
+ }
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<TxnData1>Transaction Data 1<\/TxnData1>/, body)
+ assert_match(/<TxnData2>Transaction Data 2<\/TxnData2>/, body)
+ assert_match(/<TxnData3>Transaction Data 3<\/TxnData3>/, body)
+ end
+ end
+
+ def test_pass_optional_txn_data_truncated_to_255_chars
+ options = {
+ :txn_data1 => "Transaction Data 1-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA",
+ :txn_data2 => "Transaction Data 2-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA",
+ :txn_data3 => "Transaction Data 3-01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345-EXTRA"
+ }
+
+ truncated_addendum = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345"
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<TxnData1>Transaction Data 1-#{truncated_addendum}<\/TxnData1>/, body)
+ assert_match(/<TxnData2>Transaction Data 2-#{truncated_addendum}<\/TxnData2>/, body)
+ assert_match(/<TxnData3>Transaction Data 3-#{truncated_addendum}<\/TxnData3>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_web
+ options = {:client_type => :web}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>Web<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_ivr
+ options = {:client_type => :ivr}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>IVR<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_moto
+ options = {:client_type => :moto}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>MOTO<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_unattended
+ options = {:client_type => :unattended}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>Unattended<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_internet
+ options = {:client_type => :internet}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>Internet<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_recurring
+ options = {:client_type => :recurring}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_match(/<ClientType>Recurring<\/ClientType>/, body)
+ end
+ end
+
+ def test_pass_client_type_as_symbol_for_unknown_type_omits_element
+ options = {:client_type => :unknown}
+
+ perform_each_transaction_type_with_request_body_assertions(options) do |body|
+ assert_no_match(/<ClientType>/, body)
+ end
+ end
+
private
+ def perform_each_transaction_type_with_request_body_assertions(options = {})
+ # purchase
+ stub_comms do
+ @gateway.purchase(@amount, @visa, options)
+ end.check_request do |endpoint, data, headers|
+ yield data
+ end.respond_with(successful_authorization_response)
+
+ # authorize
+ stub_comms do
+ @gateway.authorize(@amount, @visa, options)
+ end.check_request do |endpoint, data, headers|
+ yield data
+ end.respond_with(successful_authorization_response)
+
+ # capture
+ stub_comms do
+ @gateway.capture(@amount, 'identification', options)
+ end.check_request do |endpoint, data, headers|
+ yield data
+ end.respond_with(successful_authorization_response)
+
+ # refund
+ stub_comms do
+ @gateway.refund(@amount, 'identification', {:description => "description"}.merge(options))
+ end.check_request do |endpoint, data, headers|
+ yield data
+ end.respond_with(successful_authorization_response)
+
+ # store
+ stub_comms do
+ @gateway.store(@visa, options)
+ end.check_request do |endpoint, data, headers|
+ yield data
+ end.respond_with(successful_store_response)
+ end
+
def billing_id_token_purchase(options = {})
"<Txn><BillingId>#{options[:billing_id]}</BillingId><Amount>1.00</Amount><InputCurrency>NZD</InputCurrency><TxnId>aaa050be9488e8e4</TxnId><MerchantReference>Store purchase</MerchantReference><EnableAvsData>1</EnableAvsData><AvsAction>1</AvsAction><AvsStreetAddress>1234 My Street</AvsStreetAddress><AvsPostCode>K1C2N6</AvsPostCode><PostUsername>LOGIN</PostUsername><PostPassword>PASSWORD</PostPassword><TxnType>Purchase</TxnType></Txn>"
end

0 comments on commit b6cd25d

Please sign in to comment.