lib/bexio_api_client/others.ex

defmodule BexioApiClient.Others do
  @moduledoc """
  Bexio API for the other endpoints.
  """

  import BexioApiClient.Helpers
  alias BexioApiClient.SearchCriteria

  alias BexioApiClient.Others.{
    CompanyProfile,
    Country,
    Language,
    Permission,
    Todo,
    User,
    FictionalUser
  }

  alias BexioApiClient.GlobalArguments
  import BexioApiClient.GlobalArguments, only: [opts_to_query: 1]

  @type tesla_error_type :: BexioApiClient.Helpers.tesla_error_type()

  @doc """
  Fetch a list of company profiles.
  """
  @spec fetch_company_profiles(client :: Tesla.Client.t()) ::
          {:ok, [CompanyProfile.t()]} | tesla_error_type()
  def fetch_company_profiles(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/company_profile")
      end,
      &map_from_company_profiles/2
    )
  end

  @doc """
  Fetch a single company profile
  """
  @spec fetch_company_profile(client :: Tesla.Client.t(), id :: non_neg_integer()) ::
          {:ok, CompanyProfile.t()} | tesla_error_type()
  def fetch_company_profile(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/company_profile/#{id}")
      end,
      &map_from_company_profile/2
    )
  end

  defp map_from_company_profiles(company_profiles, _env),
    do: Enum.map(company_profiles, &map_from_company_profile/1)

  defp map_from_company_profile(
         %{
           "id" => id,
           "name" => name,
           "address" => address,
           "address_nr" => address_nr,
           "postcode" => postcode,
           "city" => city,
           "country_id" => country_id,
           "legal_form" => legal_form,
           "country_name" => country_name,
           "mail" => mail,
           "phone_fixed" => phone_fixed,
           "phone_mobile" => phone_mobile,
           "fax" => fax,
           "url" => url,
           "skype_name" => skype_name,
           "facebook_name" => facebook_name,
           "twitter_name" => twitter_name,
           "description" => description,
           "ust_id_nr" => ust_id_nr,
           "mwst_nr" => mwst_nr,
           "trade_register_nr" => trade_register_nr,
           "has_own_logo" => own_logo?,
           "is_public_profile" => public_profile?,
           "is_logo_public" => logo_public?,
           "is_address_public" => address_public?,
           "is_phone_public" => phone_public?,
           "is_mobile_public" => mobile_public?,
           "is_fax_public" => fax_public?,
           "is_mail_public" => mail_public?,
           "is_url_public" => url_public?,
           "is_skype_public" => skype_public?,
           "logo_base64" => logo_base64
         },
         _env \\ nil
       ) do
    %CompanyProfile{
      id: id,
      name: name,
      address: address,
      address_nr: address_nr,
      postcode: postcode,
      city: city,
      country_id: country_id,
      legal_form: String.to_atom(legal_form),
      country_name: country_name,
      mail: mail,
      phone_fixed: phone_fixed,
      phone_mobile: phone_mobile,
      fax: fax,
      url: url,
      skype_name: skype_name,
      facebook_name: facebook_name,
      twitter_name: twitter_name,
      description: description,
      ust_id_nr: ust_id_nr,
      mwst_nr: mwst_nr,
      trade_register_nr: trade_register_nr,
      own_logo?: own_logo?,
      public_profile?: public_profile?,
      logo_public?: logo_public?,
      address_public?: address_public?,
      phone_public?: phone_public?,
      mobile_public?: mobile_public?,
      fax_public?: fax_public?,
      mail_public?: mail_public?,
      url_public?: url_public?,
      skype_public?: skype_public?,
      logo_base64: logo_base64
    }
  end

  @doc """
  Fetch a list of countries.
  """
  @spec fetch_countries(client :: Tesla.Client.t()) ::
          {:ok, [Country.t()]} | tesla_error_type()
  def fetch_countries(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/country")
      end,
      &map_from_countries/2
    )
  end

  @doc """
  Search countries via query.
  The following search fields are supported:

  * name
  * name_short
  """
  @spec search_countries(
          client :: Tesla.Client.t(),
          criteria :: list(SearchCriteria.t()),
          opts :: [GlobalArguments.offset_arg()]
        ) :: {:ok, [Country.t()]} | tesla_error_type()
  def search_countries(
        client,
        criteria,
        opts \\ []
      ) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/country/search", criteria, query: opts_to_query(opts))
      end,
      &map_from_countries/2
    )
  end

  @doc """
  Fetch a single country
  """
  @spec fetch_country(client :: Tesla.Client.t(), id :: non_neg_integer()) ::
          {:ok, Country.t()} | tesla_error_type()
  def fetch_country(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/country/#{id}")
      end,
      &map_from_country/2
    )
  end

  defp map_from_countries(countries, _env), do: Enum.map(countries, &map_from_country/1)

  defp map_from_country(
         %{
           "id" => id,
           "name" => name,
           "name_short" => name_short,
           "iso_3166_alpha2" => iso_3166_alpha2
         },
         _env \\ nil
       ) do
    %Country{
      id: id,
      name: name,
      name_short: name_short,
      iso_3166_alpha2: iso_3166_alpha2
    }
  end

  @doc """
  Fetch a list of languages.
  """
  @spec fetch_languages(client :: Tesla.Client.t()) ::
          {:ok, [Language.t()]} | tesla_error_type()
  def fetch_languages(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/language")
      end,
      &map_from_languages/2
    )
  end

  @doc """
  Search languages via query.
  The following search fields are supported:

  * name
  * iso_639_1
  """
  @spec search_languages(
          client :: Tesla.Client.t(),
          criteria :: list(SearchCriteria.t()),
          opts :: [GlobalArguments.offset_arg()]
        ) :: {:ok, [Language.t()]} | tesla_error_type()
  def search_languages(
        client,
        criteria,
        opts \\ []
      ) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/language/search", criteria, query: opts_to_query(opts))
      end,
      &map_from_languages/2
    )
  end

  defp map_from_languages(languages, _env), do: Enum.map(languages, &map_from_language/1)

  defp map_from_language(
         %{
           "id" => id,
           "name" => name,
           "decimal_point" => decimal_point,
           "thousands_separator" => thousands_separator,
           "date_format_id" => date_format_id_bexio,
           "date_format" => date_format,
           "iso_639_1" => iso_639_1
         },
         _env \\ nil
       ) do
    %Language{
      id: id,
      name: name,
      decimal_point: decimal_point,
      thousands_separator: thousands_separator,
      date_format: date_format,
      date_format_id: date_format_id(date_format_id_bexio),
      iso_639_1: iso_639_1
    }
  end

  defp date_format_id(1), do: :dmy
  defp date_format_id(2), do: :mdy

  @doc """
  Fetch a list of users.
  """
  @spec fetch_users(client :: Tesla.Client.t()) ::
          {:ok, [User.t()]} | tesla_error_type()
  def fetch_users(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/3.0/users")
      end,
      &map_from_users/2
    )
  end

  @doc """
  Fetch a single user.
  """
  @spec fetch_user(client :: Tesla.Client.t(), user_id :: non_neg_integer()) ::
          {:ok, User.t()} | tesla_error_type()
  def fetch_user(client, user_id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/3.0/users/#{user_id}")
      end,
      &map_from_user/2
    )
  end

  @doc """
  Fetch a list of finctional users.
  """
  @spec fetch_fictional_users(client :: Tesla.Client.t()) ::
          {:ok, [FictionalUser.t()]} | tesla_error_type()
  def fetch_fictional_users(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/3.0/fictional_users")
      end,
      &map_from_fictional_users/2
    )
  end

  @doc """
  Fetch a finctional user.
  """
  @spec fetch_fictional_user(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, FictionalUser.t()} | tesla_error_type()
  def fetch_fictional_user(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/3.0/fictional_users/#{id}")
      end,
      &map_from_fictional_user/2
    )
  end

  @doc """
  Create a fictional user, the id of the fictional user will be ignored!
  """
  @spec create_fictional_user(client :: Tesla.Client.t(), finctional_user :: FictionalUser.t()) ::
          {:ok, FictionalUser.t()} | tesla_error_type()
  def create_fictional_user(client, fictional_user) do
    bexio_body_handling(
      fn ->
        Tesla.post(
          client,
          "/3.0/fictional_users",
          Map.take(fictional_user, [:salutation_type, :firstname, :lastname, :email, :title_id])
        )
      end,
      &map_from_fictional_user/2
    )
  end

  @doc """
  Create a fictional user
  """
  @spec update_fictional_user(client :: Tesla.Client.t(), fictional_user :: FictionalUser.t()) ::
          {:ok, FictionalUser.t()} | tesla_error_type()
  def update_fictional_user(client, fictional_user) do
    bexio_body_handling(
      fn ->
        Tesla.patch(
          client,
          "/3.0/fictional_users/#{fictional_user.id}",
          Map.take(fictional_user, [:salutation_type, :firstname, :lastname, :email, :title_id])
        )
      end,
      &map_from_fictional_user/2
    )
  end

  @doc """
  Create a fictional user
  """
  @spec delete_fictional_user(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, true | false} | tesla_error_type()
  def delete_fictional_user(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.delete(client, "/3.0/fictional_users/#{id}")
      end,
      &success_response/2
    )
  end

  defp map_from_users(users, _env), do: Enum.map(users, &map_from_user/1)

  defp map_from_user(
         %{
           "id" => id,
           "salutation_type" => salutation_type,
           "firstname" => firstname,
           "lastname" => lastname,
           "email" => email,
           "is_superadmin" => superadmin?,
           "is_accountant" => accountant?
         },
         _env \\ nil
       ) do
    %User{
      id: id,
      salutation_type: String.to_atom(salutation_type),
      firstname: firstname,
      lastname: lastname,
      email: email,
      superadmin?: superadmin?,
      accountant?: accountant?
    }
  end

  defp map_from_fictional_users(fictional_users, _env),
    do: Enum.map(fictional_users, &map_from_fictional_user/1)

  defp map_from_fictional_user(
         %{
           "id" => id,
           "salutation_type" => salutation_type,
           "firstname" => firstname,
           "lastname" => lastname,
           "email" => email,
           "title_id" => title_id
         },
         _env \\ nil
       ) do
    %FictionalUser{
      id: id,
      salutation_type: String.to_atom(salutation_type),
      firstname: firstname,
      lastname: lastname,
      email: email,
      title_id: title_id
    }
  end

  @doc """
  Get access information of logged in user
  """
  @spec get_access_information(client :: Tesla.Client.t()) ::
          {:ok, Permission.t()} | tesla_error_type()
  def get_access_information(client) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/3.0/permissions")
      end,
      &map_from_permission_response/2
    )
  end

  defp map_from_permission_response(
         %{"components" => components, "permissions" => permissions},
         _env
       ) do
    %Permission{
      components: components,
      permissions: permissions |> Enum.map(&map_permission/1) |> Enum.into(%{})
    }
  end

  defp map_permission({k, properties}) do
    map_part(String.to_atom(k), properties, %{})
  end

  defp map_part(k, %{"activation" => activation} = map, acc) do
    map_part(k, Map.delete(map, "activation"), Map.put(acc, :activation, enabled(activation)))
  end

  defp map_part(k, %{"edit" => edit} = map, acc) do
    map_part(k, Map.delete(map, "edit"), Map.put(acc, :edit, restriction(edit)))
  end

  defp map_part(k, %{"view" => view} = map, acc) do
    map_part(k, Map.delete(map, "view"), Map.put(acc, :view, restriction(view)))
  end

  defp map_part(k, %{"show" => show} = map, acc) do
    map_part(k, Map.delete(map, "show"), Map.put(acc, :show, restriction(show)))
  end

  defp map_part(k, %{}, acc), do: {k, acc}

  def enabled("disabled"), do: :disabled
  def enabled("enabled"), do: :enabled

  def restriction("none"), do: :none
  def restriction("all"), do: :all
  def restriction("own"), do: :own

  @doc """
  Fetch a list of tasks.
  """
  @spec fetch_tasks(client :: Tesla.Client.t(), opts :: [GlobalArguments.offset_arg()]) ::
          {:ok, [Task.t()]} | tesla_error_type()
  def fetch_tasks(client, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/task", query: opts_to_query(opts))
      end,
      &map_from_tasks/2
    )
  end

  @doc """
  Search a task

  Following fields are supported:

  * `subject`
  * `updated_at`
  * `user_id`
  * `contact_id`
  * `todo_status_id`
  * `module_id`
  * `entry_id`
  """
  @spec search_tasks(
          client :: Tesla.Client.t(),
          criteria :: list(SearchCriteria.t()),
          opts :: [GlobalArguments.offset_arg()]
        ) :: {:ok, [Task.t()]} | tesla_error_type()
  def search_tasks(client, criteria, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/task/search", criteria, query: opts_to_query(opts))
      end,
      &map_from_tasks/2
    )
  end

  @doc """
  Fetch a  task.
  """
  @spec fetch_task(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, Task.t()} | tesla_error_type()
  def fetch_task(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/task/#{id}")
      end,
      &map_from_task/2
    )
  end

  @doc """
  Create a  task.
  """
  @spec create_task(client :: Tesla.Client.t(), task :: Task.t()) ::
          {:ok, Task.t()} | tesla_error_type()
  def create_task(client, task) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/task", remap_task(task))
      end,
      &map_from_task/2
    )
  end

  @doc """
  Edut a  task.
  """
  @spec edit_task(client :: Tesla.Client.t(), task :: Task.t()) ::
          {:ok, Task.t()} | tesla_error_type()
  def edit_task(client, task) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/task/#{task.id}", remap_task(task))
      end,
      &map_from_task/2
    )
  end

  @doc """
  Delete a  task.
  """
  @spec delete_task(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, Task.t()} | tesla_error_type()
  def delete_task(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.delete(client, "/2.0/task/#{id}")
      end,
      &success_response/2
    )
  end

  defp remap_task(task) do
    task
    |> Map.take([
      :user_id,
      :subject,
      :info,
      :contact_id,
      :sub_contact_id,
      :entry_id,
      :module_id,
      :todo_status_id,
      :todo_priority_id,
      :remember_type_id,
      :remember_time_id,
      :communication_kind_id
    ])
    |> Map.put(:finish_date, to_iso8601(Map.get(task, :finish_date)))
    |> Map.put(:have_remember, Map.get(task, :reminder?))
    |> Map.put(:pr_project_id, Map.get(task, :project_id))
  end

  defp map_from_tasks(tasks, _env), do: Enum.map(tasks, &map_from_task/1)

  defp map_from_task(
         %{
           "id" => id,
           "user_id" => user_id,
           "finish_date" => finish_date,
           "subject" => subject,
           "place" => place,
           "info" => info,
           "contact_id" => contact_id,
           "sub_contact_id" => sub_contact_id,
           "project_id" => project_id,
           "entry_id" => entry_id,
           "module_id" => module_id,
           "todo_status_id" => todo_status_id,
           "todo_priority_id" => todo_priority_id,
           "has_reminder" => reminder?,
           "remember_type_id" => remember_type_id,
           "remember_time_id" => remember_time_id,
           "communication_kind_id" => communication_kind_id
         },
         _env \\ nil
       ) do
    %Todo{
      id: id,
      user_id: user_id,
      finish_date: to_datetime(finish_date),
      subject: subject,
      place: place,
      info: info,
      contact_id: contact_id,
      sub_contact_id: sub_contact_id,
      project_id: project_id,
      entry_id: entry_id,
      module_id: module_id,
      todo_status_id: todo_status_id,
      todo_priority_id: todo_priority_id,
      reminder?: to_boolean(reminder?),
      remember_time_id: remember_time_id,
      remember_type_id: remember_type_id,
      communication_kind_id: communication_kind_id
    }
  end

  defp to_boolean(b) when is_boolean(b), do: b
  defp to_boolean("true"), do: true
  defp to_boolean("false"), do: false

  @doc """
  Fetch a list of task priorities.
  """
  @spec fetch_task_priorities(client :: Tesla.Client.t(), opts :: [GlobalArguments.offset_arg()]) ::
          {:ok, map()} | tesla_error_type()
  def fetch_task_priorities(client, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/todo_priority", query: opts_to_query(opts))
      end,
      &body_to_map/2
    )
  end

  @doc """
  Fetch a list of task status.
  """
  @spec fetch_task_status(client :: Tesla.Client.t(), opts :: [GlobalArguments.offset_arg()]) ::
          {:ok, map()} | tesla_error_type()
  def fetch_task_status(client, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/todo_status", query: opts_to_query(opts))
      end,
      &body_to_map/2
    )
  end

  @doc """
  Fetch a list of units.
  """
  @spec fetch_units(
          client :: Tesla.Client.t(),
          opts :: [GlobalArguments.offset_without_order_by_arg()]
        ) :: {:ok, map()} | tesla_error_type()
  def fetch_units(client, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/unit", query: opts_to_query(opts))
      end,
      &body_to_map/2
    )
  end

  @doc """
  Search a unit

  Following fields are supported:

  * `name`
  """
  @spec search_units(
          client :: Tesla.Client.t(),
          criteria :: list(SearchCriteria.t()),
          opts :: [GlobalArguments.offset_arg()]
        ) :: {:ok, map()} | tesla_error_type()
  def search_units(client, criteria, opts \\ []) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/unit/search", criteria, query: opts_to_query(opts))
      end,
      &body_to_map/2
    )
  end

  @doc """
  Fetch a unit.
  """
  @spec fetch_unit(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, %{id: integer(), name: String.t()}} | tesla_error_type()
  def fetch_unit(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.get(client, "/2.0/unit/#{id}")
      end,
      &id_name/2
    )
  end

  @doc """
  Create a unit.
  """
  @spec create_unit(client :: Tesla.Client.t(), name :: String.t()) ::
          {:ok, %{id: integer(), name: String.t()}} | tesla_error_type()
  def create_unit(client, name) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/unit", %{name: name})
      end,
      &id_name/2
    )
  end

  @doc """
  Edit a unit.
  """
  @spec edit_unit(client :: Tesla.Client.t(), id :: integer(), name :: String.t()) ::
          {:ok, %{id: integer(), name: String.t()}} | tesla_error_type()
  def edit_unit(client, id, name) do
    bexio_body_handling(
      fn ->
        Tesla.post(client, "/2.0/unit/#{id}", %{name: name})
      end,
      &id_name/2
    )
  end

  @doc """
  Delete a unit.
  """
  @spec delete_unit(client :: Tesla.Client.t(), id :: integer()) ::
          {:ok, Task.t()} | tesla_error_type()
  def delete_unit(client, id) do
    bexio_body_handling(
      fn ->
        Tesla.delete(client, "/2.0/unit/#{id}")
      end,
      &success_response/2
    )
  end
end