File size: 4,509 Bytes
936b397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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