lib/glific/providers/gupshup/contacts.ex

defmodule Glific.Providers.GupshupContacts do
  @moduledoc """
  Contacts API layer between application and Gupshup
  """

  use Publicist

  @behaviour Glific.Providers.ContactBehaviour

  import GlificWeb.Gettext

  alias Glific.{
    Contacts,
    Contacts.Contact,
    Partners,
    Partners.Organization,
    Providers.Gupshup.ApiClient
  }

  @days_shift -14

  @doc """
    Update a contact phone as opted in
  """

  @spec optin_contact(map()) ::
          {:ok, Contact.t()} | {:error, Ecto.Changeset.t()} | {:error, list()}
  def optin_contact(%{organization_id: organization_id} = attrs) do
    ApiClient.optin_contact(organization_id, %{user: attrs.phone})
    |> case do
      {:ok, %Tesla.Env{status: status}} when status in 200..299 ->
        Contacts.contact_opted_in(
          attrs,
          organization_id,
          attrs[:optin_time] || DateTime.utc_now(),
          method: attrs[:method] || "BSP"
        )

      _ ->
        {:error, ["gupshup", "couldn't connect"]}
    end
  end

  @doc """
  Fetch opted in contacts data from providers server
  """
  @spec fetch_opted_in_contacts(map()) :: :ok | {:error, String.t()}
  def fetch_opted_in_contacts(attrs) do
    organization = Partners.organization(attrs.organization_id)

    result =
      ApiClient.fetch_opted_in_contacts(attrs.organization_id)
      |> validate_opted_in_contacts()

    case result do
      {:ok, users} ->
        update_contacts(users, organization)
        :ok

      error ->
        error
    end
  end

  @doc """
  Perform the gupshup API call and parse the results for downstream functions.
  We need to think about if we want to add him to behaviour
  """
  @spec validate_opted_in_contacts(Tesla.Env.result()) :: {:ok, list()} | {:error, String.t()}
  def validate_opted_in_contacts(result) do
    case result do
      {:ok, %Tesla.Env{status: status, body: body}} when status in 200..299 ->
        {:ok, response_data} = Jason.decode(body)

        if response_data["status"] == "error" do
          {:error, dgettext("errors", "Message: %{message}", message: response_data["message"])}
        else
          users = response_data["users"]
          {:ok, users}
        end

      {:ok, %Tesla.Env{status: status}} when status in 400..499 ->
        {:error, dgettext("errors", "Invalid BSP API key")}

      {:error, %Tesla.Error{reason: reason}} ->
        {:error, dgettext("errors", "Reason: %{reason}", reason: reason)}
    end
  end

  @spec update_contacts(list() | nil, Organization.t() | nil) :: :ok | any()
  defp update_contacts(users, organization) do
    Enum.each(users, fn user ->
      if user["optinStatus"] == "OPT_IN" do
        # handle scenario when contact has not sent a message yet
        last_message_at = last_message_at(user["lastMessageTimeStamp"])

        {:ok, optin_time} = DateTime.from_unix(user["optinTimeStamp"], :millisecond)

        phone = user["countryCode"] <> user["phoneCode"]

        Contacts.upsert(%{
          phone: phone,
          last_message_at: last_message_at,
          optin_time: optin_time |> DateTime.truncate(:second),
          optin_status: true,
          optin_method: user["optinSource"],
          bsp_status: check_bsp_status(last_message_at),
          organization_id: organization.id,
          language_id: organization.default_language_id,
          last_communication_at: last_message_at
        })
      end
    end)
  end

  @spec last_message_at(non_neg_integer()) :: DateTime.t()
  defp last_message_at(0) do
    Timex.shift(DateTime.utc_now(), days: @days_shift)
  end

  defp last_message_at(time) do
    DateTime.from_unix(time, :millisecond)
    |> elem(1)
    |> DateTime.truncate(:second)
  end

  @spec check_bsp_status(DateTime.t()) :: atom()
  defp check_bsp_status(last_message_at) do
    if Timex.diff(DateTime.utc_now(), last_message_at, :hours) < 24 do
      :session_and_hsm
    else
      :hsm
    end
  end
end