require 'securerandom' require 'digest' module ActiveMerchant #:nodoc: module Billing #:nodoc: # ==== USA ePay Advanced SOAP Interface # # This class encapuslates USA ePay's Advanced SOAP Interface. The Advanced Soap Interface allows # standard transactions, storing customer information, and recurring billing. Storing sensitive # information on USA ePay's servers can help with PCI DSS compliance, since customer and card data # do not need to be stored locally. # # Make sure you have enabled this functionality for your account with USA ePay. # # Information about the Advanced SOAP interface is available on the {USA ePay wiki}[http://wiki.usaepay.com/developer/soap]. # # ==== Login, Password, and Software ID # # Please follow all of USA ePay's directions for acquiring all accounts and settings. # # The value used for <tt>:login</tt> is the Key value found in the Merchant Console under Settings > Source # Key. You will have to add this key in the USA ePay Merchant Console. # # The value used for <tt>:password</tt> is the pin value also found and assigned in the Merchant Console under # Settings > Source Key. The pin is required to use all but basic transactions in the SOAP interface. # You will have to add the pin to your source key, as it defaults to none. # # The value used for the <tt>:software_id</tt> is found in the Developer's Login under the Developers Center # in your WSDL. It is the 8 character value in <soap:address> tag. A masked example: # <soap:address location="https://www.usaepay.com/soap/gate/XXXXXXXX"/> # It is also found in the link to your WSDL. This is required as every account has a different path # SOAP requests are submitted to. Optionally, you can provide the entire urls via <tt>:live_url</tt> and <tt>:test_url</tt>, if your prefer. # # ==== Responses # * <tt>#success?</tt> -- +true+ if transmitted and returned correctly # * <tt>#message</tt> -- response or fault message # * <tt>#authorization</tt> -- reference_number or nil # * <tt>#params</tt> -- hash of entire soap response contents # # ==== Address Options # * <tt>:billing_address/:shipping_address</tt> -- contains some extra options # * <tt>:name</tt> -- virtual attribute; will split to first and last name # * <tt>:first_name</tt> # * <tt>:last_name</tt> # * <tt>:address1 </tt> # * <tt>:address2 </tt> # * <tt>:city </tt> # * <tt>:state </tt> # * <tt>:zip </tt> # * <tt>:country </tt> # * <tt>:phone</tt> # * <tt>:email</tt> # * <tt>:fax</tt> # * <tt>:company</tt> # # ==== Support: # * Questions: post to {active_merchant google group}[http://groups.google.com/group/activemerchant] # * Feedback/fixes: matt (at) nearapogee (dot) com # # ==== Links: # * {USA ePay Merchant Console}[https://sandbox.usaepay.com/login] # * {USA ePay Developer Login}[https://www.usaepay.com/developer/login] # class UsaEpayAdvancedGateway < Gateway API_VERSION = "1.4" class_attribute :test_url, :live_url TEST_URL_BASE = 'https://sandbox.usaepay.com/soap/gate/' #:nodoc: LIVE_URL_BASE = 'https://www.usaepay.com/soap/gate/' #:nodoc: FAILURE_MESSAGE = "Default Failure" #:nodoc: self.supported_countries = ['US'] self.supported_cardtypes = [:visa, :master, :american_express, :discover, :diners_club, :jcb] self.homepage_url = 'http://www.usaepay.com/' self.display_name = 'USA ePay Advanced SOAP Interface' CUSTOMER_OPTIONS = { :id => [:string, 'CustomerID'], # merchant assigned number :notes => [:string, 'Notes'], :data => [:string, 'CustomData'], :url => [:string, 'URL'], # Recurring Billing :enabled => [:boolean, 'Enabled'], :schedule => [:string, 'Schedule'], :number_left => [:integer, 'NumLeft'], :currency => [:string, 'Currency'], :description => [:string, 'Description'], :order_id => [:string, 'OrderID'], :user => [:string, 'User'], :source => [:string, 'Source'], :send_receipt => [:boolean, 'SendReceipt'], :receipt_note => [:string, 'ReceiptNote'], # Point of Sale :price_tier => [:string, 'PriceTier'], :tax_class => [:string, 'TaxClass'], :lookup_code => [:string, 'LookupCode'] } #:nodoc: ADDRESS_OPTIONS = { :first_name => [:string, 'FirstName'], :last_name => [:string, 'LastName'], :address1 => [:string, 'Street'], :address2 => [:string, 'Street2'], :city => [:string, 'City'], :state => [:string, 'State'], :zip => [:string, 'Zip'], :country => [:string, 'Country'], :phone => [:string, 'Phone'], :email => [:string, 'Email'], :fax => [:string, 'Fax'], :company => [:string, 'Company'] } #:nodoc: CUSTOMER_TRANSACTION_REQUEST_OPTIONS = { :command => [:string, 'Command'], :ignore_duplicate => [:boolean, 'IgnoreDuplicate'], :client_ip => [:string, 'ClientIP'], :customer_receipt => [:boolean, 'CustReceipt'], :customer_email => [:boolean, 'CustReceiptEmail'], :customer_template => [:boolean, 'CustReceiptName'], :merchant_receipt => [:boolean, 'MerchReceipt'], :merchant_email => [:boolean, 'MerchReceiptEmail'], :merchant_template => [:boolean, 'MerchReceiptName'], :verification_value => [:boolean, 'isRecurring'], :software => [:string, 'Software'] } #:nodoc: TRANSACTION_REQUEST_OBJECT_OPTIONS = { :command => [:string, 'Command'], :ignore_duplicate => [:boolean, 'IgnoreDuplicate'], :authorization_code => [:string, 'AuthCode'], :reference_number => [:string, 'RefNum'], :account_holder => [:string, 'AccountHolder'], :client_ip => [:string, 'ClientIP'], :customer_id => [:string, 'CustomerID'], :customer_receipt => [:boolean, 'CustReceipt'], :customer_template => [:boolean, 'CustReceiptName'], :software => [:string, 'Software'] } #:nodoc: TRANSACTION_DETAIL_OPTIONS = { :invoice => [:string, 'Invoice'], :po_number => [:string, 'PONum'], :order_id => [:string, 'OrderID'], :clerk => [:string, 'Clerk'], :terminal => [:string, 'Terminal'], :table => [:string, 'Table'], :description => [:string, 'Description'], :comments => [:string, 'Comments'], :allow_partial_auth => [:boolean, 'AllowPartialAuth'], :currency => [:string, 'Currency'], :non_tax => [:boolean, 'NonTax'], } #:nodoc: TRANSACTION_DETAIL_MONEY_OPTIONS = { :amount => [:double, 'Amount'], :tax => [:double, 'Tax'], :tip => [:double, 'Tip'], :non_tax => [:boolean, 'NonTax'], :shipping => [:double, 'Shipping'], :discount => [:double, 'Discount'], :subtotal => [:double, 'Subtotal'] } #:nodoc: CREDIT_CARD_DATA_OPTIONS = { :magnetic_stripe => [:string, 'MagStripe'], :dukpt => [:string, 'DUKPT'], :signature => [:string, 'Signature'], :terminal_type => [:string, 'TermType'], :magnetic_support => [:string, 'MagSupport'], :xid => [:string, 'XID'], :cavv => [:string, 'CAVV'], :eci => [:integer, 'ECI'], :internal_card_authorization => [:boolean, 'InternalCardAduth'], :pares => [:string, 'Pares'] } #:nodoc: CHECK_DATA_OPTIONS = { :check_number => [:integer, 'CheckNumber'], :drivers_license => [:string, 'DriversLicense'], :drivers_license_state => [:string, 'DriversLicenseState'], :record_type => [:string, 'RecordType'], :aux_on_us => [:string, 'AuxOnUS'], :epc_code => [:string, 'EpcCode'], :front_image => [:string, 'FrontImage'], :back_image => [:string, 'BackImage'] } #:nodoc: RECURRING_BILLING_OPTIONS = { :schedule => [:string, 'Schedule'], :number_left => [:integer, 'NumLeft'], :enabled => [:boolean, 'Enabled'] } #:nodoc: AVS_RESULTS = { 'Y' => %w( YYY Y YYA YYD ), 'Z' => %w( NYZ Z ), 'A' => %w( YNA A YNY ), 'N' => %w( NNN N NN ), 'X' => %w( YYX X ), 'W' => %w( NYW W ), 'XXW' => %w( XXW ), 'XXU' => %w( XXU ), 'R' => %w( XXR R U E ), 'S' => %w( XXS S ), 'XXE' => %w( XXE ), 'G' => %w( XXG G C I ), 'B' => %w( YYG B M ), 'D' => %w( GGG D ), 'P' => %w( YGG P ) }.inject({}) do |map, (type, codes)| codes.each { |code| map[code] = type } map end #:nodoc: AVS_CUSTOM_MESSAGES = { 'XXW' => 'Card number not on file.', 'XXU' => 'Address information not verified for domestic transaction.', 'XXE' => 'Address verification not allowed for card type.' } #:nodoc: # Create a new gateway. # # ==== Required # * At least the live_url OR the software_id must be present. # * <tt>:software_id</tt> -- 8 character software id # OR # * <tt>:test_url</tt> -- full url for testing # * <tt>:live_url</tt> -- full url for live/production # # ==== Optional # * <tt>:soap_response</tt> -- set to +true+ to add :soap_response to the params hash containing the entire soap xml message # def initialize(options = {}) requires! options, :login, :password @options = options if @options[:software_id] self.live_url = "#{LIVE_URL_BASE}#{@options[:software_id].to_s}" self.test_url = "#{TEST_URL_BASE}#{@options[:software_id].to_s}" else self.live_url = @options[:live_url].to_s self.test_url = @options[:test_url].to_s if @options[:test_url] end super end # Standard Gateway Methods ====================================== # Make a purchase with a credit card. (Authorize and # capture for settlement.) # # Note: See run_transaction for additional options. # def purchase(money, creditcard, options={}) run_sale(options.merge!(:amount => money, :payment_method => creditcard)) end # Authorize an amount on a credit card or account. # # Note: See run_transaction for additional options. # def authorize(money, creditcard, options={}) run_auth_only(options.merge!(:amount => money, :payment_method => creditcard)) end # Capture an authorized transaction. # # Note: See run_transaction for additional options. # def capture(money, identification, options={}) capture_transaction(options.merge!(:amount => money, :reference_number => identification)) end # Void a previous transaction that has not been settled. # # Note: See run_transaction for additional options. # def void(identification, options={}) void_transaction(options.merge!(:reference_number => identification)) end # Credit a previous transaction. # # Note: See run_transaction for additional options. # def credit(money, identification, options={}) refund_transaction(options.merge!(:amount => money, :reference_number => identification)) end # Customer ====================================================== # Add a customer. # # ==== Options # * <tt>:id</tt> -- merchant assigned id # * <tt>:notes</tt> -- notes about customer # * <tt>:data</tt> -- base64 data about customer # * <tt>:url</tt> -- customer website # * <tt>:billing_address</tt> -- usual options # * <tt>:payment_methods</tt> -- array of payment method hashes. # * <tt>:method</tt> -- credit_card or check # * <tt>:name</tt> -- optional name/label for the method # * <tt>:sort</tt> -- optional integer value specifying the backup sort order, 0 is default # # ==== Recurring Options # * <tt>:enabled</tt> -- +true+ enables recurring # * <tt>:schedule</tt> -- daily, weekly, bi-weekly (every two weeks), monthly, bi-monthly (every two months), quarterly, bi-annually (every six months), annually, first of month, last day of month # * <tt>:number_left</tt> -- number of payments left; -1 for unlimited # * <tt>:next</tt> -- date of next payment (Date/Time) # * <tt>:amount</tt> -- amount of recurring payment # * <tt>:tax</tt> -- tax portion of amount # * <tt>:currency</tt> -- numeric currency code # * <tt>:description</tt> -- description of transaction # * <tt>:order_id</tt> -- transaction order id # * <tt>:user</tt> -- merchant username assigned to transaction # * <tt>:source</tt> -- name of source key assigned to billing # * <tt>:send_receipt</tt> -- +true+ to send client a receipt # * <tt>:receipt_note</tt> -- leave a note on the receipt # # ==== Point of Sale Options # * <tt>:price_tier</tt> -- name of customer price tier # * <tt>:tax_class</tt> -- tax class # * <tt>:lookup_code</tt> -- lookup code from customer/member id card; barcode or magnetic stripe; can be assigned by merchant; defaults to system assigned if blank # # ==== Response # * <tt>#message</tt> -- customer number assigned by gateway # def add_customer(options={}) request = build_request(__method__, options) commit(__method__, request) end # Update a customer by replacing all of the customer details.. # # Use quickUpdateCustomer to just update a few attributes. # # ==== Options # * Same as add_customer # def update_customer(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Enable a customer for recurring billing. # # Note: Customer does not need to have all recurring paramerters to succeed. # # ==== Required # * <tt>:customer_number</tt> # def enable_customer(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Disable a customer for recurring billing. # # ==== Required # * <tt>:customer_number</tt> # def disable_customer(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Add a payment method to a customer. # # ==== Required # * <tt>:customer_number</tt> -- number returned by add_customer response.message # * <tt>:payment_method</tt> # * <tt>:method</tt> -- credit_card or check # * <tt>:name</tt> -- optional name/label for the method # * <tt>:sort</tt> -- an integer value specifying the backup sort order, 0 is default # # ==== Optional # * <tt>:make_default</tt> -- set +true+ to make default # * <tt>:verify</tt> -- set +true+ to run auth_only verification; throws fault if cannot verify # # ==== Response # * <tt>#message</tt> -- method_id of new customer payment method # def add_customer_payment_method(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Retrive all of the payment methods belonging to a customer # # ==== Required # * <tt>:customer_number</tt> # # ==== Response # * <tt>#message</tt> -- either a single hash or an array of hashes of payment methods # def get_customer_payment_methods(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Retrive one of the payment methods belonging to a customer # # ==== Required # * <tt>:customer_number</tt> # * <tt>:method_id</tt> # # ==== Response # * <tt>#message</tt> -- hash of payment method # def get_customer_payment_method(options={}) requires! options, :customer_number, :method_id request = build_request(__method__, options) commit(__method__, request) end # Update a customer payment method. # # ==== Required # * <tt>:method_id</tt> -- method_id to update # # ==== Options # * <tt>:method</tt> -- credit_card or check # * <tt>:name</tt> -- optional name/label for the method # * <tt>:sort</tt> -- an integer value specifying the backup sort order, 0 is default # * <tt>:verify</tt> -- set +true+ to run auth_only verification; throws fault if cannot verify # # ==== Response # * <tt>#message</tt> -- hash of payment method # def update_customer_payment_method(options={}) requires! options, :method_id request = build_request(__method__, options) commit(__method__, request) end # Delete one the payment methods beloning to a customer # # ==== Required # * <tt>:customer_number</tt> # * <tt>:method_id</tt> # def delete_customer_payment_method(options={}) requires! options, :customer_number, :method_id request = build_request(__method__, options) commit(__method__, request) end # Delete a customer. # # ==== Required # * <tt>:customer_number</tt> # def delete_customer(options={}) requires! options, :customer_number request = build_request(__method__, options) commit(__method__, request) end # Run a transaction for an existing customer in the database. # # ==== Required Options # * <tt>:customer_number</tt> -- gateway assigned identifier # * <tt>:command</tt> -- Sale, AuthOnly, Credit, Check, CheckCredit # * <tt>:amount</tt> -- total amount # # ==== Options # * <tt>:method_id</tt> -- which payment method to use, 0/nil/omitted for default method # * <tt>:ignore_duplicate</tt> -- +true+ overrides duplicate transaction # * <tt>:client_ip</tt> -- client ip address # * <tt>:customer_receipt</tt> -- +true+, sends receipt to customer. active_merchant defaults to +false+ # * <tt>:customer_email</tt> -- specify if different than customer record # * <tt>:customer_template</tt> -- name of template # * <tt>:merchant_receipt</tt> -- +true+, sends receipt to merchant. active_merchant defaults to +false+ # * <tt>:merchant_email</tt> -- required if :merchant_receipt set to +true+ # * <tt>:merchant_template</tt> -- name of template # * <tt>:recurring</tt> -- defaults to +false+ *see documentation* # * <tt>:verification_value</tt> -- pci forbids storage of this value, only required for CVV2 validation # * <tt>:software</tt> -- active_merchant sets to required gateway option value # * <tt>:line_items</tt> -- XXX not implemented yet # * <tt>:custom_fields</tt> -- XXX not implemented yet # # ==== Transaction Options # * <tt>:invoice</tt> -- transaction invoice number; truncated to 10 characters; defaults to reference_number # * <tt>:po_number</tt> -- commercial purchase order number; upto 25 characters # * <tt>:order_id</tt> -- should be used to assign a unique id; upto 64 characters # * <tt>:clerk</tt> -- sales clerk # * <tt>:terminal</tt> -- terminal name # * <tt>:table</tt> -- table name/number # * <tt>:description</tt> -- description # * <tt>:comments</tt> -- comments # * <tt>:allow_partial_auth</tt> -- allow partial authorization if full amount is not available; defaults +false+ # * <tt>:currency</tt> -- numeric currency code # * <tt>:tax</tt> -- tax portion of amount # * <tt>:tip</tt> -- tip portion of amount # * <tt>:non_tax</tt> -- +true+ if transaction is non-taxable # * <tt>:shipping</tt> -- shipping portion of amount # * <tt>:discount</tt> -- amount of discount # * <tt>:subtotal</tt> -- amount of transaction before tax, tip, shipping, and discount are applied # # ==== Response # * <tt>#message</tt> -- transaction response hash # def run_customer_transaction(options={}) requires! options, :customer_number, :command, :amount request = build_request(__method__, options) commit(__method__, request) end # Transactions ================================================== # Run a transaction. # # Note: run_sale, run_auth_only, run_credit, run_check_sale, run_check_credit # methods are also available. Each takes the same options as # run_transaction, but the :command option is not required. # # Recurring Note: If recurring options are included USA ePay will create a # new customer record with the supplied information. The customer number # will be returned in the response. # # ==== Options # * <tt>:method</tt> -- credit_card or check # * <tt>:command</tt> -- sale, credit, void, creditvoid, authonly, capture, postauth, check, checkcredit; defaults to sale; only required for run_transaction when other than sale # * <tt>:reference_number</tt> -- for the original transaction; obtained by sale or authonly # * <tt>:authorization_code</tt> -- required for postauth; obtained offline # * <tt>:ignore_duplicate</tt> -- set +true+ if you want to override the duplicate tranaction handling # * <tt>:account_holder</tt> -- name of account holder # * <tt>:customer_id</tt> -- merchant assigned id # * <tt>:customer_receipt</tt> -- set +true+ to email receipt to billing email address # * <tt>:customer_template</tt> -- name of template # * <tt>:software</tt> -- stamp merchant software version for tracking # * <tt>:billing_address</tt> -- see UsaEpayCimGateway documentation for all address fields # * <tt>:shipping_address</tt> -- see UsaEpayCimGateway documentation for all address fields # * <tt>:recurring</tt> -- used for recurring billing transactions # * <tt>:schedule</tt> -- disabled, daily, weekly, bi-weekly (every two weeks), monthly, bi-monthly (every two months), quarterly, bi-annually (every six months), annually # * <tt>:next</tt> -- date customer billed next (Date/Time) # * <tt>:expire</tt> -- date the recurring transactions end (Date/Time) # * <tt>:number_left</tt> -- transactions remaining in billing cycle # * <tt>:amount</tt> -- amount to be billed each recurring transaction # * <tt>:enabled</tt> -- states if currently active # * <tt>:line_items</tt> -- XXX not implemented yet # * <tt>:custom_fields</tt> -- XXX not implemented yet # # ==== Transaction Options # * <tt>:amount</tt> -- total amount # * <tt>:invoice</tt> -- transaction invoice number; truncated to 10 characters; defaults to reference_number # * <tt>:po_number</tt> -- commercial purchase order number; upto 25 characters # * <tt>:order_id</tt> -- should be used to assign a unique id; upto 64 characters # * <tt>:clerk</tt> -- sales clerk # * <tt>:terminal</tt> -- terminal name # * <tt>:table</tt> -- table name/number # * <tt>:description</tt> -- description # * <tt>:comments</tt> -- comments # * <tt>:allow_partial_auth</tt> -- allow partial authorization if full amount is not available; defaults +false+ # * <tt>:currency</tt> -- numeric currency code # * <tt>:tax</tt> -- tax portion of amount # * <tt>:tip</tt> -- tip portion of amount # * <tt>:non_tax</tt> -- +true+ if transaction is non-taxable # * <tt>:shipping</tt> -- shipping portion of amount # * <tt>:discount</tt> -- amount of discount # * <tt>:subtotal</tt> -- amount of transaction before tax, tip, shipping, and discount are applied # # ==== Response # * <tt>#message</tt> -- transaction response hash # def run_transaction(options={}) request = build_request(__method__, options) commit(__method__, request) end TRANSACTION_METHODS = [ :run_sale, :run_auth_only, :run_credit, :run_check_sale, :run_check_credit ] #:nodoc: TRANSACTION_METHODS.each do |method| define_method method do |options| request = build_request(method, options) commit(method, request) end end # Post an authorization code obtained offline. # # ==== Required # * <tt>:authorization_code</tt> -- obtained offline # # ==== Options # * Same as run_transaction # # ==== Response # * <tt>#message</tt> -- transaction response hash # def post_auth(options={}) requires! options, :authorization_code request = build_request(__method__, options) commit(__method__, request) end # Capture an authorized transaction and move it into the current batch # for settlement. # # Note: Check with merchant bank for details/restrictions on differing # amounts than the original authorization. # # ==== Required # * <tt>:reference_number</tt> # # ==== Options # * <tt>:amount</tt> -- may be different than original amount; 0 will void authorization # # ==== Response # * <tt>#message</tt> -- transaction response hash # def capture_transaction(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Void a transaction. # # Note: Can only be voided before being settled. # # ==== Required # * <tt>:reference_number</tt> # # ==== Response # * <tt>#message</tt> -- transaction response hash # def void_transaction(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Refund transaction. # # Note: Required after a transaction has been settled. Refunds # both credit card and check transactions. # # ==== Required # * <tt>:reference_number</tt> # * <tt>:amount</tt> -- amount to refund; 0 will refund original amount # # ==== Response # * <tt>#message</tt> -- transaction response hash # def refund_transaction(options={}) requires! options, :reference_number, :amount request = build_request(__method__, options) commit(__method__, request) end # Override transaction flagged for mananager approval. # # Note: Checks only! # # ==== Required # * <tt>:reference_number</tt> # # ==== Options # * <tt>:reason</tt> # # ==== Response # * <tt>#message</tt> -- transaction response hash # def override_transaction(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Quick Transactions ============================================ # Run a sale transaction based off of a past transaction. # # Transfers referenced transaction's payment method to this # transaction. As of 6/2011, USA ePay blocks credit card numbers # at 3 years. # # ==== Required # * <tt>:reference_number</tt> -- transaction to reference payment from # * <tt>:amount</tt> -- total amount # # ==== Options # * <tt>:authorize_only</tt> -- set +true+ if you just want to authorize # # ==== Transaction Options # * <tt>:invoice</tt> -- transaction invoice number; truncated to 10 characters; defaults to reference_number # * <tt>:po_number</tt> -- commercial purchase order number; upto 25 characters # * <tt>:order_id</tt> -- should be used to assign a unique id; upto 64 characters # * <tt>:clerk</tt> -- sales clerk # * <tt>:terminal</tt> -- terminal name # * <tt>:table</tt> -- table name/number # * <tt>:description</tt> -- description # * <tt>:comments</tt> -- comments # * <tt>:allow_partial_auth</tt> -- allow partial authorization if full amount is not available; defaults +false+ # * <tt>:currency</tt> -- numeric currency code # * <tt>:tax</tt> -- tax portion of amount # * <tt>:tip</tt> -- tip portion of amount # * <tt>:non_tax</tt> -- +true+ if transaction is non-taxable # * <tt>:shipping</tt> -- shipping portion of amount # * <tt>:discount</tt> -- amount of discount # * <tt>:subtotal</tt> -- amount of transaction before tax, tip, shipping, and discount are applied # # ==== Response # * <tt>#message</tt> -- transaction response hash # def run_quick_sale(options={}) requires! options, :reference_number, :amount request = build_request(__method__, options) commit(__method__, request) end # Run a credit based off of a past transaction. # # Transfers referenced transaction's payment method to this # transaction. As of 6/2011, USA ePay blocks credit card numbers # at 3 years. # # ==== Required # * <tt>:reference_number</tt> -- transaction to reference payment from # # ==== Transaction Options # * <tt>:amount</tt> -- total amount # * <tt>:invoice</tt> -- transaction invoice number; truncated to 10 characters; defaults to reference_number # * <tt>:po_number</tt> -- commercial purchase order number; upto 25 characters # * <tt>:order_id</tt> -- should be used to assign a unique id; upto 64 characters # * <tt>:clerk</tt> -- sales clerk # * <tt>:terminal</tt> -- terminal name # * <tt>:table</tt> -- table name/number # * <tt>:description</tt> -- description # * <tt>:comments</tt> -- comments # * <tt>:allow_partial_auth</tt> -- allow partial authorization if full amount is not available; defaults +false+ # * <tt>:currency</tt> -- numeric currency code # * <tt>:tax</tt> -- tax portion of amount # * <tt>:tip</tt> -- tip portion of amount # * <tt>:non_tax</tt> -- +true+ if transaction is non-taxable # * <tt>:shipping</tt> -- shipping portion of amount # * <tt>:discount</tt> -- amount of discount # * <tt>:subtotal</tt> -- amount of transaction before tax, tip, shipping, and discount are applied # # ==== Response # * <tt>#message</tt> -- transaction response hash # def run_quick_credit(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Transaction Status ============================================ # Retrieve details of a specified transaction. # # ==== Required # * <tt>:reference_number</tt> # # ==== Response # * <tt>#message</tt> -- transaction hash # def get_transaction(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Check status of a transaction. # # ==== Required # * <tt>:reference_number</tt> # # ==== Response # * <tt>response.success</tt> -- success of the referenced transaction # * <tt>response.message</tt> -- message of the referenced transaction # * <tt>response.authorization</tt> -- same as :reference_number in options # def get_transaction_status(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Check status of a transaction (custom). # # ==== Required # * <tt>:reference_number</tt> # * <tt>:fields</tt> -- string array of fields to retrieve # * <tt>Response.AuthCode</tt> # * <tt>Response.AvsResult</tt> # * <tt>Response.AvsResultCode</tt> # * <tt>Response.BatchNum</tt> # * <tt>Response.CardCodeResult</tt> # * <tt>Response.CardCodeResultCode</tt> # * <tt>Response.ConversionRate</tt> # * <tt>Response.ConvertedAmount</tt> # * <tt>Response.ConvertedAmountCurrency</tt> # * <tt>Response.Error</tt> # * <tt>Response.ErrorCode</tt> # * <tt>Response.RefNum</tt> # * <tt>Response.Result</tt> # * <tt>Response.ResultCode</tt> # * <tt>Response.Status</tt> # * <tt>Response.StatusCode</tt> # * <tt>CheckTrace.TrackingNum</tt> # * <tt>CheckTrace.Effective</tt> # * <tt>CheckTrace.Processed</tt> # * <tt>CheckTrace.Settled</tt> # * <tt>CheckTrace.Returned</tt> # * <tt>CheckTrace.BankNote</tt> # * <tt>DateTime</tt> # * <tt>AccountHolder</tt> # * <tt>Details.Invoice</tt> # * <tt>Details.PoNum</tt> # * <tt>Details.OrderID</tt> # * <tt>Details.Clerk</tt> # * <tt>Details.Terminal</tt> # * <tt>Details.Table</tt> # * <tt>Details.Description</tt> # * <tt>Details.Amount</tt> # * <tt>Details.Currency</tt> # * <tt>Details.Tax</tt> # * <tt>Details.Tip</tt> # * <tt>Details.NonTax</tt> # * <tt>Details.Shipping</tt> # * <tt>Details.Discount</tt> # * <tt>Details.Subtotal</tt> # * <tt>CreditCardData.CardType</tt> # * <tt>CreditCardData.CardNumber</tt> # * <tt>CreditCardData.CardExpiration</tt> # * <tt>CreditCardData.CardCode</tt> # * <tt>CreditCardData.AvsStreet</tt> # * <tt>CreditCardData.AvsZip</tt> # * <tt>CreditCardData.CardPresent</tt> # * <tt>CheckData.CheckNumber</tt> # * <tt>CheckData.Routing</tt> # * <tt>CheckData.Account</tt> # * <tt>CheckData.SSN</tt> # * <tt>CheckData.DriversLicense</tt> # * <tt>CheckData.DriversLicenseState</tt> # * <tt>CheckData.RecordType</tt> # * <tt>User</tt> # * <tt>Source</tt> # * <tt>ServerIP</tt> # * <tt>ClientIP</tt> # * <tt>CustomerID</tt> # * <tt>BillingAddress.FirstName</tt> # * <tt>BillingAddress.LastName</tt> # * <tt>BillingAddress.Company</tt> # * <tt>BillingAddress.Street</tt> # * <tt>BillingAddress.Street2</tt> # * <tt>BillingAddress.City</tt> # * <tt>BillingAddress.State</tt> # * <tt>BillingAddress.Zip</tt> # * <tt>BillingAddress.Country</tt> # * <tt>BillingAddress.Phone</tt> # * <tt>BillingAddress.Fax</tt> # * <tt>BillingAddress.Email</tt> # * <tt>ShippingAddress.FirstName</tt> # * <tt>ShippingAddress.LastName</tt> # * <tt>ShippingAddress.Company</tt> # * <tt>ShippingAddress.Street</tt> # * <tt>ShippingAddress.Street2</tt> # * <tt>ShippingAddress.City</tt> # * <tt>ShippingAddress.State</tt> # * <tt>ShippingAddress.Zip</tt> # * <tt>ShippingAddress.Country</tt> # * <tt>ShippingAddress.Phone</tt> # * <tt>ShippingAddress.Fax</tt> # * <tt>ShippingAddress.Email</tt> # # ==== Response # * <tt>#message</tt> -- hash; keys are the field values # def get_transaction_custom(options={}) requires! options, :reference_number, :fields request = build_request(__method__, options) commit(__method__, request) end # Check status of a check transaction. # # ==== Required # * <tt>:reference_number</tt> # # ==== Response # * <tt>#message</tt> -- check trace hash # def get_check_trace(options={}) requires! options, :reference_number request = build_request(__method__, options) commit(__method__, request) end # Account ======================================================= # Retrieve merchant account details # # ==== Response # * <tt>#message</tt> -- account hash # def get_account_details request = build_request(__method__) commit(__method__, request) end # Builders ====================================================== private # Build soap header, etc. def build_request(action, options = {}) soap = Builder::XmlMarkup.new soap.instruct!(:xml, :version => '1.0', :encoding => 'utf-8') soap.tag! "SOAP-ENV:Envelope", 'xmlns:SOAP-ENV' => 'http://schemas.xmlsoap.org/soap/envelope/', 'xmlns:ns1' => 'urn:usaepay', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:SOAP-ENC' => 'http://schemas.xmlsoap.org/soap/encoding/', 'SOAP-ENV:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' do |soap| soap.tag! "SOAP-ENV:Body" do |soap| send("build_#{action}", soap, options) end end soap.target! end # Build generic tag. def build_tag(soap, type, tag, value) soap.tag!(tag, value, 'xsi:type' => "xsd:#{type}") if value != nil end # Build token. def build_token(soap, options) seed = SecureRandom.base64(32) hash = Digest::SHA1.hexdigest("#{@options[:login]}#{seed}#{@options[:password].to_s.strip}") soap.Token 'xsi:type' => 'ns1:ueSecurityToken' do |soap| build_tag soap, :string, 'ClientIP', options[:client_ip] soap.PinHash 'xsi:type' => 'ns1:ueHash' do |soap| build_tag soap, :string, "HashValue", hash build_tag soap, :string, "Seed", seed build_tag soap, :string, "Type", 'sha1' end build_tag soap, :string, 'SourceKey', @options[:login] end end # Customer ====================================================== def build_add_customer(soap, options) soap.tag! "ns1:addCustomer" do |soap| build_token soap, options build_customer_data soap, options build_tag soap, :double, 'Amount', amount(options[:amount]) build_tag soap, :double, 'Tax', amount(options[:tax]) build_tag soap, :string, 'Next', options[:next].strftime("%Y-%m-%d") if options[:next] end end def build_customer(soap, options, type, add_customer_data=false) soap.tag! "ns1:#{type}" do |soap| build_token soap, options build_tag soap, :integer, 'CustNum', options[:customer_number] build_customer_data soap, options if add_customer_data end end def build_update_customer(soap, options) build_customer(soap, options, 'updateCustomer', true) end def build_enable_customer(soap, options) build_customer(soap, options, 'enableCustomer') end def build_disable_customer(soap, options) build_customer(soap, options, 'disableCustomer') end def build_delete_customer(soap, options) build_customer(soap, options, 'deleteCustomer') end def build_add_customer_payment_method(soap, options) soap.tag! "ns1:addCustomerPaymentMethod" do |soap| build_token soap, options build_tag soap, :integer, 'CustNum', options[:customer_number] build_customer_payment_methods soap, options build_tag soap, :boolean, 'MakeDefault', options[:make_default] build_tag soap, :boolean, 'Verify', options[:verify] end end def build_get_customer_payment_method(soap, options) soap.tag! 'ns1:getCustomerPaymentMethod' do |soap| build_token soap, options build_tag soap, :integer, 'CustNum', options[:customer_number] build_tag soap, :integer, 'MethodID', options[:method_id] end end def build_get_customer_payment_methods(soap, options) build_customer(soap, options, 'getCustomerPaymentMethods') end def build_update_customer_payment_method(soap, options) soap.tag! 'ns1:updateCustomerPaymentMethod' do |soap| build_token soap, options build_customer_payment_methods soap, options build_tag soap, :boolean, 'Verify', options[:verify] end end def build_delete_customer_payment_method(soap, options) soap.tag! "ns1:deleteCustomerPaymentMethod" do |soap| build_token soap, options build_tag soap, :integer, 'Custnum', options[:customer_number] build_tag soap, :integer, 'PaymentMethodID', options[:method_id] end end def build_run_customer_transaction(soap, options) soap.tag! "ns1:runCustomerTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'CustNum', options[:customer_number] build_tag soap, :integer, 'PaymentMethodID', options[:method_id] || 0 build_customer_transaction soap, options end end # Transactions ================================================== def build_run_transaction(soap, options) soap.tag! 'ns1:runTransaction' do |soap| build_token soap, options build_transaction_request_object soap, options, 'Parameters' end end def build_run_sale(soap, options) soap.tag! 'ns1:runSale' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_run_auth_only(soap, options) soap.tag! 'ns1:runAuthOnly' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_run_credit(soap, options) soap.tag! 'ns1:runCredit' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_run_check_sale(soap, options) soap.tag! 'ns1:runCheckSale' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_run_check_credit(soap, options) soap.tag! 'ns1:runCheckCredit' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_post_auth(soap, options) soap.tag! 'ns1:postAuth' do |soap| build_token soap, options build_transaction_request_object soap, options end end def build_run_quick_sale(soap, options) soap.tag! 'ns1:runQuickSale' do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_transaction_detail soap, options build_tag soap, :boolean, 'AuthOnly', options[:authorize_only] || false end end def build_run_quick_credit(soap, options) soap.tag! 'ns1:runQuickCredit' do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_transaction_detail soap, options end end def build_get_transaction(soap, options) soap.tag! "ns1:getTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] end end def build_get_transaction_status(soap, options) soap.tag! "ns1:getTransactionStatus" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] end end def build_get_transaction_custom(soap, options) soap.tag! "ns1:getTransactionCustom" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_transaction_field_array soap, options end end def build_get_check_trace(soap, options) soap.tag! "ns1:getCheckTrace" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] end end def build_capture_transaction(soap, options) soap.tag! "ns1:captureTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_tag soap, :double, 'RefNum', amount(options[:amount]) end end def build_void_transaction(soap, options) soap.tag! "ns1:voidTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] end end def build_refund_transaction(soap, options) soap.tag! "ns1:refundTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_tag soap, :integer, 'Amount', amount(options[:amount]) end end def build_override_transaction(soap, options) soap.tag! "ns1:overrideTransaction" do |soap| build_token soap, options build_tag soap, :integer, 'RefNum', options[:reference_number] build_tag soap, :string, 'Reason', options[:reason] end end # Account ======================================================= def build_get_account_details(soap, options) soap.tag! "ns1:getAccountDetails" do |soap| build_token soap, options end end # Customer Helpers ============================================== def build_customer_data(soap, options) soap.CustomerData 'xsi:type' => 'ns1:CustomerObject' do CUSTOMER_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end build_billing_address soap, options build_customer_payments soap, options build_custom_fields soap, options end end def build_customer_payments(soap, options) if options[:payment_methods] length = options[:payment_methods].length soap.PaymentMethods 'SOAP-ENC:arrayType' => "ns1:PaymentMethod[#{length}]", 'xsi:type' =>"ns1:PaymentMethodArray" do |soap| build_customer_payment_methods soap, options end end end def extract_methods_and_tag(options) case when options[:payment_method] && !options[:payment_methods] payment_methods = [options[:payment_method]] tag_name = 'PaymentMethod' when options[:payment_methods] && !options[:payment_method] payment_methods = options[:payment_methods] tag_name = 'item' else payment_methods = [options] tag_name = 'PaymentMethod' end [payment_methods, tag_name] end def build_credit_card_or_check(soap, payment_method) case when payment_method[:method].kind_of?(ActiveMerchant::Billing::CreditCard) build_tag soap, :string, 'CardNumber', payment_method[:method].number build_tag soap, :string, 'CardExpiration', "#{"%02d" % payment_method[:method].month}#{payment_method[:method].year}" if options[:billing_address] build_tag soap, :string, 'AvsStreet', options[:billing_address][:address1] build_tag soap, :string, 'AvsZip', options[:billing_address][:zip] end build_tag soap, :string, 'CardCode', payment_method[:method].verification_value when payment_method[:method].kind_of?(ActiveMerchant::Billing::Check) build_tag soap, :string, 'Account', payment_method[:method].number build_tag soap, :string, 'Routing', payment_method[:method].routing_number build_tag soap, :string, 'AccountType', payment_method[:method].account_type.capitalize build_tag soap, :string, 'DriversLicense', options[:drivers_license] build_tag soap, :string, 'DriversLicenseState', options[:drivers_license_state] build_tag soap, :string, 'RecordType', options[:record_type] end end def build_customer_payment_methods(soap, options) payment_methods, tag_name = extract_methods_and_tag(options) payment_methods.each do |payment_method| soap.tag! tag_name, 'xsi:type' => "ns1:PaymentMethod" do |soap| build_tag soap, :integer, 'MethodID', payment_method[:method_id] build_tag soap, :string, 'MethodType', payment_method[:type] build_tag soap, :string, 'MethodName', payment_method[:name] build_tag soap, :integer, 'SecondarySort', payment_method[:sort] build_credit_card_or_check(soap, payment_method) end end end def build_customer_transaction(soap, options) soap.Parameters 'xsi:type' => "ns1:CustomerTransactionRequest" do |soap| build_transaction_detail soap, options CUSTOMER_TRANSACTION_REQUEST_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end build_custom_fields soap, options build_line_items soap, options end end # Transaction Helpers =========================================== def build_transaction_request_object(soap, options, name='Params') soap.tag! name, 'xsi:type' => "ns1:TransactionRequestObject" do |soap| TRANSACTION_REQUEST_OBJECT_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end case when options[:payment_method] == nil when options[:payment_method].kind_of?(ActiveMerchant::Billing::CreditCard) build_credit_card_data soap, options when options[:payment_method].kind_of?(ActiveMerchant::Billing::Check) build_check_data soap, options else raise ArgumentError, 'options[:payment_method] must be a CreditCard or Check' end build_transaction_detail soap, options build_billing_address soap, options build_shipping_address soap, options build_recurring_billing soap, options build_line_items soap, options build_custom_fields soap, options end end def build_transaction_detail(soap, options) soap.Details 'xsi:type' => "ns1:TransactionDetail" do |soap| TRANSACTION_DETAIL_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end TRANSACTION_DETAIL_MONEY_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], amount(options[k]) end end end def build_credit_card_data(soap, options) soap.CreditCardData 'xsi:type' => "ns1:CreditCardData" do |soap| build_tag soap, :string, 'CardNumber', options[:payment_method].number build_tag soap, :string, 'CardExpiration', "#{"%02d" % options[:payment_method].month}#{options[:payment_method].year}" if options[:billing_address] build_tag soap, :string, 'AvsStreet', options[:billing_address][:address1] build_tag soap, :string, 'AvsZip', options[:billing_address][:zip] end build_tag soap, :string, 'CardCode', options[:payment_method].verification_value build_tag soap, :boolean, 'CardPresent', options[:card_present] || false CREDIT_CARD_DATA_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end end end def build_check_data(soap, options) soap.CheckData 'xsi:type' => "ns1:CheckData" do |soap| build_tag soap, :string, 'Account', options[:payment_method].number build_tag soap, :string, 'Routing', options[:payment_method].routing_number build_tag soap, :string, 'AccountType', options[:payment_method].account_type.capitalize CHECK_DATA_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[k] end end end def build_recurring_billing(soap, options) if options[:recurring] soap.RecurringBilling 'xsi:type' => "ns1:RecurringBilling" do |soap| build_tag soap, :double, 'Amount', amount(options[:recurring][:amount]) build_tag soap, :string, 'Next', options[:recurring][:next].strftime("%Y-%m-%d") if options[:recurring][:next] build_tag soap, :string, 'Expire', options[:recurring][:expire].strftime("%Y-%m-%d") if options[:recurring][:expire] RECURRING_BILLING_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[:recurring][k] end end end end def build_transaction_field_array(soap, options) soap.Fields 'SOAP-ENC:arryType' => "xsd:string[#{options[:fields].length}]", 'xsi:type' => 'ns1:stringArray' do |soap| options[:fields].each do |field| build_tag soap, :string, 'item', field end end end # General Helpers =============================================== def build_billing_address(soap, options) if options[:billing_address] if options[:billing_address][:name] name = options[:billing_address][:name].split(nil,2) # divide name options[:billing_address][:first_name], options[:billing_address][:last_name] = name[0], name[1] end soap.BillingAddress 'xsi:type' => "ns1:Address" do ADDRESS_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[:billing_address][k] end end end end def build_shipping_address(soap, options) if options[:shipping_address] if options[:shipping_address][:name] name = options[:shipping_address][:name].split(nil,2) # divide name options[:shipping_address][:first_name], options[:shipping_address][:last_name] = name[0], name[1] end soap.ShippingAddress 'xsi:type' => "ns1:Address" do ADDRESS_OPTIONS.each do |k,v| build_tag soap, v[0], v[1], options[:shipping_address][k] end end end end def build_line_items(soap, options) # TODO end def build_custom_fields(soap, options) # TODO end # Request ======================================================= def commit(action, request) url = test? ? test_url : live_url begin soap = ssl_post(url, request, "Content-Type" => "text/xml") rescue ActiveMerchant::ResponseError => error soap = error.response.body end response = build_response(action, soap) end def build_response(action, soap) response_params, success, message, authorization, avs, cvv = parse(action, soap) response_params.merge!('soap_response' => soap) if @options[:soap_response] response = Response.new( success, message, response_params, :test => test?, :authorization => authorization, :avs_result => avs_from(avs), :cvv_result => cvv ) end def avs_from(avs) avs_params = { :code => avs } avs_params.merge!(:message => AVS_CUSTOM_MESSAGES[avs]) if AVS_CUSTOM_MESSAGES.key?(avs) avs_params end def parse(action, soap) xml = REXML::Document.new(soap) root = REXML::XPath.first(xml, "//SOAP-ENV:Body") response = root ? parse_element(root[0]) : { :response => soap } success, message, authorization, avs, cvv = false, FAILURE_MESSAGE, nil, nil, nil fault = (!response) || (response.length < 1) || response.has_key?('faultcode') return [response, success, response['faultstring'], authorization, avs, cvv] if fault if response.respond_to?(:[]) && p = response["#{action}_return"] if p.respond_to?(:key?) && p.key?('result_code') success = p['result_code'] == 'A' ? true : false authorization = p['ref_num'] avs = AVS_RESULTS[p['avs_result_code']] cvv = p['card_code_result_code'] else success = true end message = case action when :get_customer_payment_methods p['item'] when :get_transaction_custom p['item'].inject({}) { |map, field| map[field['field']] = field['value']; map } else p end elsif response.respond_to?(:[]) && p = response[:response] message = p # when response is html end [response, success, message, authorization, avs, cvv] end def parse_element(node) if node.has_elements? response = {} node.elements.each do |e| key = e.name.underscore value = parse_element(e) if response.has_key?(key) if response[key].is_a?(Array) response[key].push(value) else response[key] = [response[key], value] end else response[key] = parse_element(e) end end else response = node.text end response end end end end