lib/glific/providers/gupshup/partner/partner_api.ex

defmodule Glific.Providers.Gupshup.PartnerAPI do
  @moduledoc """
  A module to handle fetching tier related information like quality rating and app rating using partner API
  """

  alias Glific.{
    Caches,
    Partners,
    Partners.Saas
  }

  use Tesla

  alias Plug.Conn.Query
  alias Tesla.Multipart

  plug(Tesla.Middleware.FormUrlencoded,
    encode: &Query.encode/1
  )

  @partner_url "https://partner.gupshup.io/partner/account"

  @doc """
    Fetch App details based on API key and App name
  """
  @spec fetch_app_details(non_neg_integer()) :: any()
  def fetch_app_details(org_id) do
    organization = Partners.organization(org_id)
    gupshup_secrets = organization.services["bsp"].secrets

    post_request(
      @partner_url <> "/api/appLink",
      %{
        apiKey: gupshup_secrets["api_key"],
        appName: gupshup_secrets["app_name"]
      },
      token_type: :partner_token
    )
    |> case do
      {:ok, res} ->
        res["partnerApps"]

      error ->
        error
    end
  end

  @doc """
  Fetches Partner token and App Access token to get tier information
  for an organization with input app id
  """
  @spec get_quality_rating(non_neg_integer()) :: {:error, any} | {:ok, map()}
  def get_quality_rating(org_id) do
    (app_url(org_id) <> "/ratings")
    |> get_request(org_id: org_id)
    |> case do
      {:ok, res} ->
        {:ok,
         %{
           current_limit: res["currentLimit"],
           event: res["event"],
           event_time: res["eventTime"],
           previous_limit: res["oldLimit"]
         }}

      error ->
        {:error, "Error while getting the ratings. #{inspect(error)}"}
    end
  end

  @doc """
   Get gupshup media handle id based on giving org id and the url
  """
  @spec get_media_handle_id(non_neg_integer, binary, any) :: String.t()
  def get_media_handle_id(org_id, url, _type \\ "") do
    data =
      Multipart.new()
      |> Multipart.add_field("file", url)
      |> Multipart.add_field("file_type", MIME.from_path(url))

    (app_url(org_id) <> "/upload/media")
    |> post_request(data,
      org_id: org_id
    )
    |> case do
      {:ok, %{"status" => "success", "handleId" => %{"message" => handle_id}} = _res} ->
        handle_id

      {:error, error} ->
        raise(error)
    end
  end

  @doc """
  Remove hsm template from the WABA.
  """
  @spec delete_hsm_template(non_neg_integer, binary) :: tuple()
  def delete_hsm_template(org_id, element_name) do
    (app_url(org_id) <> "/template/" <> element_name)
    |> delete_request(org_id: org_id)
    |> case do
      {:ok, %{"status" => "success"} = res} ->
        {:ok, res}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Remove hsm template from the WABA.
  """
  @spec apply_for_template(non_neg_integer(), map) :: tuple()
  def apply_for_template(org_id, payload) do
    payload = Map.put(payload, "appId", app_id!(org_id))

    (app_url(org_id) <> "/templates")
    |> post_request(payload,
      org_id: org_id
    )
    |> case do
      {:ok, response} ->
        {:ok, response}

      {:error, error} ->
        {:error, error}
    end
  end

  @global_organization_id 0
  @spec get_partner_token :: {:ok, map()} | {:error, any}
  defp get_partner_token do
    # disabling the cache refresh because by default whenever
    # we fetch the info from cache it refreshes the TTL
    {:ok, partner_token} =
      Caches.get(@global_organization_id, "partner_token", refresh_cache: false)

    if partner_token,
      do: {:ok, %{partner_token: partner_token}},
      else: fetch_partner_token()
  end

  @spec fetch_partner_token :: {:ok, map()} | {:error, any}
  defp fetch_partner_token do
    url = @partner_url <> "/login"
    credentials = Saas.isv_credentials()

    request_params = %{"email" => credentials["email"], "password" => credentials["password"]}

    post(url, request_params, headers: [])
    |> case do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        res = Jason.decode!(body)

        {:ok, token} =
          Caches.set(@global_organization_id, "partner_token", res["token"], ttl: :timer.hours(22))

        {:ok, %{partner_token: token}}

      {_status, response} ->
        {:error, "Could not fetch the partner token #{inspect(response)}"}
    end
  end

  @spec get_partner_app_token(non_neg_integer) ::
          {:error, String.t()} | {:ok, %{partner_app_token: any}}
  defp get_partner_app_token(org_id) do
    {:ok, partner_app_token} = Caches.get(org_id, "partner_app_token", refresh_cache: false)

    if partner_app_token,
      do: {:ok, %{partner_app_token: partner_app_token}},
      else: fetch_partner_app_token(org_id)
  end

  @spec fetch_partner_app_token(non_neg_integer) ::
          {:error, String.t()} | {:ok, %{partner_app_token: any}}
  defp fetch_partner_app_token(org_id) do
    url = app_url(org_id) <> "/token"

    get_request(url, token_type: :partner_token)
    |> case do
      {:ok, res} ->
        app_token = get_in(res, ["token", "token"])
        Caches.set(org_id, "partner_app_token", app_token, ttl: :timer.hours(22))
        {:ok, %{partner_app_token: app_token}}

      {:error, error} ->
        {:error, "Could not fetch the partner app token #{inspect(error)}"}
    end
  end

  @spec headers(atom(), Keyword.t()) :: list()
  defp headers(:app_token, opts) do
    org_id = Keyword.get(opts, :org_id)
    {:ok, %{partner_app_token: partner_app_token}} = get_partner_app_token(org_id)
    [{"token", partner_app_token}, {"Authorization", partner_app_token}]
  end

  defp headers(:partner_token, _opts) do
    get_partner_token()
    |> case do
      {:ok, %{partner_token: partner_token}} ->
        [
          {"token", partner_token},
          {"Authorization", partner_token}
        ]

      _ ->
        []
    end
  end

  @spec post_request(String.t(), map(), Keyword.t()) :: tuple()
  defp post_request(url, data, opts) do
    req_headers = headers(Keyword.get(opts, :token_type, :app_token), opts)

    post(url, data, headers: req_headers)
    |> case do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        {:ok, Jason.decode!(body)}

      err ->
        {:error, "#{inspect(err)}"}
    end
  end

  @spec get_request(String.t(), Keyword.t()) :: tuple()
  defp get_request(url, opts) do
    req_headers = headers(Keyword.get(opts, :token_type, :app_token), opts)

    get(url, headers: req_headers)
    |> case do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        {:ok, Jason.decode!(body)}

      err ->
        {:error, "#{inspect(err)}"}
    end
  end

  @spec delete_request(String.t(), Keyword.t()) :: tuple()
  defp delete_request(url, opts) do
    req_headers = headers(Keyword.get(opts, :token_type, :app_token), opts)

    delete(url, headers: req_headers)
    |> case do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        {:ok, Jason.decode!(body)}

      err ->
        {:error, "#{inspect(err)}"}
    end
  end

  @spec app_id(non_neg_integer()) :: {:ok, String.t()} | {:error, String.t()}
  defp app_id(org_id) do
    organization = Partners.organization(org_id)
    gupshup_secrets = organization.services["bsp"].secrets

    if gupshup_secrets["app_id"] in [nil, ""] do
      {:error, "App Id not found."}
    else
      {:ok, gupshup_secrets["app_id"]}
    end
  end

  @spec app_id!(non_neg_integer()) :: String.t()
  defp app_id!(org_id) do
    {:ok, app_id} = app_id(org_id)
    app_id
  end

  @app_url "https://partner.gupshup.io/partner/app/"

  @spec app_url(non_neg_integer()) :: String.t()
  defp app_url(org_id),
    do: @app_url <> app_id!(org_id)
end