contactually/zuora-ruby

View on GitHub
lib/zuora/soap/client.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Zuora
  module Soap
    class Client
      attr_reader :session_token

      SOAP_API_URI = '/apps/services/a/91.0'
      SESSION_TOKEN_XPATH =
        %w[//soapenv:Envelope soapenv:Body api:loginResponse
           api:result api:Session].join('/').freeze

      # Creates a connection instance.
      # Makes an initial SOAP request to fetch session token.
      # Subsequent requests contain the authenticated session id
      # in headers.
      # @param [String] username
      # @param [String] password
      # @param [Boolean] sandbox
      # @return [Zuora::SoapClient]
      def initialize(username, password, sandbox = true)
        @sandbox = sandbox
        authenticate!(username, password)
      end

      # Fire a request
      # @param [Xml] body - an object responding to .xml
      # @return [Zuora::Response]
      def request!(body)
        raise 'body must support .to_xml' unless body.respond_to? :to_xml

        raw_response = connection.post do |request|
          request.url SOAP_API_URI
          request.headers['Content-Type'] = 'text/xml'
          request.body = body.to_xml
        end

        # Handle rate limiting
        return handle_rate_limiting(body) if raw_response.status == 429

        response = Zuora::Response.new(raw_response)
        begin
          response.handle_errors(response.to_h)
        rescue StandardError => e
          return handle_lock_competition(e, body)
        end
        response
      end

      # The primary interface via which users should make SOAP requests.
      # client.call :create, object_name: :BillRun, data: {...}
      # client.call :subscribe, account: {...}, sold_to_contact: {...}
      # @param [Symbol] call_name - one of :create, :subscribe, :amend, :update
      # @return [Faraday:Response] - response
      def call!(call_name, *args)
        factory = Zuora::Dispatcher.send call_name
        xml_builder = factory.new(*args).xml_builder
        request_data = envelope_for call_name, xml_builder
        request! request_data
      end

      private

      # @param [Xml] body
      # @return [Zuora::Response]
      def handle_rate_limiting(body)
        sleep(Zuora::RETRY_WAITING_PERIOD)
        request!(body)
      end

      def handle_lock_competition(error, body)
        if error.message =~ /(Operation failed due to a lock competition)/i
          handle_rate_limiting(body)
        else
          raise error
        end
      end

      # Makes auth request, handles response
      # @return [Faraday::Response]
      # @param [String] username
      # @param [String] password
      def authenticate!(username, password)
        auth_response = call! :login,
          username: username,
          password: password

        handle_auth_response auth_response
      rescue Object => e
        raise Zuora::Errors::SoapConnectionError, e
      end

      # Generate envelope for request
      # @param [Symbol] call_name - one of the supported calls (see #call)
      # @param [Callable] xml_builder_modifier - function taking a builder
      # @return [Nokogiri::XML::Builder]
      def envelope_for(call_name, xml_builder_modifier)
        if call_name == :login
          Zuora::Utils::Envelope.xml(nil, xml_builder_modifier)
        else
          Zuora::Utils::Envelope.authenticated_xml(@session_token) do |b|
            xml_builder_modifier.call b
          end
        end
      end

      # Handle auth response, setting session
      # @params [Faraday::Response]
      # @return [Faraday::Response]
      # @throw [Zuora::Errors::InvalidCredentials]
      def handle_auth_response(response)
        if response.raw.status == 200
          @session_token = extract_session_token response
        else
          message = 'Unable to connect with provided credentials'
          raise Zuora::Errors::InvalidCredentials, message
        end
        response
      end

      # Extracts session token from response and sets instance variable
      # for use in subsequent requests
      # @param [Faraday::Response] response - response to auth request
      def extract_session_token(response)
        response.to_h.envelope.body.login_response.result.session
      end

      # Initializes a connection using api_url
      # @return [Faraday::Connection]
      def connection
        Faraday.new(api_url, ssl: { verify: true }) do |conn|
          conn.adapter Faraday.default_adapter
        end
      end

      # @return [String] - SOAP url based on @sandbox
      def api_url
        host_prefix = @sandbox ? 'sandbox' : ''
        "https://api#{host_prefix}.zuora.com/apps/services/a/91.0"
      end
    end
  end
end