require 'nokogiri' module ActiveMerchant #:nodoc: module Billing #:nodoc: # iTransact, Inc. is an authorized reseller of the PaymentClearing gateway. If your merchant service provider uses PaymentClearing.com to process payments, you can use this module. # # # Please note, the username and API Access Key are not what you use to log into the Merchant Control Panel. # # ==== How to get your GatewayID and API Access Key # # 1. If you don't already have a Gateway Account, go to http://www.itransact.com/merchant/test.html to sign up. # 2. Go to http://support.paymentclearing.com and login or register, if necessary. # 3. Click on "Submit a Ticket." # 4. Select "Merchant Support" as the department and click "Next" # 5. Enter *both* your company name and GatewayID. Put "API Access Key" in the subject. In the body, you can request a username, but it may already be in use. # # ==== Initialization # # Once you have the username, API Access Key, and your GatewayId, you're ready # to begin. You initialize the Gateway like so: # # gateway = ActiveMerchant::Billing::ItransactGateway.new( # :login => "#{THE_USERNAME}", # :password => "#{THE_API_ACCESS_KEY}", # :gateway_id => "#{THE_GATEWAY_ID}" # ) # # ==== Important Notes # 1. Recurring is not implemented # 1. CreditTransactions are not implemented (these are credits not related to a previously run transaction). # 1. TransactionStatus is not implemented # class ItransactGateway < Gateway self.live_url = self.test_url = 'https://secure.paymentclearing.com/cgi-bin/rc/xmltrans2.cgi' # The countries the gateway supports merchants from as 2 digit ISO country codes self.supported_countries = ['US'] # The card types supported by the payment gateway self.supported_cardtypes = %i[visa master american_express discover] # The homepage URL of the gateway self.homepage_url = 'http://www.itransact.com/' # The name of the gateway self.display_name = 'iTransact' # # Creates a new instance of the iTransact Gateway. # # ==== Parameters # * options - A Hash of options # # ==== Options Hash # * :login - A String containing your PaymentClearing assigned API Access Username # * :password - A String containing your PaymentClearing assigned API Access Key # * :gateway_id - A String containing your PaymentClearing assigned GatewayID # * :test_mode - true or false. Run *all* transactions with the 'TestMode' element set to 'TRUE'. # def initialize(options = {}) requires!(options, :login, :password, :gateway_id) super end # Performs an authorize transaction. In PaymentClearing's documentation # this is known as a "PreAuth" transaction. # # ==== Parameters # * money - The amount to be captured. Should be an Integer amount in cents. # * creditcard - The CreditCard details for the transaction # * options - A Hash of options # # ==== Options Hash # The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as: # * :order_items - An Array of Hash objects with the keys :description, :cost (in cents!), and :quantity. If this is provided, :description and money will be ignored. # * :vendor_data - An Array of Hash objects with the keys being the name of the VendorData element and value being the value. # * :send_customer_email - true or false. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'. # * :send_merchant_email - true or false. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'. # * :email_text - An Array of (up to ten (10)) String objects to be included in emails # * :test_mode - true or false. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # # ==== Examples # response = gateway.authorize(1000, creditcard, # :order_id => '1212', :address => {...}, :email => 'test@test.com', # :order_items => [ # {:description => 'Line Item 1', :cost => '8.98', :quantity => '6'}, # {:description => 'Line Item 2', :cost => '6.99', :quantity => '4'} # ], # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :send_customer_email => true, # :send_merchant_email => true, # :email_text => ['line1', 'line2', 'line3'], # :test_mode => true # ) # def authorize(money, payment_source, options = {}) payload = Nokogiri::XML::Builder.new do |xml| xml.AuthTransaction { xml.Preauth add_customer_data(xml, payment_source, options) add_invoice(xml, money, options) add_payment_source(xml, payment_source) add_transaction_control(xml, options) add_vendor_data(xml, options) } end.doc commit(payload) end # Performs an authorize and capture in single transaction. In PaymentClearing's # documentation this is known as an "Auth" or a "Sale" transaction # # ==== Parameters # * money - The amount to be captured. Should be nil or an Integer amount in cents. # * creditcard - The CreditCard details for the transaction # * options - A Hash of options # # ==== Options Hash # The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as: # * :order_items - An Array of Hash objects with the keys :description, :cost (in cents!), and :quantity. If this is provided, :description and money will be ignored. # * :vendor_data - An Array of Hash objects with the keys being the name of the VendorData element and value being the value. # * :send_customer_email - true or false. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'. # * :send_merchant_email - true or false. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'. # * :email_text - An Array of (up to ten (10)) String objects to be included in emails # * :test_mode - true or false. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # # ==== Examples # response = gateway.purchase(1000, creditcard, # :order_id => '1212', :address => {...}, :email => 'test@test.com', # :order_items => [ # {:description => 'Line Item 1', :cost => '8.98', :quantity => '6'}, # {:description => 'Line Item 2', :cost => '6.99', :quantity => '4'} # ], # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :send_customer_email => true, # :send_merchant_email => true, # :email_text => ['line1', 'line2', 'line3'], # :test_mode => true # ) # def purchase(money, payment_source, options = {}) payload = Nokogiri::XML::Builder.new do |xml| xml.AuthTransaction { add_customer_data(xml, payment_source, options) add_invoice(xml, money, options) add_payment_source(xml, payment_source) add_transaction_control(xml, options) add_vendor_data(xml, options) } end.doc commit(payload) end # Captures the funds from an authorize transaction. In PaymentClearing's # documentation this is known as a "PostAuth" transaction. # # ==== Parameters # * money - The amount to be captured. Should be an Integer amount in cents # * authorization - The authorization returned from the previous capture or purchase request # * options - A Hash of options, all are optional. # # ==== Options Hash # The standard options apply here (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address), as well as: # * :vendor_data - An Array of Hash objects with the keys being the name of the VendorData element and value being the value. # * :send_customer_email - true or false. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'. # * :send_merchant_email - true or false. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'. # * :email_text - An Array of (up to ten (10)) String objects to be included in emails # * :test_mode - true or false. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # # ==== Examples # response = gateway.capture(1000, creditcard, # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :send_customer_email => true, # :send_merchant_email => true, # :email_text => ['line1', 'line2', 'line3'], # :test_mode => true # ) # def capture(money, authorization, options = {}) payload = Nokogiri::XML::Builder.new do |xml| xml.PostAuthTransaction { xml.OperationXID(authorization) add_invoice(xml, money, options) add_transaction_control(xml, options) add_vendor_data(xml, options) } end.doc commit(payload) end # This will reverse a previously run transaction which *has* *not* settled. # # ==== Parameters # * authorization - The authorization returned from the previous capture or purchase request # * options - A Hash of options, all are optional # # ==== Options Hash # The standard options (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address) are ignored. # * :vendor_data - An Array of Hash objects with the keys being the name of the VendorData element and value being the value. # * :send_customer_email - true or false. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'. # * :send_merchant_email - true or false. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'. # * :email_text - An Array of (up to ten (10)) String objects to be included in emails # * :test_mode - true or false. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # # ==== Examples # response = gateway.void('9999999999', # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :send_customer_email => true, # :send_merchant_email => true, # :email_text => ['line1', 'line2', 'line3'], # :test_mode => true # ) # def void(authorization, options = {}) payload = Nokogiri::XML::Builder.new do |xml| xml.VoidTransaction { xml.OperationXID(authorization) add_transaction_control(xml, options) add_vendor_data(xml, options) } end.doc commit(payload) end # This will reverse a previously run transaction which *has* settled. # # ==== Parameters # * money - The amount to be credited. Should be an Integer amount in cents # * authorization - The authorization returned from the previous capture or purchase request # * options - A Hash of options, all are optional # # ==== Options Hash # The standard options (:order_id, :ip, :customer, :invoice, :merchant, :description, :email, :currency, :address, :billing_address, :shipping_address) are ignored. # * :vendor_data - An Array of Hash objects with the keys being the name of the VendorData element and value being the value. # * :send_customer_email - true or false. Runs the transaction with the 'SendCustomerEmail' element set to 'TRUE' or 'FALSE'. # * :send_merchant_email - true or false. Runs the transaction with the 'SendMerchantEmail' element set to 'TRUE' or 'FALSE'. # * :email_text - An Array of (up to ten (10)) String objects to be included in emails # * :test_mode - true or false. Runs the transaction with the 'TestMode' element set to 'TRUE' or 'FALSE'. # # ==== Examples # response = gateway.refund(555, '9999999999', # :vendor_data => [{'repId' => '1234567'}, {'customerId' => '9886'}], # :send_customer_email => true, # :send_merchant_email => true, # :email_text => ['line1', 'line2', 'line3'], # :test_mode => true # ) # def refund(money, authorization, options = {}) payload = Nokogiri::XML::Builder.new do |xml| xml.TranCredTransaction { xml.OperationXID(authorization) add_invoice(xml, money, options) add_transaction_control(xml, options) add_vendor_data(xml, options) } end.doc commit(payload) end private def add_customer_data(xml, payment_source, options) billing_address = options[:billing_address] || options[:address] shipping_address = options[:shipping_address] || options[:address] xml.CustomerData { xml.Email(options[:email]) unless options[:email].blank? xml.CustId(options[:order_id]) unless options[:order_id].blank? xml.BillingAddress { xml.FirstName(payment_source.first_name || parse_first_name(billing_address[:name])) xml.LastName(payment_source.last_name || parse_last_name(billing_address[:name])) xml.Address1(billing_address[:address1]) xml.Address2(billing_address[:address2]) unless billing_address[:address2].blank? xml.City(billing_address[:city]) xml.State(billing_address[:state]) xml.Zip(billing_address[:zip]) xml.Country(billing_address[:country]) xml.Phone(billing_address[:phone]) } xml.ShippingAddress { xml.FirstName(payment_source.first_name || parse_first_name(shipping_address[:name])) xml.LastName(payment_source.last_name || parse_last_name(shipping_address[:name])) xml.Address1(shipping_address[:address1]) xml.Address2(shipping_address[:address2]) unless shipping_address[:address2].blank? xml.City(shipping_address[:city]) xml.State(shipping_address[:state]) xml.Zip(shipping_address[:zip]) xml.Country(shipping_address[:country]) xml.Phone(shipping_address[:phone]) } unless shipping_address.blank? } end def add_invoice(xml, money, options) xml.AuthCode options[:force] if options[:force] if options[:order_items].blank? xml.Total(amount(money)) unless money.nil? || money < 0.01 xml.Description(options[:description]) unless options[:description].blank? else xml.OrderItems { options[:order_items].each do |item| xml.Item { xml.Description(item[:description]) xml.Cost(amount(item[:cost])) xml.Qty(item[:quantity].to_s) } end } end end def add_payment_source(xml, source) case determine_funding_source(source) when :credit_card then add_creditcard(xml, source) when :check then add_check(xml, source) end end def determine_funding_source(payment_source) case payment_source when ActiveMerchant::Billing::CreditCard :credit_card when ActiveMerchant::Billing::Check :check end end def add_creditcard(xml, creditcard) xml.AccountInfo { xml.CardAccount { xml.AccountNumber(creditcard.number.to_s) xml.ExpirationMonth(creditcard.month.to_s.rjust(2, '0')) xml.ExpirationYear(creditcard.year.to_s) xml.CVVNumber(creditcard.verification_value.to_s) unless creditcard.verification_value.blank? } } end def add_check(xml, check) xml.AccountInfo { xml.ABA(check.routing_number.to_s) xml.AccountNumber(check.account_number.to_s) xml.AccountSource(check.account_type.to_s) xml.AccountType(check.account_holder_type.to_s) xml.CheckNumber(check.number.to_s) } end def add_transaction_control(xml, options) xml.TransactionControl { # if there was a 'global' option set... xml.TestMode(@options[:test_mode].upcase) if !@options[:test_mode].blank? # allow the global option to be overridden... xml.TestMode(options[:test_mode].upcase) if !options[:test_mode].blank? xml.SendCustomerEmail(options[:send_customer_email].upcase) unless options[:send_customer_email].blank? xml.SendMerchantEmail(options[:send_merchant_email].upcase) unless options[:send_merchant_email].blank? xml.EmailText { options[:email_text].each do |item| xml.EmailTextItem(item) end } if options[:email_text] } end def add_vendor_data(xml, options) return if options[:vendor_data].blank? xml.VendorData { options[:vendor_data].each do |k, v| xml.Element { xml.Name(k) xml.Key(v) } end } end def commit(payload) # Set the Content-Type header -- otherwise the URL decoding messes up # the Base64 encoded payload signature! response = parse(ssl_post(self.live_url, post_data(payload), 'Content-Type' => 'text/xml')) Response.new(successful?(response), response[:error_message], response, test: test?, authorization: response[:xid], avs_result: { code: response[:avs_response] }, cvv_result: response[:cvv_response]) end def post_data(payload) payload_xml = payload.root.to_xml(indent: 0) payload_signature = sign_payload(payload_xml) request = Nokogiri::XML::Builder.new do |xml| xml.GatewayInterface { xml.APICredentials { xml.Username(@options[:login]) xml.PayloadSignature(payload_signature) xml.TargetGateway(@options[:gateway_id]) } } end.doc request.root.children.first.after payload.root request.to_xml(indent: 0) end def parse(raw_xml) doc = REXML::Document.new(raw_xml) response = Hash.new transaction_result = doc.root.get_elements('TransactionResponse/TransactionResult/*') transaction_result.each do |e| response[e.name.to_s.underscore.to_sym] = e.text unless e.text.blank? end response end def successful?(response) # Turns out the PaymentClearing gateway is not consistent... response[:status].casecmp('ok').zero? end def test_mode?(response) # The '1' is a legacy thing; most of the time it should be 'TRUE'... response[:test_mode] == 'TRUE' || response[:test_mode] == '1' end def message_from(response) response[:error_message] end def sign_payload(payload) key = @options[:password].to_s digest = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA1.new(key), key, payload) signature = Base64.encode64(digest) signature.chomp! end end end end