lib/glific/saas/consulting_hour.ex

defmodule Glific.Saas.ConsultingHour do
  @moduledoc """
  The table structure to record consulting hours
  """

  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query, warn: false

  alias __MODULE__

  alias Glific.{
    Partners.Organization,
    Repo
  }

  # define all the required fields for
  @required_fields [
    :organization_id,
    :participants,
    :staff,
    :when,
    :duration,
    :content
  ]

  # define all the optional fields for organization
  @optional_fields [
    :organization_name,
    :is_billable
  ]

  @type t() :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: non_neg_integer | nil,
          organization_name: String.t() | nil,
          participants: String.t() | nil,
          staff: String.t() | nil,
          when: DateTime.t() | nil,
          duration: non_neg_integer | nil,
          is_billable: boolean() | true,
          content: String.t() | nil,
          organization_id: non_neg_integer | nil,
          organization: Organization.t() | Ecto.Association.NotLoaded.t() | nil,
          inserted_at: :utc_datetime | nil,
          updated_at: :utc_datetime | nil
        }

  schema "consulting_hours" do
    field :organization_name, :string
    field :participants, :string
    field :staff, :string

    field :when, :utc_datetime
    field :duration, :integer
    field :content, :string

    field :is_billable, :boolean, default: true

    belongs_to :organization, Organization

    timestamps(type: :utc_datetime)
  end

  @doc """
  Standard changeset pattern we use for all data types
  """
  @spec changeset(ConsultingHour.t(), map()) :: Ecto.Changeset.t()
  def changeset(consulting_hour, attrs) do
    consulting_hour
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> validate_required(@required_fields)
    |> unique_constraint([:when, :staff, :organization_id],
      message: "Sorry, Consulting hours are already filled for this call"
    )
    |> foreign_key_constraint(:organization_id)
  end

  @doc """
  Create a consulting_hour record
  """
  @spec create_consulting_hour(map()) :: {:ok, ConsultingHour.t()} | {:error, Ecto.Changeset.t()}
  def create_consulting_hour(attrs) do
    %ConsultingHour{}
    |> changeset(Map.put(attrs, :organization_id, attrs.organization_id))
    |> Repo.insert()
  end

  @doc """
  Retrieve a consulting_hour record by clauses
  """
  @spec get_consulting_hour(map()) :: ConsultingHour.t() | nil
  def get_consulting_hour(clauses),
    do: Repo.get_by(ConsultingHour, clauses, skip_organization_id: true)

  @doc """
  Returns the list of consulting hours.

  ## Examples

      iex> list_consulting_hours()
      [%ConsultingHour{}, ...]

  """
  @spec list_consulting_hours(map()) :: [ConsultingHour.t()]
  def list_consulting_hours(args),
    do:
      Repo.list_filter(args, ConsultingHour, &Repo.opts_with_inserted_at/2, &filter_with/2,
        skip_organization_id: true
      )

  @beginning_of_day ~T[00:00:00.000]
  @end_of_day ~T[23:59:59.000]

  @doc """
  Return the count of consulting hours, using the same filter as list_consulting_hours
  """
  @spec fetch_consulting_hours(map()) :: String.t()
  def fetch_consulting_hours(args) do
    start_time = DateTime.new!(args.filter.start_date, @beginning_of_day, "Etc/UTC")
    end_time = DateTime.new!(args.filter.end_date, @end_of_day, "Etc/UTC")

    ConsultingHour
    |> where([m], m.organization_id == ^args.filter.client_id)
    |> where([m], m.when >= ^start_time)
    |> where([m], m.when <= ^end_time)
    |> Repo.all(skip_organization_id: true)
    |> convert_to_csv_string()
  end

  @default_headers "content,duration,inserted_at,is_billable,organization_name,participants,staff,when\n"
  @minimal_map [
    :content,
    :duration,
    :inserted_at,
    :is_billable,
    :organization_name,
    :participants,
    :staff,
    :when
  ]

  @doc false
  @spec convert_to_csv_string([ConsultingHour.t()]) :: String.t()
  def convert_to_csv_string(consulting_hours) do
    consulting_hours
    |> Enum.reduce(@default_headers, fn consulting_hour, acc ->
      acc <> minimal_map(consulting_hour) <> "\n"
    end)
  end

  @spec minimal_map(ConsultingHour.t()) :: String.t()
  defp minimal_map(consulting_hour) do
    consulting_hour
    |> Map.take(@minimal_map)
    |> convert_time()
    |> parse_delimiter(:staff)
    |> parse_delimiter(:participants)
    |> parse_delimiter(:content)
    |> Map.values()
    |> Enum.reduce("", fn key, acc ->
      acc <> if is_binary(key), do: "#{key},", else: "#{inspect(key)},"
    end)
  end

  @doc false
  @spec parse_delimiter(map(), atom()) :: map()
  def parse_delimiter(data, key) do
    data
    |> Map.get(key)
    |> then(&put_in(data[key], "\"#{&1}\""))
  end

  @spec convert_time(map()) :: map()
  defp convert_time(consulting_hour) do
    consulting_hour
    |> Map.put(:inserted_at, Timex.format!(consulting_hour.inserted_at, "{YYYY}-{0M}-{0D}"))
    |> Map.put(:when, Timex.format!(consulting_hour.when, "{YYYY}-{0M}-{0D}"))
  end

  @doc """
  Return the count of consulting hours, using the same filter as list_consulting_hours
  """
  @spec count_consulting_hours(map()) :: integer
  def count_consulting_hours(args),
    do: Repo.count_filter(args, ConsultingHour, &filter_with/2, skip_organization_id: true)

  @spec filter_with(Ecto.Queryable.t(), %{optional(atom()) => any}) :: Ecto.Queryable.t()
  defp filter_with(query, filter) do
    query = Repo.filter_with(query, filter)
    # these filters are specific to consulting hours only.
    Enum.reduce(filter, query, fn
      {:organization_name, organization_name}, query ->
        from q in query, where: ilike(q.organization_name, ^"%#{organization_name}%")

      {:staff, staff}, query ->
        from q in query, where: ilike(q.staff, ^"%#{staff}%")

      {:is_billable, is_billable}, query ->
        from q in query, where: q.is_billable == ^is_billable

      {:participants, participants}, query ->
        from q in query, where: ilike(q.participants, ^"%#{participants}%")

      {:start_date, start_date}, query ->
        start_time = DateTime.new!(start_date, @beginning_of_day, "Etc/UTC")
        from q in query, where: q.when >= ^start_time

      {:end_date, end_date}, query ->
        end_time = DateTime.new!(end_date, @end_of_day, "Etc/UTC")
        from q in query, where: q.when <= ^end_time

      _, query ->
        query
    end)
  end

  @doc """
  Update the consulting_hour record
  """
  @spec update_consulting_hour(ConsultingHour.t(), map()) ::
          {:ok, ConsultingHour.t()} | {:error, Ecto.Changeset.t()}
  def update_consulting_hour(%ConsultingHour{} = consulting_hour, attrs) do
    consulting_hour
    |> changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Delete the consulting_hour record
  """
  @spec delete_consulting_hour(ConsultingHour.t()) ::
          {:ok, ConsultingHour.t()} | {:error, Ecto.Changeset.t()}
  def delete_consulting_hour(%ConsultingHour{} = consulting_hour) do
    Repo.delete(consulting_hour, skip_organization_id: true)
  end
end