require 'securerandom'
require 'digest'

module ActiveMerchant # :nodoc:
  module Billing # :nodoc:
    # ==== USA ePay Advanced SOAP Interface
    #
    # This class encapsulates 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'

      TEST_URL_BASE = 'https://sandbox.usaepay.com/soap/gate/' # :nodoc:
      LIVE_URL_BASE = 'https://www.usaepay.com/soap/gate/' # :nodoc:

      self.test_url = TEST_URL_BASE
      self.live_url = LIVE_URL_BASE

      FAILURE_MESSAGE = 'Default Failure' # :nodoc:

      self.supported_countries = ['US']
      self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
      self.homepage_url = 'http://www.usaepay.com/'
      self.display_name = 'USA ePay Advanced SOAP Interface'

      CUSTOMER_PROFILE_OPTIONS = {
        id: [:string, 'CustomerID'], # merchant assigned number
        notes: [:string, 'Notes'],
        data: [:string, 'CustomData'],
        url: [:string, 'URL']
      } # :nodoc:

      CUSTOMER_RECURRING_BILLING_OPTIONS = {
        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']
      } # :nodoc:

      CUSTOMER_POINT_OF_SALE_OPTIONS = {
        price_tier: [:string, 'PriceTier'],
        tax_class: [:string, 'TaxClass'],
        lookup_code: [:string, 'LookupCode']
      } # :nodoc:

      CUSTOMER_OPTIONS = [
        CUSTOMER_PROFILE_OPTIONS,
        CUSTOMER_RECURRING_BILLING_OPTIONS,
        CUSTOMER_POINT_OF_SALE_OPTIONS
      ].inject(:merge) # :nodoc:

      COMMON_ADDRESS_OPTIONS = {
        first_name: [:string, 'FirstName'],
        last_name: [:string, 'LastName'],
        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:

      ADDRESS_OPTIONS = [
        COMMON_ADDRESS_OPTIONS,
        {
          address1: [:string, 'Street'],
          address2: [:string, 'Street2']
        }
      ].inject(:merge) # :nodoc

      CUSTOMER_UPDATE_DATA_FIELDS = [
        CUSTOMER_PROFILE_OPTIONS,
        CUSTOMER_RECURRING_BILLING_OPTIONS,
        COMMON_ADDRESS_OPTIONS,
        {
          address1: [:string, 'Address'],
          address2: [:string, 'Address2']
        },
        {
          card_number: [:string, 'CardNumber'],
          card_exp: [:string, 'CardExp'],
          account: [:string, 'Account'],
          routing: [:string, 'Routing'],
          check_format: [:string, 'CheckFormat'],
          record_type: [:string, 'RecordType']
        }
      ].inject(:merge) # :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'],
        recurring: [:boolean, 'isRecurring'],
        verification_value: [:string, 'CardCode'],
        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, 'InternalCardAuth'],
        pares: [:string, 'Pares']
      } # :nodoc:

      CHECK_DATA_OPTIONS = {
        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)

        if options[:software_id]
          self.live_url = "#{LIVE_URL_BASE}#{options[:software_id]}"
          self.test_url = "#{TEST_URL_BASE}#{options[:software_id]}"
        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

      # Refund a previous transaction.
      #
      # Note: See run_transaction for additional options.
      #
      def refund(money, identification, options = {})
        refund_transaction(options.merge!(amount: money, reference_number: identification))
      end

      def credit(money, identification, options = {})
        ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
        refund(money, identification, options)
      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.
      #
      # ==== Required
      # * <tt>:customer_number</tt> -- customer to update
      #
      # ==== Options
      #  * Same as add_customer
      #
      def update_customer(options = {})
        requires! options, :customer_number

        request = build_request(__method__, options)
        commit(__method__, request)
      end

      # Update a customer by replacing only the provided fields.
      #
      # ==== Required
      # * <tt>:customer_number</tt> -- customer to update
      # * <tt>:update_data</tt> -- FieldValue array of fields to retrieve
      #   * <tt>:first_name</tt>
      #   * <tt>:last_name</tt>
      #   * <tt>:id</tt>
      #   * <tt>:company</tt>
      #   * <tt>:address</tt>
      #   * <tt>:address2</tt>
      #   * <tt>:city</tt>
      #   * <tt>:state</tt>
      #   * <tt>:zip</tt>
      #   * <tt>:country</tt>
      #   * <tt>:phone</tt>
      #   * <tt>:fax</tt>
      #   * <tt>:email</tt>
      #   * <tt>:url</tt>
      #   * <tt>:receipt_note</tt>
      #   * <tt>:send_receipt</tt>
      #   * <tt>:notes</tt>
      #   * <tt>:description</tt>
      #   * <tt>:order_id</tt>
      #   * <tt>:enabled</tt>
      #   * <tt>:schedule</tt>
      #   * <tt>:next</tt>
      #   * <tt>:num_left</tt>
      #   * <tt>:amount</tt>
      #   * <tt>:custom_data</tt>
      #   * <tt>:source</tt>
      #   * <tt>:user</tt>
      #   * <tt>:card_number</tt>
      #   * <tt>:card_exp</tt>
      #   * <tt>:account</tt>
      #   * <tt>:routing</tt>
      #   * <tt>:check_format</tt> or <tt>:record_type</tt>
      #
      # ==== Response
      # * <tt>#message</tt> -- boolean; Returns true if successful. Exception thrown all failures.
      #
      def quick_update_customer(options = {})
        requires! options, :customer_number
        requires! options, :update_data

        request = build_request(__method__, options)
        commit(__method__, request)
      end

      # Enable a customer for recurring billing.
      #
      # Note: Customer does not need to have all recurring parameters 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

      # Retrieve 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

      # Retrieve 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 belonging 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>:payment_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 transaction 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 = %i[
        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 manager 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 = {})
        envelope_obj = {
          '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/'
        }
        soap = Builder::XmlMarkup.new
        soap.instruct!(:xml, version: '1.0', encoding: 'utf-8')
        soap.tag! 'SOAP-ENV:Envelope', envelope_obj do
          soap.tag! 'SOAP-ENV:Body' do
            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
          build_tag soap, :string, 'ClientIP', options[:client_ip]
          soap.PinHash 'xsi:type' => 'ns1:ueHash' do
            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
          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
          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_quick_update_customer(soap, options)
        soap.tag! 'ns1:quickUpdateCustomer' do
          build_token soap, options
          build_tag soap, :integer, 'CustNum', options[:customer_number]
          build_field_value_array soap, 'UpdateData', 'FieldValue', options[:update_data], CUSTOMER_UPDATE_DATA_FIELDS
        end
      end

      def build_add_customer_payment_method(soap, options)
        soap.tag! 'ns1:addCustomerPaymentMethod' do
          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
          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
          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
          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
          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
          build_token soap, options
          build_transaction_request_object soap, options, 'Parameters'
        end
      end

      def build_run_sale(soap, options)
        soap.tag! 'ns1:runSale' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_run_auth_only(soap, options)
        soap.tag! 'ns1:runAuthOnly' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_run_credit(soap, options)
        soap.tag! 'ns1:runCredit' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_run_check_sale(soap, options)
        soap.tag! 'ns1:runCheckSale' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_run_check_credit(soap, options)
        soap.tag! 'ns1:runCheckCredit' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_post_auth(soap, options)
        soap.tag! 'ns1:postAuth' do
          build_token soap, options
          build_transaction_request_object soap, options
        end
      end

      def build_run_quick_sale(soap, options)
        soap.tag! 'ns1:runQuickSale' do
          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
          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
          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
          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
          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
          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
          build_token soap, options
          build_tag soap, :integer, 'RefNum', options[:reference_number]
          build_tag soap, :double, 'Amount', amount(options[:amount])
        end
      end

      def build_void_transaction(soap, options)
        soap.tag! 'ns1:voidTransaction' do
          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
          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
          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
          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
            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.to_s[-2..-1]}"
          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].account_number
          build_tag soap, :string, 'Routing', payment_method[:method].routing_number
          build_tag soap, :string, 'AccountType', payment_method[:method].account_type.capitalize unless payment_method[:method].account_type.nil?
          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
            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
          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
          TRANSACTION_REQUEST_OBJECT_OPTIONS.each do |k, v|
            build_tag soap, v[0], v[1], options[k]
          end
          case
          when options[:payment_method].nil?
            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
          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
          build_tag soap, :string, 'CardNumber', options[:payment_method].number
          build_tag soap, :string, 'CardExpiration', build_card_expiration(options)
          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_card_expiration(options)
        month = options[:payment_method].month
        year  = options[:payment_method].year
        "#{'%02d' % month}#{year.to_s[-2..-1]}" unless month.nil? || year.nil?
      end

      def build_check_data(soap, options)
        soap.CheckData 'xsi:type' => 'ns1:CheckData' do
          build_tag soap, :integer, 'CheckNumber', options[:payment_method].number
          build_tag soap, :string, 'Account', options[:payment_method].account_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
            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
          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]
          options[:billing_address][:first_name], options[:billing_address][:last_name] = split_names(options[:billing_address][:name]) if options[:billing_address][:name]
          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]
          options[:shipping_address][:first_name], options[:shipping_address][:last_name] = split_names(options[:shipping_address][:name]) if options[:shipping_address][:name]
          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_field_value_array(soap, tag_name, type, custom_data, fields)
        soap.tag! tag_name, 'SOAP-ENC:arryType' => "xsd:#{type}[#{options.length}]", 'xsi:type' => "ns1:#{type}Array" do
          custom_data.each do |k, v|
            build_field_value soap, fields[k][1], v, fields[k][0] if fields.key?(k)
          end
        end
      end

      def build_field_value(soap, field, value, value_type)
        soap.FieldValue 'xsi:type' => 'ns1:FieldValue' do
          build_tag soap, :string, 'Field', field
          build_tag soap, value_type, 'Value', value
        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 => e
          soap = e.response.body
        end

        build_response(action, soap)
      end

      def build_response(action, soap)
        response_params, success, message, authorization, avs, cvv = parse(action, soap)

        response_params['soap_response'] = soap if @options[:soap_response]

        Response.new(
          success,
          message,
          response_params,
          test: test?,
          authorization:,
          avs_result: avs_from(avs),
          cvv_result: cvv
        )
      end

      def avs_from(avs)
        avs_params = { code: avs }
        avs_params[: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'
            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
              items = p['item'].kind_of?(Array) ? p['item'] : [p['item']]
              items.inject({}) { |hash, item| hash[item['field']] = item['value']; hash }
            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
