defmodule Plausible.Funnels do @moduledoc """ This module implements contextual Funnel interface, allowing listing, creating and deleting funnel definitions. For brief explanation of what a Funnel is, please see `Plausible.Funnel` schema. See `Plausible.Stats.Funnel` for the evaluation logic. """ use Plausible.Funnel alias Plausible.Repo import Ecto.Query @spec create(Plausible.Site.t(), String.t(), [map()], Keyword.t()) :: {:ok, Funnel.t()} | {:error, Ecto.Changeset.t() | :invalid_funnel_size | :upgrade_required} def create(site, name, steps, opts \\ []) def create(site, name, steps, opts) when is_list(steps) and length(steps) in Funnel.min_steps()..Funnel.max_steps() do site = Plausible.Repo.preload(site, :team) case Plausible.Billing.Feature.Funnels.check_availability(site.team) do {:error, _} = error -> error :ok -> site |> create_changeset(name, steps, opts) |> Repo.insert() end end def create(_site, _name, _goals, _opts) do {:error, :invalid_funnel_size} end @spec update(Funnel.t(), String.t(), [map()], Keyword.t()) :: {:ok, Funnel.t()} | {:error, Ecto.Changeset.t() | :invalid_funnel_size | :upgrade_required} def update(funnel, name, steps, opts \\ []) do site = Plausible.Repo.preload(funnel, site: :team).site case Plausible.Billing.Feature.Funnels.check_availability(site.team) do {:error, _} = error -> error :ok -> funnel |> edit_changeset(name, steps, strict_order?: Keyword.get(opts, :strict_order?, !!funnel.strict_order) ) |> Repo.update() end end @spec create_changeset(Plausible.Site.t(), String.t(), [map()], Keyword.t()) :: Ecto.Changeset.t() def create_changeset(site, name, steps, opts \\ []) do Funnel.changeset(%Funnel{site_id: site.id}, %{ name: name, steps: steps, strict_order: Keyword.get(opts, :strict_order?, false) }) end @spec edit_changeset(Plausible.Funnel.t(), String.t(), [map()], Keyword.t()) :: Ecto.Changeset.t() def edit_changeset(funnel, name, steps, opts) do Funnel.changeset(funnel, %{ name: name, steps: steps, strict_order: Keyword.get(opts, :strict_order?, false) }) end @spec ephemeral_definition(Plausible.Site.t(), String.t(), [map()], Keyword.t()) :: Funnel.t() def ephemeral_definition(site, name, steps, opts \\ []) do site |> create_changeset(name, steps, opts) |> Ecto.Changeset.apply_changes() end @spec list(Plausible.Site.t()) :: [ %{name: String.t(), id: pos_integer(), steps_count: pos_integer()} ] def list(%Plausible.Site{} = site) do q = from(f in Funnel, inner_join: steps in assoc(f, :steps), where: f.site_id == ^site.id, group_by: f.id, order_by: [desc: :id], select: %{name: f.name, id: f.id, steps_count: count(steps)} ) Repo.all(q) end @spec delete(Plausible.Site.t() | pos_integer(), pos_integer()) :: :ok def delete(%Plausible.Site{id: site_id}, funnel_id) do delete(site_id, funnel_id) end def delete(site_id, funnel_id) do Repo.delete_all( from(f in Funnel, where: f.site_id == ^site_id, where: f.id == ^funnel_id ) ) :ok end @spec get(Plausible.Site.t() | pos_integer(), pos_integer() | String.t()) :: Funnel.t() | nil def get(%Plausible.Site{id: site_id}, by) do get(site_id, by) end def get(site_id, funnel_name) when is_integer(site_id) and is_binary(funnel_name) do site_id |> base_get_query() |> where([f], f.name == ^funnel_name) |> Repo.one() end def get(site_id, funnel_id) when is_integer(site_id) and is_integer(funnel_id) do site_id |> base_get_query() |> where([f], f.id == ^funnel_id) |> Repo.one() end @spec with_goals_query(Plausible.Site.t()) :: Ecto.Query.t() def with_goals_query(site) do from(f in Funnel, inner_join: steps in assoc(f, :steps), where: f.site_id == ^site.id, group_by: f.id, order_by: [desc: :id], preload: [steps: :goal] ) end defp base_get_query(site_id) do from(f in Funnel, where: f.site_id == ^site_id, inner_join: steps in assoc(f, :steps), inner_join: goal in assoc(steps, :goal), order_by: steps.step_order, preload: [ steps: {steps, goal: goal} ] ) end end