lib/glific/partners/organization.ex

defmodule Glific.Partners.Organization do
  @moduledoc """
  Organizations are the group of users who will access the system
  """
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query, warn: false

  alias __MODULE__

  alias Glific.{
    Contacts.Contact,
    Enums.OrganizationStatus,
    Partners.OrganizationSettings.OutOfOffice,
    Partners.OrganizationSettings.RegxFlow,
    Partners.Provider,
    Repo,
    Settings.Language
  }

  # define all the required fields for organization
  @required_fields [
    :name,
    :shortcode,
    :email,
    :bsp_id,
    :default_language_id
  ]

  # define all the optional fields for organization
  @optional_fields [
    :contact_id,
    :is_active,
    :is_approved,
    :status,
    :timezone,
    :active_language_ids,
    :session_limit,
    :organization_id,
    :signature_phrase,
    :last_communication_at,
    :fields,
    :newcontact_flow_id,
    :is_suspended,
    :suspended_until
  ]

  @type t() :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          id: non_neg_integer | nil,
          name: String.t() | nil,
          shortcode: String.t() | nil,
          email: String.t() | nil,
          bsp_id: non_neg_integer | nil,
          bsp: Provider.t() | Ecto.Association.NotLoaded.t() | nil,
          services: map(),
          root_user: map() | nil,
          contact_id: non_neg_integer | nil,
          contact: Contact.t() | Ecto.Association.NotLoaded.t() | nil,
          default_language_id: non_neg_integer | nil,
          default_language: Language.t() | Ecto.Association.NotLoaded.t() | nil,
          out_of_office: OutOfOffice.t() | nil,
          regx_flow: RegxFlow.t() | nil,
          newcontact_flow_id: non_neg_integer | nil,
          hours: list() | nil,
          days: list() | nil,
          is_active: boolean() | true,
          is_approved: boolean() | false,
          status: String.t() | nil,
          timezone: String.t() | nil,
          active_language_ids: [integer] | [],
          languages: [Language.t()] | nil,
          session_limit: non_neg_integer | nil,
          organization_id: non_neg_integer | nil,
          signature_phrase: binary | nil,
          last_communication_at: :utc_datetime | nil,
          inserted_at: :utc_datetime | nil,
          updated_at: :utc_datetime | nil,
          fields: map() | nil,
          is_suspended: boolean() | false,
          suspended_until: DateTime.t() | nil
        }

  schema "organizations" do
    field(:name, :string)
    field(:shortcode, :string)

    field(:email, :string)

    # we'll cache all the services here
    field(:services, :map, virtual: true, default: %{})

    # we'll cache the root user of the org here, this gives
    # us a permission object for calls from gupshup and
    # flow editor
    field(:root_user, :map, virtual: true)

    # lets cache the start/end hours in here
    # to make it easier on the flows
    field(:hours, {:array, :time}, virtual: true)
    field(:days, {:array, :integer}, virtual: true)

    belongs_to(:bsp, Provider, foreign_key: :bsp_id)
    belongs_to(:contact, Contact)
    belongs_to(:default_language, Language)

    embeds_one(:out_of_office, OutOfOffice, on_replace: :update)

    embeds_one(:regx_flow, RegxFlow, on_replace: :update)

    # id of flow which gets triggered when new contact joins bot
    field(:newcontact_flow_id, :integer)
    field(:is_active, :boolean, default: true)
    field(:is_approved, :boolean, default: false)

    field(:status, OrganizationStatus)

    field(:timezone, :string, default: "Asia/Kolkata")

    field(:active_language_ids, {:array, :integer}, default: [])

    # new version of ecto was giving us an error if we set the inner_type ot Language
    field(:languages, {:array, :any}, virtual: true)

    field(:session_limit, :integer, default: 60)

    # this is just to make our friends in org id enforcer happy and to keep the code clean
    field(:organization_id, :integer)

    # webhook sign phrase, kept encrypted (soon)
    field(:signature_phrase, Glific.Encrypted.Binary)

    field(:last_communication_at, :utc_datetime)

    field(:fields, :map, default: %{})

    # lets add support for suspending orgs briefly
    field(:is_suspended, :boolean, default: false)
    field(:suspended_until, :utc_datetime)

    # 2085
    # Lets create a virtual field for now to conditionally enable
    # the display of node uuids. We need an NGO friendly way to do this globally
    field(:is_flow_uuid_display, :boolean, default: false, virtual: true)

    # virtual field for roles and permission
    field(:is_roles_and_permission, :boolean, default: false, virtual: true)

    # A virtual field for now to conditionally enable contact profile feature for an organization
    field(:is_contact_profile_enabled, :boolean, default: false, virtual: true)

    timestamps(type: :utc_datetime)
  end

  @doc """
  Standard changeset pattern we use for all data types
  """
  @spec changeset(Organization.t(), map()) :: Ecto.Changeset.t()
  def changeset(organization, attrs) do
    organization
    |> cast(attrs, @required_fields ++ @optional_fields)
    |> add_out_of_office_if_missing()
    |> cast_embed(:out_of_office, with: &OutOfOffice.out_of_office_changeset/2)
    |> cast_embed(:regx_flow, with: &RegxFlow.regx_flow_changeset/2)
    |> validate_required(@required_fields)
    |> validate_inclusion(:timezone, Tzdata.zone_list())
    |> validate_active_languages()
    |> validate_default_language()
    |> unique_constraint(:shortcode)
    |> unique_constraint(:contact_id)
  end

  @doc false
  @spec to_minimal_map(Organization.t()) :: map()
  def to_minimal_map(organization) do
    Map.take(organization, [:id | @required_fields ++ @optional_fields])
  end

  @spec validate_active_languages(Ecto.Changeset.t()) :: Ecto.Changeset.t()
  defp validate_active_languages(changeset) do
    language_ids =
      Language
      |> select([l], l.id)
      |> Repo.all()

    changeset
    |> validate_subset(:active_language_ids, language_ids)
  end

  @spec validate_default_language(Ecto.Changeset.t()) :: Ecto.Changeset.t()
  defp validate_default_language(changeset) do
    default_language_id = get_field(changeset, :default_language_id)
    active_language_ids = get_field(changeset, :active_language_ids)

    check_valid_language(changeset, default_language_id, active_language_ids)
  end

  @spec check_valid_language(Ecto.Changeset.t(), non_neg_integer(), [non_neg_integer()]) ::
          Ecto.Changeset.t()
  defp check_valid_language(changeset, nil, _), do: changeset
  defp check_valid_language(changeset, _, nil), do: changeset

  defp check_valid_language(changeset, default_language_id, active_language_ids) do
    if default_language_id in active_language_ids,
      do: changeset,
      else:
        add_error(
          changeset,
          :default_language_id,
          "default language must be updated according to active languages"
        )
  end

  @spec add_out_of_office_if_missing(Ecto.Changeset.t()) :: Ecto.Changeset.t()
  defp add_out_of_office_if_missing(
         %Ecto.Changeset{data: %Organization{out_of_office: nil}} = changeset
       ) do
    out_of_office_default_data = %{
      enabled: false,
      enabled_days: [
        %{enabled: false, id: 1},
        %{enabled: false, id: 2},
        %{enabled: false, id: 3},
        %{enabled: false, id: 4},
        %{enabled: false, id: 5},
        %{enabled: false, id: 6},
        %{enabled: false, id: 7}
      ]
    }

    changeset
    |> put_change(:out_of_office, out_of_office_default_data)
  end

  defp add_out_of_office_if_missing(changeset) do
    changeset
  end
end

defimpl FunWithFlags.Actor, for: Map do
  @moduledoc false

  @doc """
  All users are organization actors for now. At some point, we might make
  organization a group and isolate specific users

  Implementation of id for the map protocol
  """
  @spec id(map()) :: String.t()
  def id(%{organization_id: organization_id}) do
    "org:#{organization_id}"
  end
end