lib/adyen/client.rb



# rubocop:disable Metrics/ParameterLists
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/MethodLength
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/ClassLength

require 'faraday'
require 'json'
require_relative './errors'
require_relative './result'

module Adyen
  class Client
    attr_accessor :ws_user, :ws_password, :api_key, :oauth_token, :client, :adapter
    attr_reader :env, :connection_options, :adapter_options

    def initialize(ws_user: nil, ws_password: nil, api_key: nil, oauth_token: nil, env: :live, adapter: nil, mock_port: 3001,
                   live_url_prefix: nil, mock_service_url_base: nil, connection_options: nil, adapter_options: nil)
      @ws_user = ws_user
      @ws_password = ws_password
      @api_key = api_key
      @oauth_token = oauth_token
      @env = env
      @adapter = adapter || Faraday.default_adapter
      if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new('2.1')
        # for faraday 2.1 and higher
        @adapter_options = adapter_options || Faraday.default_adapter_options
      else
        # for faraday 1.x and 2.0
        @adapter_options = adapter_options || {}
      end
      @mock_service_url_base = mock_service_url_base || "http://localhost:#{mock_port}"
      @live_url_prefix = live_url_prefix
      @connection_options = connection_options || Faraday::ConnectionOptions.new
    end

    # make sure that env can only be :live, :test, or :mock
    def env=(value)
      raise ArgumentError, "Invalid value for Client.env: '#{value}'' - must be one of [:live, :test, :mock]" unless %i[
        live test mock
      ].include? value

      @env = value
    end

    # remove 'https' from live_url_prefix if necessary
    def live_url_prefix=(value)
      value['https://'] = '' unless value['https://'].nil?
      @live_url_prefix = value
    end

    # base URL for API given service and @env
    def service_url_base(service)
      if @env == :mock
        @mock_service_url_base
      else
        case service
        when 'Checkout'
          url = "https://checkout-#{@env}.adyen.com"
          supports_live_url_prefix = true
        when 'Account', 'Fund', 'Notification', 'Hop'
          url = "https://cal-#{@env}.adyen.com/cal/services/#{service}"
          supports_live_url_prefix = false
        when 'Recurring', 'Payment', 'Payout', 'BinLookup', 'StoredValue', 'BalanceControlService'
          url = "https://pal-#{@env}.adyen.com/pal/servlet/#{service}"
          supports_live_url_prefix = true
        when 'PosTerminalManagement'
          url = "https://postfmapi-#{@env}.adyen.com/postfmapi/terminal"
          supports_live_url_prefix = false
        when 'DataProtectionService', 'DisputesService'
          url = "https://ca-#{@env}.adyen.com/ca/services/#{service}"
          supports_live_url_prefix = false
        when 'LegalEntityManagement'
          url = "https://kyc-#{@env}.adyen.com/lem"
          supports_live_url_prefix = false
        when 'BalancePlatform'
          url = "https://balanceplatform-api-#{@env}.adyen.com/bcl"
          supports_live_url_prefix = false
        when 'Transfers'
          url = "https://balanceplatform-api-#{@env}.adyen.com/btl"
          supports_live_url_prefix = false
        when 'Management'
          url = "https://management-#{@env}.adyen.com"
          supports_live_url_prefix = false
        when 'TerminalCloudAPI'
          url = "https://terminal-api-#{@env}.adyen.com"
          supports_live_url_prefix = false
        else
          raise ArgumentError, 'Invalid service specified'
        end

        if @live_url_prefix.nil? && (@env == :live) && supports_live_url_prefix
          raise ArgumentError,
                "Please set Client.live_url_prefix to the portion \
          of your merchant-specific URL prior to '-[service]-live.adyenpayments.com'"
        end

        if @env == :live && supports_live_url_prefix
          url.insert(8, "#{@live_url_prefix}-")
          url['adyen.com'] = 'adyenpayments.com'
        end

        url
      end
    end

    # construct full URL from service and endpoint
    def service_url(service, action, version)
      if service == "Checkout" && @env == :live
        return "#{service_url_base(service)}/checkout/v#{version}/#{action}"
      elsif version == nil
        return "#{service_url_base(service)}/#{action}"
      else
        return "#{service_url_base(service)}/v#{version}/#{action}"
      end
    end

    # send request to adyen API
    def call_adyen_api(service, action, request_data, headers, version, _with_application_info: false)
      # get URL for requested endpoint
      url = service_url(service, action.is_a?(String) ? action : action.fetch(:url), version)

      auth_type = auth_type(service, request_data)

      # initialize Faraday connection object
      conn = Faraday.new(url, @connection_options) do |faraday|
        faraday.adapter @adapter, **@adapter_options
        faraday.headers['Content-Type'] = 'application/json'
        faraday.headers['User-Agent'] = "#{Adyen::NAME}/#{Adyen::VERSION}"

        # set header based on auth_type and service
        auth_header(auth_type, faraday)

        # add optional headers if specified in request
        # will overwrite default headers if overlapping
        headers.map do |key, value|
          faraday.headers[key] = value
        end

        # add library headers
        faraday.headers['adyen-library-name'] = Adyen::NAME
        faraday.headers['adyen-library-version'] = Adyen::VERSION
      end
      # if json string convert to hash
      # needed to add applicationInfo
      request_data = JSON.parse(request_data) if request_data.is_a?(String)

      # convert to json
      request_data = request_data.to_json

      if action.is_a?(::Hash)
        if action.fetch(:method) == 'get'
          begin
            response = conn.get
          rescue Faraday::ConnectionFailed => e
            raise e, "Connection to #{url} failed"
          end
        end
        if action.fetch(:method) == 'delete'
          begin
            response = conn.delete
          rescue Faraday::ConnectionFailed => e
            raise e, "Connection to #{url} failed"
          end
        end
        if action.fetch(:method) == 'patch'
          begin
            response = conn.patch do |req|
              req.body = request_data
            end
          rescue Faraday::ConnectionFailed => e
            raise e, "Connection to #{url} failed"
          end
        end
        if action.fetch(:method) == 'post'
          # post request to Adyen
          begin
            response = conn.post do |req|
              req.body = request_data
            end
          rescue Faraday::ConnectionFailed => e
            raise e, "Connection to #{url} failed"
          end
        end
      else
        begin
          response = conn.post do |req|
            req.body = request_data
          end
        rescue Faraday::ConnectionFailed => e
          raise e, "Connection to #{url} failed"
        end
      end
      # check for API errors
      case response.status
      when 401
        raise Adyen::AuthenticationError.new(
          'Invalid API authentication; https://docs.adyen.com/user-management/how-to-get-the-api-key', request_data
        )
      when 403
        raise Adyen::PermissionError.new('Missing user permissions; https://docs.adyen.com/user-management/user-roles',
                                         request_data, response.body)
      end

      # delete has no response.body (unless it throws an error)
      if response.body.nil? || response.body === ''
        AdyenResult.new('{}', response.headers, response.status)
      # terminal API async call returns always 'ok'
      elsif response.body === 'ok'
        AdyenResult.new('{}', response.headers, response.status)
      else
        AdyenResult.new(response.body, response.headers, response.status)
      end
    end

    # services
    def checkout
      @checkout ||= Adyen::Checkout.new(self)
    end

    def payment
      @payment ||= Adyen::Payment.new(self)
    end

    def payout
      @payout ||= Adyen::Payout.new(self)
    end

    def recurring
      @recurring ||= Adyen::Recurring.new(self)
    end

    def marketpay
      @marketpay ||= Adyen::Marketpay::Marketpay.new(self)
    end

    def pos_terminal_management
      @pos_terminal_management ||= Adyen::PosTerminalManagement.new(self)
    end

    def data_protection
      @data_protection ||= Adyen::DataProtection.new(self)
    end

    def disputes
      @disputes ||= Adyen::Disputes.new(self)
    end

    def bin_lookup
      @bin_lookup ||= Adyen::BinLookup.new(self)
    end

    def legal_entity_management
      @legal_entity_management ||= Adyen::LegalEntityManagement.new(self)
    end

    def balance_platform
      @balance_platform ||= Adyen::BalancePlatform.new(self)
    end

    def transfers
      @transfers ||= Adyen::Transfers.new(self)
    end

    def management
      @management ||= Adyen::Management.new(self)
    end

    def stored_value
      @stored_value ||= Adyen::StoredValue.new(self)
    end

    def balance_control_service
      @balance_control_service ||= Adyen::BalanceControlService.new(self)
    end

    def terminal_cloud_api
      @terminal_cloud_api ||= Adyen::TerminalCloudAPI.new(self)
    end

    private

    def auth_header(auth_type, faraday)
      case auth_type
      when "basic"
        if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new('2.0')
          # for faraday 2.0 and higher
          faraday.request :authorization, :basic, @ws_user, @ws_password
        else
          # for faraday 1.x
          faraday.basic_auth(@ws_user, @ws_password)
        end
      when "api-key"
        faraday.headers["x-api-key"] = @api_key
      when "oauth"
        faraday.headers["Authorization"] = "Bearer #{@oauth_token}"
      end
    end

    def auth_type(service, request_data)
      # make sure valid authentication has been provided
      validate_auth_type(service, request_data)
      # Will prioritize authentication methods in this order:
      # api-key, oauth, basic
      return "api-key" unless @api_key.nil?
      return "oauth" unless @oauth_token.nil?
      "basic"
    end

    def validate_auth_type(service, request_data)
      # ensure authentication has been provided
      if @api_key.nil? && @oauth_token.nil? && (@ws_password.nil? || @ws_user.nil?)
        raise Adyen::AuthenticationError.new(
          'No authentication found - please set api_key, oauth_token, or ws_user and ws_password',
          request_data
        )
      end
      if service == "PaymentSetupAndVerification" && @api_key.nil? && @oauth_token.nil? && @ws_password.nil? && @ws_user.nil?
        raise Adyen::AuthenticationError.new('Checkout service requires API-key or oauth_token', request_data),
              'Checkout service requires API-key or oauth_token'
      end
    end
  end
end
# rubocop:enable all