module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class ExactGateway < Gateway
      self.live_url = self.test_url = 'https://secure2.e-xact.com/vplug-in/transaction/rpc-enc/service.asmx'

      API_VERSION = '8.5'

      TEST_LOGINS = [{ login: 'A00049-01', password: 'test1' },
                     { login: 'A00427-01', password: 'testus' }]

      TRANSACTIONS = { sale: '00',
                       authorization: '01',
                       capture: '32',
                       credit: '34' }

      ENVELOPE_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
                              'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
                              'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' }

      SEND_AND_COMMIT_ATTRIBUTES = { 'xmlns:n1' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/Request',
                                     'env:encodingStyle' => 'http://schemas.xmlsoap.org/soap/encoding/' }

      SEND_AND_COMMIT_SOURCE_ATTRIBUTES = { 'xmlns:n2' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/encodedTypes',
                                            'xsi:type' => 'n2:Transaction' }

      POST_HEADERS = { 'soapAction' => 'http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/SendAndCommit',
                       'Content-Type' => 'text/xml' }

      SUCCESS = 'true'

      SENSITIVE_FIELDS = %i[verification_str2 expiry_date card_number]

      self.supported_countries = %w[CA US]
      self.supported_cardtypes = %i[visa master american_express jcb discover]
      self.homepage_url = 'http://www.e-xact.com'
      self.display_name = 'E-xact'

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

        super
      end

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

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

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

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

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

      private

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

        xml.instruct!
        xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
          xml.tag! 'env:Body' do
            xml.tag! 'n1:SendAndCommit', SEND_AND_COMMIT_ATTRIBUTES do
              xml.tag! 'SendAndCommitSource', SEND_AND_COMMIT_SOURCE_ATTRIBUTES do
                add_credentials(xml)
                add_transaction_type(xml, action)
                xml << body
              end
            end
          end
        end
        xml.target!
      end

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

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

        xml.target!
      end

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

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

        xml.target!
      end

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

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

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

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

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

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

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

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

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

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

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

      def commit(action, request)
        response = parse(ssl_post(self.live_url, build_request(action, request), POST_HEADERS))

        Response.new(
          successful?(response),
          message_from(response),
          response,
          test: test?,
          authorization: authorization_from(response),
          avs_result: { code: response[:avs] },
          cvv_result: response[:cvv2]
        )
      rescue ResponseError => e
        case e.response.code
        when '401'
          return Response.new(false, "Invalid Login: #{e.response.body}", {}, test: test?)
        else
          raise
        end
      end

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

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

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

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

        if root = REXML::XPath.first(xml, '//types:TransactionResult')
          parse_elements(response, root)
        elsif root = REXML::XPath.first(xml, '//soap:Fault')
          parse_elements(response, root)
        end

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

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