diff --git a/config/test.exs b/config/test.exs index b9e44e5d6a81..883ee037aae3 100644 --- a/config/test.exs +++ b/config/test.exs @@ -17,7 +17,7 @@ config :plausible, Plausible.ClickhouseRepo, config :plausible, Plausible.Mailer, adapter: Bamboo.TestAdapter config :plausible, - paddle_api: Plausible.PaddleApi.Mock, + paddle_api: Plausible.Billing.TestPaddleApiMock, google_api: Plausible.Google.API.Mock config :bamboo, :refute_timeout, 10 diff --git a/lib/plausible/billing/plan.ex b/lib/plausible/billing/plan.ex index 132a7c585d33..45e8acd18064 100644 --- a/lib/plausible/billing/plan.ex +++ b/lib/plausible/billing/plan.ex @@ -16,7 +16,7 @@ defmodule Plausible.Billing.Plan do # production plans, contain multiple generations of plans in the same file # for testing purposes. field :generation, :integer - field :kind, Ecto.Enum, values: [:growth, :business] + field :kind, Ecto.Enum, values: [:starter, :growth, :business] field :features, Plausible.Billing.Ecto.FeatureList field :monthly_pageview_limit, :integer diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex index d04785b81817..a6affd5cd458 100644 --- a/lib/plausible/billing/plans.ex +++ b/lib/plausible/billing/plans.ex @@ -4,7 +4,7 @@ defmodule Plausible.Billing.Plans do alias Plausible.Billing.{Subscription, Plan, EnterprisePlan} alias Plausible.Teams - @generations [:legacy_plans, :plans_v1, :plans_v2, :plans_v3, :plans_v4] + @generations [:legacy_plans, :plans_v1, :plans_v2, :plans_v3, :plans_v4, :plans_v5] for group <- Enum.flat_map(@generations, &[&1, :"sandbox_#{&1}"]) do path = Application.app_dir(:plausible, ["priv", "#{group}.json"]) @@ -32,41 +32,62 @@ defmodule Plausible.Billing.Plans do end end - @spec growth_plans_for(Subscription.t()) :: [Plan.t()] + defp starter_plans_for(legacy?) do + if legacy? do + [] + else + Enum.filter(plans_v5(), &(&1.kind == :starter)) + end + end + + @spec growth_plans_for(Subscription.t(), boolean()) :: [Plan.t()] @doc """ Returns a list of growth plans available for the subscription to choose. As new versions of plans are introduced, subscriptions which were on old plans can still choose from old plans. """ - def growth_plans_for(subscription) do + def growth_plans_for(subscription, legacy? \\ false) do owned_plan = get_regular_plan(subscription) + default_plans = if legacy?, do: plans_v4(), else: plans_v5() + cond do - is_nil(owned_plan) -> plans_v4() - subscription && Subscriptions.expired?(subscription) -> plans_v4() - owned_plan.kind == :business -> plans_v4() + is_nil(owned_plan) -> default_plans + subscription && Subscriptions.expired?(subscription) -> default_plans + owned_plan.kind == :business -> default_plans owned_plan.generation == 1 -> plans_v1() |> drop_high_plans(owned_plan) owned_plan.generation == 2 -> plans_v2() |> drop_high_plans(owned_plan) owned_plan.generation == 3 -> plans_v3() owned_plan.generation == 4 -> plans_v4() + owned_plan.generation == 5 -> plans_v5() end |> Enum.filter(&(&1.kind == :growth)) end - def business_plans_for(subscription) do + def business_plans_for(subscription, legacy? \\ false) do owned_plan = get_regular_plan(subscription) + default_plans = if legacy?, do: plans_v4(), else: plans_v5() + cond do - subscription && Subscriptions.expired?(subscription) -> plans_v4() + subscription && Subscriptions.expired?(subscription) -> default_plans owned_plan && owned_plan.generation < 4 -> plans_v3() - true -> plans_v4() + owned_plan && owned_plan.generation < 5 -> plans_v4() + true -> default_plans end |> Enum.filter(&(&1.kind == :business)) end def available_plans_for(subscription, opts \\ []) do - plans = growth_plans_for(subscription) ++ business_plans_for(subscription) + legacy? = Keyword.get(opts, :legacy?, false) + + plans = + Enum.concat([ + starter_plans_for(legacy?), + growth_plans_for(subscription, legacy?), + business_plans_for(subscription, legacy?) + ]) plans = if Keyword.get(opts, :with_prices) do @@ -192,42 +213,31 @@ defmodule Plausible.Billing.Plans do end @doc """ - Returns the most appropriate plan for a team based on its usage during a - given cycle. + Returns the most appropriate monthly pageview volume for a given usage cycle. + The cycle is either last 30 days (for trials) or last billing cycle for teams + with an existing subscription. - If the usage during the cycle exceeds the enterprise-level threshold, or if - the team already has an enterprise plan, it suggests the :enterprise - plan. + The generation and tier from which we're searching for a suitable volume doesn't + matter - the monthly pageview volumes for all plans starting from v3 are going from + 10k to 10M. This function uses v4 Growth but it might as well be e.g. v5 Business. - Otherwise, it recommends the plan where the cycle usage falls just under the - plan's limit from the available options for the team. + If the usage during the cycle exceeds the enterprise-level threshold, or if + the team already has an enterprise plan, it returns `:enterprise`. Otherwise, + a string representing the volume, e.g. "100k" or "5M". """ - @enterprise_level_usage 10_000_000 - @spec suggest(Teams.Team.t(), non_neg_integer()) :: Plan.t() - def suggest(team, usage_during_cycle) do - cond do - usage_during_cycle > @enterprise_level_usage -> - :enterprise - - Teams.Billing.enterprise_configured?(team) -> - :enterprise - - true -> - subscription = Teams.Billing.get_subscription(team) - suggest_by_usage(subscription, usage_during_cycle) + @spec suggest_volume(Teams.Team.t(), non_neg_integer()) :: String.t() | :enterprise + def suggest_volume(team, usage_during_cycle) do + if Teams.Billing.enterprise_configured?(team) do + :enterprise + else + plans_v4() + |> Enum.filter(&(&1.kind == :growth)) + |> Enum.find(%{volume: :enterprise}, &(usage_during_cycle < &1.monthly_pageview_limit)) + |> Map.get(:volume) end end - def suggest_by_usage(subscription, usage_during_cycle) do - available_plans = - if business_tier?(subscription), - do: business_plans_for(subscription), - else: growth_plans_for(subscription) - - Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit)) - end - def all() do - legacy_plans() ++ plans_v1() ++ plans_v2() ++ plans_v3() ++ plans_v4() + legacy_plans() ++ plans_v1() ++ plans_v2() ++ plans_v3() ++ plans_v4() ++ plans_v5() end end diff --git a/lib/plausible/billing/qouta/quota.ex b/lib/plausible/billing/qouta/quota.ex index 7b7757d948dc..59e8ad42c6dd 100644 --- a/lib/plausible/billing/qouta/quota.ex +++ b/lib/plausible/billing/qouta/quota.ex @@ -57,12 +57,35 @@ defmodule Plausible.Billing.Quota do `:custom` is returned. This means that this kind of usage should get on a custom plan. - To avoid confusion, we do not recommend Growth tiers for customers that - are already on a Business tier (even if their usage would fit Growth). + To avoid confusion, we do not recommend a lower tier for customers that + are already on a higher tier (even if their usage is low enough). `nil` is returned if the usage is not eligible for upgrade. """ - def suggest_tier(usage, highest_growth, highest_business, owned_tier) do + def suggest_tier(usage, highest_starter, highest_growth, highest_business, owned_tier) do + cond do + not eligible_for_upgrade?(usage) -> + nil + + usage_fits_plan?(usage, highest_starter) and owned_tier not in [:business, :growth] -> + :starter + + usage_fits_plan?(usage, highest_growth) and owned_tier != :business -> + :growth + + usage_fits_plan?(usage, highest_business) -> + :business + + true -> + :custom + end + end + + @doc """ + [DEPRECATED] Used in LegacyChoosePlan in order to suggest a tier + when `starter_tier` flag is not enabled. + """ + def legacy_suggest_tier(usage, highest_growth, highest_business, owned_tier) do cond do not eligible_for_upgrade?(usage) -> nil usage_fits_plan?(usage, highest_growth) and owned_tier != :business -> :growth diff --git a/lib/plausible/billing/site_locker.ex b/lib/plausible/billing/site_locker.ex index 611665d585e2..823371b6028f 100644 --- a/lib/plausible/billing/site_locker.ex +++ b/lib/plausible/billing/site_locker.ex @@ -55,11 +55,11 @@ defmodule Plausible.Billing.SiteLocker do defp send_grace_period_end_email(team, true) do team = Repo.preload(team, [:owners, :billing_members]) usage = Teams.Billing.monthly_pageview_usage(team) - suggested_plan = Plausible.Billing.Plans.suggest(team, usage.last_cycle.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.last_cycle.total) for recipient <- team.owners ++ team.billing_members do recipient - |> PlausibleWeb.Email.dashboard_locked(team, usage, suggested_plan) + |> PlausibleWeb.Email.dashboard_locked(team, usage, suggested_volume) |> Plausible.Mailer.send() end end diff --git a/lib/plausible_web/components/billing/legacy_plan_benefits.ex b/lib/plausible_web/components/billing/legacy_plan_benefits.ex new file mode 100644 index 000000000000..c793104d4658 --- /dev/null +++ b/lib/plausible_web/components/billing/legacy_plan_benefits.ex @@ -0,0 +1,141 @@ +defmodule PlausibleWeb.Components.Billing.LegacyPlanBenefits do + @moduledoc """ + [DEPRECATED] This file is essentially a copy of + `PlausibleWeb.Components.Billing.PlanBenefits` with the + intent of keeping the old behaviour in place for the users without + the `starter_tier` feature flag enabled. + """ + + use Phoenix.Component + alias Plausible.Billing.Plan + + attr :benefits, :list, required: true + attr :class, :string, default: nil + + @doc """ + This function takes a list of benefits returned by either one of: + + * `for_growth/1` + * `for_business/2` + * `for_enterprise/1`. + + and renders them as HTML. + + The benefits in the given list can be either strings or functions + returning a Phoenix component. This allows, for example, to render + links within the plan benefit text. + """ + def render(assigns) do + ~H""" +
+ Sites API access for + <.link + class="text-indigo-500 hover:text-indigo-400" + href="https://plausible.io/white-label-web-analytics" + > + reselling + +
+ """ + end +end diff --git a/lib/plausible_web/components/billing/legacy_plan_box.ex b/lib/plausible_web/components/billing/legacy_plan_box.ex new file mode 100644 index 000000000000..bf4507a06421 --- /dev/null +++ b/lib/plausible_web/components/billing/legacy_plan_box.ex @@ -0,0 +1,378 @@ +defmodule PlausibleWeb.Components.Billing.LegacyPlanBox do + @moduledoc """ + [DEPRECATED] This file is essentially a copy of + `PlausibleWeb.Components.Billing.PlanBox` with the + intent of keeping the old behaviour in place for the users without + the `starter_tier` feature flag enabled. + """ + + use PlausibleWeb, :component + + require Plausible.Billing.Subscription.Status + alias PlausibleWeb.Components.Billing.{PlanBenefits, Notice} + alias Plausible.Billing.{Plan, Quota, Subscription} + + def standard(assigns) do + highlight = + cond do + assigns.owned && assigns.recommended -> "Current" + assigns.recommended -> "Recommended" + true -> nil + end + + assigns = assign(assigns, :highlight, highlight) + + ~H""" ++ + Custom + +
+ + <.contact_button class="" /> ++ {@text} +
++ + Custom + +
+ + """ + end + + defp render_price_info(assigns) do + ~H""" ++ <.price_tag + kind={@kind} + selected_interval={@selected_interval} + plan_to_render={@plan_to_render} + /> +
++ VAT if applicable
+ """ + end + + defp price_tag(%{plan_to_render: %Plan{monthly_cost: nil}} = assigns) do + ~H""" + + N/A + + """ + end + + defp price_tag(%{selected_interval: :monthly} = assigns) do + ~H""" + + {@plan_to_render.monthly_cost |> Plausible.Billing.format_price()} + + + /month + + """ + end + + defp price_tag(%{selected_interval: :yearly} = assigns) do + ~H""" + + {@plan_to_render.monthly_cost |> Money.mult!(12) |> Plausible.Billing.format_price()} + + + {@plan_to_render.yearly_cost |> Plausible.Billing.format_price()} + + + /year + + """ + end + + defp checkout(assigns) do + paddle_product_id = get_paddle_product_id(assigns.plan_to_render, assigns.selected_interval) + change_plan_link_text = change_plan_link_text(assigns) + + subscription = + Plausible.Teams.Billing.get_subscription(assigns.current_team) + + billing_details_expired = + Subscription.Status.in?(subscription, [ + Subscription.Status.paused(), + Subscription.Status.past_due() + ]) + + subscription_deleted = Subscription.Status.deleted?(subscription) + usage_check = check_usage_within_plan_limits(assigns) + + {checkout_disabled, disabled_message} = + cond do + not Quota.eligible_for_upgrade?(assigns.usage) -> + {true, nil} + + change_plan_link_text == "Currently on this plan" && not subscription_deleted -> + {true, nil} + + usage_check != :ok -> + {true, "Your usage exceeds this plan"} + + billing_details_expired -> + {true, "Please update your billing details first"} + + true -> + {false, nil} + end + + exceeded_plan_limits = + case usage_check do + {:error, {:over_plan_limits, limits}} -> + limits + + _ -> + [] + end + + feature_usage_check = Quota.ensure_feature_access(assigns.usage, assigns.plan_to_render) + + assigns = + assigns + |> assign(:paddle_product_id, paddle_product_id) + |> assign(:change_plan_link_text, change_plan_link_text) + |> assign(:checkout_disabled, checkout_disabled) + |> assign(:disabled_message, disabled_message) + |> assign(:exceeded_plan_limits, exceeded_plan_limits) + |> assign(:confirm_message, losing_features_message(feature_usage_check)) + + ~H""" + <%= if @owned_plan && Plausible.Billing.Subscriptions.resumable?(@current_team.subscription) do %> + <.change_plan_link {assigns} /> + <% else %> +
+ {Phoenix.Naming.humanize(limit)}
+
Sites API access for <.link - class="text-indigo-500 hover:text-indigo-400" + class="text-indigo-500 hover:underline" href="https://plausible.io/white-label-web-analytics" > reselling diff --git a/lib/plausible_web/components/billing/plan_box.ex b/lib/plausible_web/components/billing/plan_box.ex index 62ee886f3cbf..3568459d111a 100644 --- a/lib/plausible_web/components/billing/plan_box.ex +++ b/lib/plausible_web/components/billing/plan_box.ex @@ -7,6 +7,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do alias PlausibleWeb.Components.Billing.{PlanBenefits, Notice} alias Plausible.Billing.{Plan, Quota, Subscription} + @plan_box_price_container_class "relative h-20 pt-4 max-h-20 whitespace-nowrap overflow-hidden" + def standard(assigns) do highlight = cond do @@ -15,13 +17,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do true -> nil end - assigns = assign(assigns, :highlight, highlight) + assigns = + assigns + |> assign(:highlight, highlight) + |> assign(:price_container_class, @plan_box_price_container_class) ~H"""
-
+
-
+
+
Custom
- <.price_tag
- kind={@kind}
- selected_interval={@selected_interval}
- plan_to_render={@plan_to_render}
- />
- + VAT if applicable
+
+ {@monthly_cost}
+
+
+ /month
+
+
- {if @owned_plan,
- do: "Change subscription plan",
- else: "Upgrade your account"}
- Back to Settings
+ {if @owned_plan,
+ do: "Change your subscription plan",
+ else: "Upgrade your trial to a paid plan"}
+
- <.render_usage pageview_usage={@usage.monthly_pageviews} />
-
+ <.render_usage pageview_usage={@usage.monthly_pageviews} />
+
+ You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly. If your pageviews exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point.
+
-
- What happens if I go over my page views limit?
-
-
+ {if @owned_plan,
+ do: "Change subscription plan",
+ else: "Upgrade your account"}
+
+ <.render_usage pageview_usage={@usage.monthly_pageviews} />
+
+
+ What happens if I go over my page views limit?
+
+
+ {render_slot(@inner_block)}
+
+ """
+ end
+
+ attr :id, :string, required: true
+ attr :title, :string, required: true
+ attr :open_by_default, :boolean, default: false
+ attr :title_class, :string, default: ""
+ slot :inner_block, required: true
+
+ def accordion_item(assigns) do
+ ~H"""
+
+ Traffic based plans that match your growth
+
+
-<%= if @suggested_plan == :enterprise do %>
+<%= if @suggested_volume == :enterprise do %>
Your usage exceeds our standard plans, so please reply back to this email for a tailored quote.
<% else %>
- "?__team=#{@team.identifier}"}>Click here to upgrade your subscription. We recommend you upgrade to the {@suggested_plan.volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
+ "?__team=#{@team.identifier}"}>Click here to upgrade your subscription. We recommend you upgrade to the {@suggested_volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
If your usage decreases in the future, you can switch to a lower plan at any time. Any credit balance will automatically apply to future payments.
<% end %>
diff --git a/lib/plausible_web/templates/email/over_limit.html.heex b/lib/plausible_web/templates/email/over_limit.html.heex
index f14fa65fed95..a3d7ff5ba4cd 100644
--- a/lib/plausible_web/templates/email/over_limit.html.heex
+++ b/lib/plausible_web/templates/email/over_limit.html.heex
@@ -10,10 +10,10 @@ During the last billing cycle ({PlausibleWeb.TextHelpers.format_date_range(
)}), your account used {PlausibleWeb.AuthView.delimit_integer(@usage.penultimate_cycle.total)} billable pageviews. Note that billable pageviews include both standard pageviews and custom events. In your
PlausibleWeb.Router.Helpers.settings_path(PlausibleWeb.Endpoint, :subscription) <> "?__team=#{@team.identifier}"}>account settings, you'll find an overview of your usage and limits.
-<%= if @suggested_plan == :enterprise do %>
+<%= if @suggested_volume == :enterprise do %>
Your usage exceeds our standard plans, so please reply back to this email for a tailored quote.
<% else %>
- "?__team=#{@team.identifier}"}>Click here to upgrade your subscription. We recommend you upgrade to the {@suggested_plan.volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
+ "?__team=#{@team.identifier}"}>Click here to upgrade your subscription. We recommend you upgrade to the {@suggested_volume}/mo plan. The new charge will be prorated to reflect the amount you have already paid and the time until your current subscription is supposed to expire.
If your usage decreases in the future, you can switch to a lower plan at any time. Any credit balance will automatically apply to future payments.
<% end %>
diff --git a/lib/plausible_web/templates/email/trial_upgrade_email.html.heex b/lib/plausible_web/templates/email/trial_upgrade_email.html.heex
index d7536a6a94f3..30be57e7ab70 100644
--- a/lib/plausible_web/templates/email/trial_upgrade_email.html.heex
+++ b/lib/plausible_web/templates/email/trial_upgrade_email.html.heex
@@ -6,10 +6,10 @@ In the last month, your account has used {PlausibleWeb.AuthView.delimit_integer(
" and custom events in total",
else:
""}.
-<%= if @suggested_plan == :enterprise do %>
+<%= if @suggested_volume == :enterprise do %>
This is more than our standard plans, so please reply back to this email to get a quote for your volume.
<% else %>
- Based on that we recommend you select a {@suggested_plan.volume}/mo plan.
+ Based on that we recommend you select a {@suggested_volume}/mo plan.
"?__team=#{@team.identifier}"}>
Upgrade now
diff --git a/lib/plausible_web/templates/layout/app.html.heex b/lib/plausible_web/templates/layout/app.html.heex
index 6aa35a7a12be..2c57983bb255 100644
--- a/lib/plausible_web/templates/layout/app.html.heex
+++ b/lib/plausible_web/templates/layout/app.html.heex
@@ -33,7 +33,7 @@
]}
style={if assigns[:background], do: "background-color: #{assigns[:background]}"}
>
- <%= if !assigns[:embedded] do %>
+ <%= if !assigns[:embedded] && !assigns[:hide_header?] do %>
{render("_header.html", assigns)}
<%= if !assigns[:disable_global_notices?] do %>
diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex
index 8092f9e42df2..c91431c9713b 100644
--- a/lib/workers/check_usage.ex
+++ b/lib/workers/check_usage.ex
@@ -108,11 +108,11 @@ defmodule Plausible.Workers.CheckUsage do
defp check_regular_subscriber(subscriber, usage_mod) do
case check_pageview_usage_two_cycles(subscriber, usage_mod) do
{:over_limit, pageview_usage} ->
- suggested_plan =
- Plausible.Billing.Plans.suggest(subscriber, pageview_usage.last_cycle.total)
+ suggested_volume =
+ Plausible.Billing.Plans.suggest_volume(subscriber, pageview_usage.last_cycle.total)
for owner <- subscriber.owners ++ subscriber.billing_members do
- PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_plan)
+ PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_volume)
|> Plausible.Mailer.send()
end
diff --git a/lib/workers/send_trial_notifications.ex b/lib/workers/send_trial_notifications.ex
index 4e72710b5f03..085c73e749dd 100644
--- a/lib/workers/send_trial_notifications.ex
+++ b/lib/workers/send_trial_notifications.ex
@@ -64,20 +64,20 @@ defmodule Plausible.Workers.SendTrialNotifications do
defp send_tomorrow_reminder(users, team) do
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
- suggested_plan = Plausible.Billing.Plans.suggest(team, usage.total)
+ suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
for user <- users do
- PlausibleWeb.Email.trial_upgrade_email(user, team, "tomorrow", usage, suggested_plan)
+ PlausibleWeb.Email.trial_upgrade_email(user, team, "tomorrow", usage, suggested_volume)
|> Plausible.Mailer.send()
end
end
defp send_today_reminder(users, team) do
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
- suggested_plan = Plausible.Billing.Plans.suggest(team, usage.total)
+ suggested_volume = Plausible.Billing.Plans.suggest_volume(team, usage.total)
for user <- users do
- PlausibleWeb.Email.trial_upgrade_email(user, team, "today", usage, suggested_plan)
+ PlausibleWeb.Email.trial_upgrade_email(user, team, "today", usage, suggested_volume)
|> Plausible.Mailer.send()
end
end
diff --git a/priv/plans_v5.json b/priv/plans_v5.json
index 04c0b3da7f53..afb9d05b6955 100644
--- a/priv/plans_v5.json
+++ b/priv/plans_v5.json
@@ -113,9 +113,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -129,9 +129,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -145,9 +145,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -161,9 +161,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -177,9 +177,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -193,9 +193,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -209,9 +209,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -225,9 +225,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -241,13 +241,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -261,13 +261,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -281,13 +281,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -301,13 +301,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -321,13 +321,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -341,13 +341,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -361,13 +361,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -381,13 +381,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
}
diff --git a/priv/sandbox_plans_v5.json b/priv/sandbox_plans_v5.json
index 4cfc02be1882..6c593de8f0a4 100644
--- a/priv/sandbox_plans_v5.json
+++ b/priv/sandbox_plans_v5.json
@@ -113,9 +113,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -129,9 +129,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -145,9 +145,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -161,9 +161,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -177,9 +177,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -193,9 +193,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -209,9 +209,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -225,9 +225,9 @@
"team_member_limit": 3,
"features": [
"goals",
- "site_segments",
"teams",
- "shared_links"
+ "shared_links",
+ "site_segments"
],
"data_retention_in_years": 3
},
@@ -241,13 +241,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -261,13 +261,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -281,13 +281,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -301,13 +301,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -321,13 +321,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -341,13 +341,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -361,13 +361,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
},
@@ -381,13 +381,13 @@
"team_member_limit": 10,
"features": [
"goals",
+ "teams",
+ "shared_links",
+ "site_segments",
"props",
"stats_api",
"revenue_goals",
- "funnels",
- "site_segments",
- "teams",
- "shared_links"
+ "funnels"
],
"data_retention_in_years": 5
}
diff --git a/test/plausible/billing/plans_test.exs b/test/plausible/billing/plans_test.exs
index b394c77287f1..2457835029cf 100644
--- a/test/plausible/billing/plans_test.exs
+++ b/test/plausible/billing/plans_test.exs
@@ -36,21 +36,21 @@ defmodule Plausible.Billing.PlansTest do
|> assert_generation(2)
end
- test "growth_plans_for/1 returns v4 plans for expired legacy subscriptions" do
+ test "growth_plans_for/1 returns latest plans for expired legacy subscriptions" do
new_user()
|> subscribe_to_plan(@v1_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
- |> assert_generation(4)
+ |> assert_generation(5)
end
- test "growth_plans_for/1 shows v4 plans for everyone else" do
+ test "growth_plans_for/1 shows latest plans for everyone else" do
new_user(trial_expiry_date: Date.utc_today())
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
- |> assert_generation(4)
+ |> assert_generation(5)
end
test "growth_plans_for/1 does not return business plans" do
@@ -69,7 +69,7 @@ defmodule Plausible.Billing.PlansTest do
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.growth_plans_for()
- |> assert_generation(4)
+ |> assert_generation(5)
end
test "business_plans_for/1 returns v3 business plans for a user on a legacy plan" do
@@ -92,13 +92,13 @@ defmodule Plausible.Billing.PlansTest do
assert_generation(business_plans, 3)
end
- test "business_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
+ test "business_plans_for/1 returns latest plans for invited users with trial_expiry = nil" do
nil
|> Plans.business_plans_for()
- |> assert_generation(4)
+ |> assert_generation(5)
end
- test "business_plans_for/1 returns v4 plans for expired legacy subscriptions" do
+ test "business_plans_for/1 returns latest plans for expired legacy subscriptions" do
user =
new_user()
|> subscribe_to_plan(@v2_plan_id, status: :deleted, next_bill_date: ~D[2023-11-10])
@@ -107,10 +107,10 @@ defmodule Plausible.Billing.PlansTest do
|> team_of(with_subscription?: true)
|> Map.fetch!(:subscription)
|> Plans.business_plans_for()
- |> assert_generation(4)
+ |> assert_generation(5)
end
- test "business_plans_for/1 returns v4 business plans for everyone else" do
+ test "business_plans_for/1 returns latest business plans for everyone else" do
user = new_user(trial_expiry_date: Date.utc_today())
subscription =
@@ -121,7 +121,7 @@ defmodule Plausible.Billing.PlansTest do
business_plans = Plans.business_plans_for(subscription)
assert Enum.all?(business_plans, &(&1.kind == :business))
- assert_generation(business_plans, 4)
+ assert_generation(business_plans, 5)
end
test "available_plans returns all plans for user with prices when asked for" do
@@ -179,7 +179,7 @@ defmodule Plausible.Billing.PlansTest do
Plausible.Teams.Billing.latest_enterprise_plan_with_price(team, "127.0.0.1")
assert enterprise_plan.paddle_plan_id == "123"
- assert price == Money.new(:EUR, "10.0")
+ assert price == Money.new(:EUR, "123.00")
end
end
@@ -214,42 +214,27 @@ defmodule Plausible.Billing.PlansTest do
end
end
- describe "suggested_plan/2" do
+ describe "suggest_volume/2" do
test "returns suggested plan based on usage" do
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
- assert %Plausible.Billing.Plan{
- monthly_pageview_limit: 100_000,
- monthly_cost: nil,
- monthly_product_id: "558745",
- volume: "100k",
- yearly_cost: nil,
- yearly_product_id: "590752"
- } = Plans.suggest(team, 10_000)
-
- assert %Plausible.Billing.Plan{
- monthly_pageview_limit: 200_000,
- monthly_cost: nil,
- monthly_product_id: "597485",
- volume: "200k",
- yearly_cost: nil,
- yearly_product_id: "597486"
- } = Plans.suggest(team, 100_000)
+ assert Plans.suggest_volume(team, 10_000) == "100k"
+ assert Plans.suggest_volume(team, 100_000) == "200k"
end
- test "returns nil when user has enterprise-level usage" do
+ test "returns :enterprise when user has enterprise-level usage" do
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
- assert :enterprise == Plans.suggest(team, 100_000_000)
+ assert Plans.suggest_volume(team, 10_000_000) == :enterprise
end
- test "returns nil when user is on an enterprise plan" do
+ test "returns :enterprise when user is on an enterprise plan" do
team =
new_user()
|> subscribe_to_plan(@v1_plan_id)
|> subscribe_to_enterprise_plan(billing_interval: :yearly, subscription?: false)
|> team_of()
- assert :enterprise == Plans.suggest(team, 10_000)
+ assert Plans.suggest_volume(team, 10_000) == :enterprise
end
end
@@ -309,7 +294,31 @@ defmodule Plausible.Billing.PlansTest do
"857091",
"857092",
"857093",
- "857094"
+ "857094",
+ "910414",
+ "910416",
+ "910418",
+ "910420",
+ "910422",
+ "910424",
+ "910426",
+ "910428",
+ "910430",
+ "910432",
+ "910434",
+ "910436",
+ "910438",
+ "910440",
+ "910442",
+ "910444",
+ "910446",
+ "910448",
+ "910450",
+ "910452",
+ "910454",
+ "910456",
+ "910458",
+ "910460"
] == Plans.yearly_product_ids()
end
end
diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs
index 5667632fc117..ee9174b3eb83 100644
--- a/test/plausible/billing/quota_test.exs
+++ b/test/plausible/billing/quota_test.exs
@@ -17,11 +17,13 @@ defmodule Plausible.Billing.QuotaTest do
@v2_plan_id "654177"
@v3_plan_id "749342"
@v4_1m_plan_id "857101"
- @v4_10m_growth_plan_id "857104"
- @v4_10m_business_plan_id "857112"
+ @v5_10m_starter_plan_id "910427"
+ @v5_10m_growth_plan_id "910443"
+ @v5_10m_business_plan_id "910459"
- @highest_growth_plan Plausible.Billing.Plans.find(@v4_10m_growth_plan_id)
- @highest_business_plan Plausible.Billing.Plans.find(@v4_10m_business_plan_id)
+ @highest_starter_plan Plausible.Billing.Plans.find(@v5_10m_starter_plan_id)
+ @highest_growth_plan Plausible.Billing.Plans.find(@v5_10m_growth_plan_id)
+ @highest_business_plan Plausible.Billing.Plans.find(@v5_10m_business_plan_id)
on_ee do
@v3_business_plan_id "857481"
@@ -958,7 +960,12 @@ defmodule Plausible.Billing.QuotaTest do
suggested_tier =
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
- |> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ nil
+ )
assert suggested_tier == nil
end
@@ -969,18 +976,44 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.merge(%{monthly_pageviews: %{last_30_days: %{total: 12_000_000}}, sites: 1})
- |> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ nil
+ )
assert suggested_tier == :custom
end
+ test "returns :starter if usage within starter limits",
+ %{team: team} do
+ suggested_tier =
+ team
+ |> Plausible.Teams.Billing.quota_usage(with_features: true)
+ |> Map.put(:sites, 2)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ nil
+ )
+
+ assert suggested_tier == :starter
+ end
+
test "returns :growth if usage within growth limits",
%{team: team} do
suggested_tier =
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
- |> Map.put(:sites, 1)
- |> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
+ |> Map.put(:sites, 8)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ nil
+ )
assert suggested_tier == :growth
end
@@ -991,7 +1024,12 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.put(:sites, 1)
- |> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, :business)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ :business
+ )
assert suggested_tier == :business
end
@@ -1002,7 +1040,12 @@ defmodule Plausible.Billing.QuotaTest do
team
|> Plausible.Teams.Billing.quota_usage(with_features: true)
|> Map.merge(%{sites: 1, features: [Plausible.Billing.Feature.Funnels]})
- |> Quota.suggest_tier(@highest_growth_plan, @highest_business_plan, nil)
+ |> Quota.suggest_tier(
+ @highest_starter_plan,
+ @highest_growth_plan,
+ @highest_business_plan,
+ nil
+ )
assert suggested_tier == :business
end
diff --git a/test/plausible/help_scout_test.exs b/test/plausible/help_scout_test.exs
index f3d534cfbd82..8df6bb2e55c0 100644
--- a/test/plausible/help_scout_test.exs
+++ b/test/plausible/help_scout_test.exs
@@ -120,7 +120,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: ^plan_link,
- plan_label: "10k Plan (€10 monthly)"
+ plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -145,7 +145,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: ^plan_link,
- plan_label: "10k Plan (€100 yearly)"
+ plan_label: "10k Plan (€190 yearly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -179,7 +179,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: _,
- plan_label: "1M Enterprise Plan (€10 monthly)"
+ plan_label: "1M Enterprise Plan (€123 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -198,7 +198,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paid",
plan_link: _,
- plan_label: "1M Enterprise Plan (€10 yearly)"
+ plan_label: "1M Enterprise Plan (€123 yearly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -216,7 +216,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Pending cancellation",
plan_link: _,
- plan_label: "10k Plan (€10 monthly)"
+ plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -235,7 +235,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Canceled",
plan_link: _,
- plan_label: "10k Plan (€10 monthly)"
+ plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -253,7 +253,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Paused",
plan_link: _,
- plan_label: "10k Plan (€10 monthly)"
+ plan_label: "10k Plan (€19 monthly)"
}} = HelpScout.get_details_for_customer("500")
end
@@ -271,7 +271,7 @@ defmodule Plausible.HelpScoutTest do
status_link: _,
status_label: "Dashboard locked",
plan_link: _,
- plan_label: "10k Plan (€10 monthly)",
+ plan_label: "10k Plan (€19 monthly)",
sites_count: 1
}} = HelpScout.get_details_for_customer("500")
end
diff --git a/test/plausible/release_test.exs b/test/plausible/release_test.exs
index 4c4d59f9886a..a72584a295a8 100644
--- a/test/plausible/release_test.exs
+++ b/test/plausible/release_test.exs
@@ -40,7 +40,7 @@ defmodule Plausible.ReleaseTest do
assert stdout =~ "Loading plausible.."
assert stdout =~ "Starting dependencies.."
assert stdout =~ "Starting repos.."
- assert stdout =~ "Inserted 54 plans"
+ assert stdout =~ "Inserted 78 plans"
end
test "ecto_repos sanity check" do
diff --git a/test/plausible_web/controllers/billing_controller_test.exs b/test/plausible_web/controllers/billing_controller_test.exs
index 6b5e0bada810..2c15718acd64 100644
--- a/test/plausible_web/controllers/billing_controller_test.exs
+++ b/test/plausible_web/controllers/billing_controller_test.exs
@@ -146,7 +146,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*\s*5k\s*<\/b>\s*hourly api requests/
- assert doc =~ ~r/The plan is priced at\s*\s*€10\s*<\/b>\s*/
+ assert doc =~ ~r/The plan is priced at\s*\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end
@@ -197,7 +197,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*\s*5k\s*<\/b>\s*hourly api requests/
- assert doc =~ ~r/The plan is priced at\s*\s*€10\s*<\/b>\s*/
+ assert doc =~ ~r/The plan is priced at\s*\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end
@@ -321,7 +321,7 @@ defmodule PlausibleWeb.BillingControllerTest do
assert doc =~ ~r/Up to\s*\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*\s*5k\s*<\/b>\s*hourly api requests/
- assert doc =~ ~r/The plan is priced at\s*\s*€10\s*<\/b>\s*/
+ assert doc =~ ~r/The plan is priced at\s*\s*€123\s*<\/b>\s*/
assert doc =~ "per year"
end
diff --git a/test/plausible_web/email_test.exs b/test/plausible_web/email_test.exs
index b8b380d5d610..3abbc86abcf1 100644
--- a/test/plausible_web/email_test.exs
+++ b/test/plausible_web/email_test.exs
@@ -152,7 +152,6 @@ defmodule PlausibleWeb.EmailTest do
team = build(:team, identifier: Ecto.UUID.generate())
penultimate_cycle = Date.range(~D[2023-03-01], ~D[2023-03-31])
last_cycle = Date.range(~D[2023-04-01], ~D[2023-04-30])
- suggested_plan = %Plausible.Billing.Plan{volume: "100k"}
usage = %{
penultimate_cycle: %{date_range: penultimate_cycle, total: 12_300},
@@ -160,7 +159,7 @@ defmodule PlausibleWeb.EmailTest do
}
%{html_body: html_body, subject: subject} =
- PlausibleWeb.Email.over_limit_email(user, team, usage, suggested_plan)
+ PlausibleWeb.Email.over_limit_email(user, team, usage, "100k")
assert subject == "[Action required] You have outgrown your Plausible subscription tier"
@@ -214,7 +213,6 @@ defmodule PlausibleWeb.EmailTest do
team = build(:team, identifier: Ecto.UUID.generate())
penultimate_cycle = Date.range(~D[2023-03-01], ~D[2023-03-31])
last_cycle = Date.range(~D[2023-04-01], ~D[2023-04-30])
- suggested_plan = %Plausible.Billing.Plan{volume: "100k"}
usage = %{
penultimate_cycle: %{date_range: penultimate_cycle, total: 12_300},
@@ -222,7 +220,7 @@ defmodule PlausibleWeb.EmailTest do
}
%{html_body: html_body, subject: subject} =
- PlausibleWeb.Email.dashboard_locked(user, team, usage, suggested_plan)
+ PlausibleWeb.Email.dashboard_locked(user, team, usage, "100k")
assert subject == "[Action required] Your Plausible dashboard is now locked"
diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs
index b2a1a279210e..25b7687c5d6e 100644
--- a/test/plausible_web/live/choose_plan_test.exs
+++ b/test/plausible_web/live/choose_plan_test.exs
@@ -11,9 +11,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@v1_10k_yearly_plan_id "572810"
@v1_50m_yearly_plan_id "650653"
@v2_20m_yearly_plan_id "653258"
- @v4_growth_10k_yearly_plan_id "857079"
+ @v5_starter_5m_monthly_plan_id "910425"
+ @v4_growth_10k_monthly_plan_id "857097"
@v4_growth_200k_yearly_plan_id "857081"
+ @v5_growth_10k_yearly_plan_id "910430"
+ @v5_growth_200k_yearly_plan_id "910434"
@v4_business_5m_monthly_plan_id "857111"
+ @v5_business_5m_monthly_plan_id "910457"
@v3_business_10k_monthly_plan_id "857481"
@monthly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="monthly"]/
@@ -22,16 +26,32 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@slider_input ~s/input[name="slider"]/
@slider_value "#slider-value"
+ @starter_plan_box "#starter-plan-box"
+ @starter_plan_tooltip "#starter-plan-box .tooltip-content"
+ @starter_price_tag_amount "#starter-price-tag-amount"
+ @starter_price_tag_interval "#starter-price-tag-interval"
+ @starter_discount_price_tag_amount "#starter-discount-price-tag-amount"
+ @starter_discount_price_tag_strikethrough_amount "#starter-discount-price-tag-strikethrough-amount"
+ @starter_vat_notice "#starter-vat-notice"
+ @starter_highlight_pill "#{@starter_plan_box} #highlight-pill"
+ @starter_checkout_button "#starter-checkout"
+
@growth_plan_box "#growth-plan-box"
@growth_plan_tooltip "#growth-plan-box .tooltip-content"
@growth_price_tag_amount "#growth-price-tag-amount"
@growth_price_tag_interval "#growth-price-tag-interval"
+ @growth_discount_price_tag_amount "#growth-discount-price-tag-amount"
+ @growth_discount_price_tag_strikethrough_amount "#growth-discount-price-tag-strikethrough-amount"
+ @growth_vat_notice "#growth-vat-notice"
@growth_highlight_pill "#{@growth_plan_box} #highlight-pill"
@growth_checkout_button "#growth-checkout"
@business_plan_box "#business-plan-box"
@business_price_tag_amount "#business-price-tag-amount"
@business_price_tag_interval "#business-price-tag-interval"
+ @business_discount_price_tag_amount "#business-discount-price-tag-amount"
+ @business_discount_price_tag_strikethrough_amount "#business-discount-price-tag-strikethrough-amount"
+ @business_vat_notice "#business-vat-notice"
@business_highlight_pill "#{@business_plan_box} #highlight-pill"
@business_checkout_button "#business-checkout"
@@ -46,14 +66,15 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays basic page content", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
- assert doc =~ "Upgrade your account"
+ assert doc =~ "Upgrade your trial"
+ assert doc =~ "Back to Settings"
assert doc =~ "You have used"
assert doc =~ "0"
assert doc =~ "billable pageviews in the last 30 days"
- assert doc =~ "Questions?"
- assert doc =~ "What happens if I go over my page views limit?"
+ assert doc =~ "Any other questions?"
+ assert doc =~ "What happens if I go over my monthly pageview limit?"
assert doc =~ "Enterprise"
- assert doc =~ "+ VAT if applicable"
+ assert doc =~ "+ VAT"
end
test "does not render any global notices", %{conn: conn} do
@@ -64,16 +85,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
+ assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert starter_box =~ "Email/Slack reports"
+ assert starter_box =~ "Google Analytics import"
+ assert starter_box =~ "Goals and custom events"
+ assert starter_box =~ "Up to 3 sites"
+ assert starter_box =~ "3 years of data retention"
+
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
- assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
- assert growth_box =~ "Email/Slack reports"
- assert growth_box =~ "Google Analytics import"
- assert growth_box =~ "Goals and custom events"
+ assert growth_box =~ "Team Accounts"
+ assert growth_box =~ "Shared Links"
+ assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
@@ -98,23 +126,24 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"https://plausible.io/white-label-web-analytics"
end
- test "default billing interval is monthly, and can switch to yearly", %{conn: conn} do
+ test "default billing interval is yearly, and can switch to monthly", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
- assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
- refute class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ refute class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
- doc = element(lv, @yearly_interval_button) |> render_click()
+ doc = element(lv, @monthly_interval_button) |> render_click()
- refute class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
- assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ refute class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
end
test "default pageview limit is 10k", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @slider_value) == "10k"
- assert text_of_element(doc, @growth_price_tag_amount) == "€10"
- assert text_of_element(doc, @business_price_tag_amount) == "€90"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€90"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€140"
+ assert text_of_element(doc, @business_price_tag_amount) == "€190"
end
test "pageview slider changes selected volume and prices shown", %{conn: conn} do
@@ -122,41 +151,69 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "100k")
assert text_of_element(doc, @slider_value) == "100k"
- assert text_of_element(doc, @growth_price_tag_amount) == "€20"
- assert text_of_element(doc, @business_price_tag_amount) == "€100"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€190"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€290"
+ assert text_of_element(doc, @business_price_tag_amount) == "€390"
doc = set_slider(lv, "200k")
assert text_of_element(doc, @slider_value) == "200k"
- assert text_of_element(doc, @growth_price_tag_amount) == "€30"
- assert text_of_element(doc, @business_price_tag_amount) == "€110"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€290"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€440"
+ assert text_of_element(doc, @business_price_tag_amount) == "€590"
doc = set_slider(lv, "500k")
assert text_of_element(doc, @slider_value) == "500k"
- assert text_of_element(doc, @growth_price_tag_amount) == "€40"
- assert text_of_element(doc, @business_price_tag_amount) == "€120"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€490"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€740"
+ assert text_of_element(doc, @business_price_tag_amount) == "€990"
doc = set_slider(lv, "1M")
assert text_of_element(doc, @slider_value) == "1M"
- assert text_of_element(doc, @growth_price_tag_amount) == "€50"
- assert text_of_element(doc, @business_price_tag_amount) == "€130"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€690"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€1,040"
+ assert text_of_element(doc, @business_price_tag_amount) == "€1,390"
doc = set_slider(lv, "2M")
assert text_of_element(doc, @slider_value) == "2M"
- assert text_of_element(doc, @growth_price_tag_amount) == "€60"
- assert text_of_element(doc, @business_price_tag_amount) == "€140"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€890"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€1,340"
+ assert text_of_element(doc, @business_price_tag_amount) == "€1,790"
doc = set_slider(lv, "5M")
assert text_of_element(doc, @slider_value) == "5M"
- assert text_of_element(doc, @growth_price_tag_amount) == "€70"
- assert text_of_element(doc, @business_price_tag_amount) == "€150"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€1,290"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€1,940"
+ assert text_of_element(doc, @business_price_tag_amount) == "€2,590"
doc = set_slider(lv, "10M")
assert text_of_element(doc, @slider_value) == "10M"
- assert text_of_element(doc, @growth_price_tag_amount) == "€80"
- assert text_of_element(doc, @business_price_tag_amount) == "€160"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€1,690"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€2,540"
+ assert text_of_element(doc, @business_price_tag_amount) == "€3,390"
end
- test "renders contact links for business and growth tiers when enterprise-level volume selected",
+ test "displays monthly discount for yearly plans", %{conn: conn} do
+ {:ok, lv, _doc} = get_liveview(conn)
+
+ doc = set_slider(lv, "200k")
+
+ assert text_of_element(doc, @starter_price_tag_amount) == "€290"
+ assert text_of_element(doc, @starter_discount_price_tag_amount) == "€24.17"
+ assert text_of_element(doc, @starter_discount_price_tag_strikethrough_amount) == "€29"
+ assert text_of_element(doc, @starter_vat_notice) == "+ VAT if applicable"
+
+ assert text_of_element(doc, @growth_price_tag_amount) == "€440"
+ assert text_of_element(doc, @growth_discount_price_tag_amount) == "€36.67"
+ assert text_of_element(doc, @growth_discount_price_tag_strikethrough_amount) == "€44"
+ assert text_of_element(doc, @growth_vat_notice) == "+ VAT if applicable"
+
+ assert text_of_element(doc, @business_price_tag_amount) == "€590"
+ assert text_of_element(doc, @business_discount_price_tag_amount) == "€49.17"
+ assert text_of_element(doc, @business_discount_price_tag_strikethrough_amount) == "€59"
+ assert text_of_element(doc, @business_vat_notice) == "+ VAT if applicable"
+ end
+
+ test "renders contact links for all tiers when enterprise-level volume selected",
%{
conn: conn
} do
@@ -164,6 +221,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "10M+")
+ assert text_of_element(doc, "#starter-custom-price") =~ "Custom"
+ assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, "#growth-custom-price") =~ "Custom"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
assert text_of_element(doc, "#business-custom-price") =~ "Custom"
@@ -171,28 +230,36 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "10M")
+ refute text_of_element(doc, "#starter-custom-price") =~ "Custom"
+ refute text_of_element(doc, @starter_plan_box) =~ "Contact us"
refute text_of_element(doc, "#growth-custom-price") =~ "Custom"
refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
refute text_of_element(doc, "#business-custom-price") =~ "Custom"
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
end
- test "switching billing interval changes business and growth prices", %{conn: conn} do
+ test "switching billing interval changes prices", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
- assert text_of_element(doc, @growth_price_tag_amount) == "€10"
- assert text_of_element(doc, @growth_price_tag_interval) == "/month"
+ assert text_of_element(doc, @starter_price_tag_amount) == "€90"
+ assert text_of_element(doc, @starter_price_tag_interval) == "/year"
- assert text_of_element(doc, @business_price_tag_amount) == "€90"
- assert text_of_element(doc, @business_price_tag_interval) == "/month"
-
- doc = element(lv, @yearly_interval_button) |> render_click()
-
- assert text_of_element(doc, @growth_price_tag_amount) == "€100"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€140"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
- assert text_of_element(doc, @business_price_tag_amount) == "€900"
+ assert text_of_element(doc, @business_price_tag_amount) == "€190"
assert text_of_element(doc, @business_price_tag_interval) == "/year"
+
+ doc = element(lv, @monthly_interval_button) |> render_click()
+
+ assert text_of_element(doc, @starter_price_tag_amount) == "€9"
+ assert text_of_element(doc, @starter_price_tag_interval) == "/month"
+
+ assert text_of_element(doc, @growth_price_tag_amount) == "€14"
+ assert text_of_element(doc, @growth_price_tag_interval) == "/month"
+
+ assert text_of_element(doc, @business_price_tag_amount) == "€19"
+ assert text_of_element(doc, @business_price_tag_interval) == "/month"
end
test "checkout buttons are 'paddle buttons' with dynamic onclick attribute", %{
@@ -209,7 +276,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"disableLogout" => true,
"email" => user.email,
"passthrough" => "ee:true;user:#{user.id};team:#{team.id}",
- "product" => @v4_growth_200k_yearly_plan_id,
+ "product" => @v5_growth_200k_yearly_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, @growth_checkout_button))
@@ -217,8 +284,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
set_slider(lv, "5M")
doc = element(lv, @monthly_interval_button) |> render_click()
+ assert get_paddle_checkout_params(find(doc, @starter_checkout_button))["product"] ==
+ @v5_starter_5m_monthly_plan_id
+
assert get_paddle_checkout_params(find(doc, @business_checkout_button))["product"] ==
- @v4_business_5m_monthly_plan_id
+ @v5_business_5m_monthly_plan_id
end
test "warns about losing access to a feature", %{conn: conn, site: site} do
@@ -230,18 +300,30 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {Paddle.Checkout.open"
end
- test "recommends Growth tier when no premium features were used", %{conn: conn} do
+ test "recommends Starter", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @starter_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ refute element_exists?(doc, @business_highlight_pill)
+ end
+
+ test "recommends Growth", %{conn: conn, site: site} do
+ for _ <- 1..3, do: add_guest(site, role: :viewer)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ refute element_exists?(doc, @starter_highlight_pill)
assert text_of_element(doc, @growth_highlight_pill) == "Recommended"
refute element_exists?(doc, @business_highlight_pill)
end
- test "recommends Business when Revenue Goals used during trial", %{conn: conn, site: site} do
+ test "recommends Business", %{conn: conn, site: site} do
insert(:goal, site: site, currency: :USD, event_name: "Purchase")
{:ok, _lv, doc} = get_liveview(conn)
+ refute element_exists?(doc, @starter_highlight_pill)
assert text_of_element(doc, @business_highlight_pill) == "Recommended"
refute element_exists?(doc, @growth_highlight_pill)
end
@@ -308,6 +390,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
+ refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
refute element_exists?(doc, @growth_plan_tooltip)
@@ -317,6 +400,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@@ -336,6 +420,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
+ refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@@ -344,6 +429,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@@ -363,6 +449,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
+ refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
@@ -371,20 +458,21 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "10k")
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
end
- describe "for a user with an active v4 growth subscription plan" do
- setup [:create_user, :create_site, :log_in, :subscribe_v4_growth]
+ describe "for a user with an active v5 growth subscription plan" do
+ setup [:create_user, :create_site, :log_in, :subscribe_v5_growth]
test "displays basic page content", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
- assert doc =~ "Change subscription plan"
- assert doc =~ "Questions?"
- refute doc =~ "What happens if I go over my page views limit?"
+ assert doc =~ "Change your subscription plan"
+ assert doc =~ "Any other questions?"
+ assert doc =~ "What happens if I go over my monthly pageview limit?"
end
test "does not render any global notices", %{conn: conn} do
@@ -395,17 +483,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
+ assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert starter_box =~ "Email/Slack reports"
+ assert starter_box =~ "Google Analytics import"
+ assert starter_box =~ "Goals and custom events"
+ assert starter_box =~ "Up to 3 sites"
+ assert starter_box =~ "3 years of data retention"
+
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
- assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
- assert growth_box =~ "Email/Slack reports"
- assert growth_box =~ "Google Analytics import"
- assert growth_box =~ "Goals and custom events"
- assert growth_box =~ "3 years of data retention"
+ assert growth_box =~ "Team Accounts"
+ assert growth_box =~ "Shared Links"
+ assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Up to 10 team members"
@@ -475,6 +569,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [Billing.pending_site_ownerships_notice_title()])
assert doc =~ "Your account has been invited to become the owner of a site"
+ assert text_of_element(doc, @starter_plan_tooltip) ==
+ "Your usage exceeds the following limit(s): Team member limit"
+
assert text_of_element(doc, @growth_plan_tooltip) ==
"Your usage exceeds the following limit(s): Team member limit"
@@ -505,9 +602,17 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute element_exists?(doc, @growth_highlight_pill)
end
- test "gets default selected interval from current subscription plan", %{conn: conn} do
+ test "gets default selected interval from current subscription plan", %{
+ conn: conn,
+ user: user
+ } do
{:ok, _lv, doc} = get_liveview(conn)
assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+
+ subscribe_to_plan(user, @v4_growth_10k_monthly_plan_id)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+ assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
end
test "sets pageview slider according to last cycle usage", %{conn: conn} do
@@ -540,22 +645,27 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "200k")
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Currently on this plan"
- assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
+ assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
+
doc = element(lv, @monthly_interval_button) |> render_click()
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Change billing interval"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
doc = set_slider(lv, "1M")
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Upgrade"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
doc = set_slider(lv, "100k")
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade"
assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
end
@@ -568,15 +678,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
growth_checkout_button = find(doc, @growth_checkout_button)
assert text_of_attr(growth_checkout_button, "onclick") =~
- "if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_growth_10k_yearly_plan_id)}'}"
+ "if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_growth_10k_yearly_plan_id)}'}"
set_slider(lv, "5M")
doc = element(lv, @monthly_interval_button) |> render_click()
+ starter_checkout_button = find(doc, @starter_checkout_button)
+
+ assert text_of_attr(starter_checkout_button, "onclick") =~
+ "if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_starter_5m_monthly_plan_id)}'}"
+
business_checkout_button = find(doc, @business_checkout_button)
assert text_of_attr(business_checkout_button, "onclick") =~
- "if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_business_5m_monthly_plan_id)}'}"
+ "if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v5_business_5m_monthly_plan_id)}'}"
end
end
@@ -607,6 +722,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert class =~ "ring-indigo-600"
assert text_of_element(doc, @business_highlight_pill) == "Current"
+ refute element_exists?(doc, @starter_highlight_pill)
refute element_exists?(doc, @growth_highlight_pill)
end
@@ -631,6 +747,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @enterprise_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @starter_highlight_pill)
refute element_exists?(doc, @business_highlight_pill)
refute element_exists?(doc, @growth_highlight_pill)
end
@@ -640,23 +757,29 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, "5M")
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
+ assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
assert text_of_element(doc, @business_checkout_button) == "Currently on this plan"
+
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none bg-gray-400"
doc = element(lv, @yearly_interval_button) |> render_click()
- assert text_of_element(doc, @business_checkout_button) == "Change billing interval"
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
+ assert text_of_element(doc, @business_checkout_button) == "Change billing interval"
doc = set_slider(lv, "10M")
- assert text_of_element(doc, @business_checkout_button) == "Upgrade"
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
+ assert text_of_element(doc, @business_checkout_button) == "Upgrade"
doc = set_slider(lv, "100k")
- assert text_of_element(doc, @business_checkout_button) == "Downgrade"
+ assert text_of_element(doc, @starter_checkout_button) == "Downgrade to Starter"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
+ assert text_of_element(doc, @business_checkout_button) == "Downgrade"
end
test "checkout is disabled when team member usage exceeds rendered plan limit", %{
@@ -668,6 +791,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @starter_plan_box) =~ "Your usage exceeds this plan"
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
+
+ assert text_of_element(doc, @starter_plan_tooltip) ==
+ "Your usage exceeds the following limit(s): Team member limit"
+
assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
@@ -683,6 +812,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @starter_plan_box) =~ "Your usage exceeds this plan"
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
+
+ assert text_of_element(doc, @starter_plan_tooltip) ==
+ "Your usage exceeds the following limit(s): Site limit"
+
assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
@@ -701,6 +836,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @starter_plan_tooltip) =~ "Team member limit"
+ assert text_of_element(doc, @starter_plan_tooltip) =~ "Site limit"
+
assert text_of_element(doc, @growth_plan_tooltip) =~ "Team member limit"
assert text_of_element(doc, @growth_plan_tooltip) =~ "Site limit"
end
@@ -757,16 +895,23 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "displays plan benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ starter_box = text_of_element(doc, @starter_plan_box)
growth_box = text_of_element(doc, @growth_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
+ assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert starter_box =~ "Email/Slack reports"
+ assert starter_box =~ "Google Analytics import"
+ assert starter_box =~ "Goals and custom events"
+ assert starter_box =~ "Up to 3 sites"
+ assert starter_box =~ "3 years of data retention"
+
assert growth_box =~ "Up to 3 team members"
assert growth_box =~ "Up to 10 sites"
- assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
- assert growth_box =~ "Email/Slack reports"
- assert growth_box =~ "Google Analytics import"
- assert growth_box =~ "Goals and custom events"
+ assert growth_box =~ "Team Accounts"
+ assert growth_box =~ "Shared Links"
+ assert growth_box =~ "Shared Segments"
assert business_box =~ "Everything in Growth"
assert business_box =~ "Unlimited team members"
@@ -824,6 +969,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, "#{@growth_checkout_button} + div") =~
"Please update your billing details first"
+
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none bg-gray-400"
+
+ assert text_of_element(doc, "#{@starter_checkout_button} + div") =~
+ "Please update your billing details first"
end
end
@@ -858,6 +1008,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, "#{@growth_checkout_button} + div") =~
"Please update your billing details first"
+
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none bg-gray-400"
+
+ assert text_of_element(doc, "#{@starter_checkout_button} + div") =~
+ "Please update your billing details first"
end
end
@@ -871,6 +1026,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "checkout buttons are paddle buttons", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_attr(find(doc, @starter_checkout_button), "onclick") =~
+ "Paddle.Checkout.open"
+
assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ "Paddle.Checkout.open"
assert text_of_attr(find(doc, @business_checkout_button), "onclick") =~
@@ -909,7 +1068,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "highlights recommended tier", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
- assert text_of_element(doc, @growth_highlight_pill) == "Recommended"
+ assert text_of_element(doc, @starter_highlight_pill) == "Recommended"
+ refute text_of_element(doc, @growth_highlight_pill) == "Recommended"
refute text_of_element(doc, @business_highlight_pill) == "Recommended"
end
end
@@ -923,7 +1083,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [])
end
- test "on a 50M v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not",
+ test "on a 50M v1 plan, Growth plans are available at 20M, 50M, 50M+, but Starter and Business plans are not",
%{conn: conn, user: user} do
create_subscription_for(user, paddle_plan_id: @v1_50m_yearly_plan_id)
@@ -931,23 +1091,27 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, 8)
assert text_of_element(doc, @slider_value) == "20M"
+ assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
- assert text_of_element(doc, @growth_price_tag_amount) == "€900"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€1,800"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 9)
assert text_of_element(doc, @slider_value) == "50M"
+ assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
- assert text_of_element(doc, @growth_price_tag_amount) == "€1,000"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€2,640"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 10)
assert text_of_element(doc, @slider_value) == "50M+"
+ assert text_of_element(doc, @starter_plan_box) =~ "Contact us"
assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
doc = set_slider(lv, 7)
assert text_of_element(doc, @slider_value) == "10M"
+ refute text_of_element(doc, @starter_plan_box) =~ "Contact us"
refute text_of_element(doc, @business_plan_box) =~ "Contact us"
refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
end
@@ -960,13 +1124,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
doc = set_slider(lv, 8)
assert text_of_element(doc, @slider_value) == "20M"
- assert text_of_element(doc, @business_plan_box) =~ "Contact us"
- assert text_of_element(doc, @growth_price_tag_amount) == "€900"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€2,250"
assert text_of_element(doc, @growth_price_tag_interval) == "/year"
doc = set_slider(lv, 9)
assert text_of_element(doc, @slider_value) == "20M+"
- assert text_of_element(doc, @business_plan_box) =~ "Contact us"
assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
end
end
@@ -1000,12 +1162,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
refute growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
end
- test "displays business and enterprise plan benefits", %{conn: conn} do
+ test "displays Starter, Business and Enterprise benefits", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ starter_box = text_of_element(doc, @starter_plan_box)
business_box = text_of_element(doc, @business_plan_box)
enterprise_box = text_of_element(doc, @enterprise_plan_box)
+ assert starter_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert starter_box =~ "Email/Slack reports"
+ assert starter_box =~ "Google Analytics import"
+ assert starter_box =~ "Goals and custom events"
+ assert starter_box =~ "Up to 3 sites"
+ assert starter_box =~ "3 years of data retention"
+
assert business_box =~ "Everything in Growth"
assert business_box =~ "Funnels"
assert business_box =~ "Ecommerce revenue attribution"
@@ -1053,6 +1223,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "100k")
+ refute class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@@ -1061,9 +1232,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a free_10k subscription" do
setup [:create_user, :create_site, :log_in, :subscribe_free_10k]
- test "recommends growth tier when no premium features used", %{conn: conn} do
+ test "recommends starter tier", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
- assert element_exists?(doc, @growth_highlight_pill)
+ assert element_exists?(doc, @starter_highlight_pill)
+ refute element_exists?(doc, @growth_highlight_pill)
refute element_exists?(doc, @business_highlight_pill)
end
@@ -1074,6 +1246,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, @business_plan_box) =~ "Recommended"
refute text_of_element(doc, @growth_plan_box) =~ "Recommended"
+ refute text_of_element(doc, @starter_plan_box) =~ "Recommended"
end
test "renders Paddle upgrade buttons", %{conn: conn, user: user} do
@@ -1087,7 +1260,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"disableLogout" => true,
"email" => user.email,
"passthrough" => "ee:true;user:#{user.id};team:#{team.id}",
- "product" => @v4_growth_200k_yearly_plan_id,
+ "product" => @v5_growth_200k_yearly_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, @growth_checkout_button))
@@ -1103,6 +1276,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
check_notice_titles(doc, [Billing.upgrade_ineligible_notice_title()])
assert text_of_element(doc, "#upgrade-eligible-notice") =~ "You cannot start a subscription"
+ assert class_of_element(doc, @starter_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
end
@@ -1127,9 +1301,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "allows to subscribe", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @starter_plan_box) =~ "Recommended"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
- assert text_of_element(doc, @growth_plan_box) =~ "Recommended"
end
end
@@ -1155,8 +1329,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
end)
end
- defp subscribe_v4_growth(%{user: user}) do
- create_subscription_for(user, paddle_plan_id: @v4_growth_200k_yearly_plan_id)
+ defp subscribe_v5_growth(%{user: user}) do
+ create_subscription_for(user, paddle_plan_id: @v5_growth_200k_yearly_plan_id)
end
defp subscribe_v4_business(%{user: user}) do
diff --git a/test/plausible_web/live/legacy_choose_plan_test.exs b/test/plausible_web/live/legacy_choose_plan_test.exs
new file mode 100644
index 000000000000..a9c28c9a4021
--- /dev/null
+++ b/test/plausible_web/live/legacy_choose_plan_test.exs
@@ -0,0 +1,1259 @@
+defmodule PlausibleWeb.Live.LegacyChoosePlanTest do
+ @moduledoc """
+ [Deprecated]. This file tests the legacy behaviour of the
+ /billing/choose-plan page without the `starter_tier` feature flag enabled.
+ """
+ use PlausibleWeb.ConnCase, async: true
+ use Plausible.Teams.Test
+ @moduletag :ee_only
+
+ import Phoenix.LiveViewTest
+ import Plausible.Test.Support.HTML
+ require Plausible.Billing.Subscription.Status
+ alias Plausible.{Repo, Billing, Billing.Subscription}
+
+ @v1_10k_yearly_plan_id "572810"
+ @v1_50m_yearly_plan_id "650653"
+ @v2_20m_yearly_plan_id "653258"
+ @v4_growth_10k_yearly_plan_id "857079"
+ @v4_growth_200k_yearly_plan_id "857081"
+ @v4_business_5m_monthly_plan_id "857111"
+ @v3_business_10k_monthly_plan_id "857481"
+
+ @monthly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="monthly"]/
+ @yearly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="yearly"]/
+ @interval_button_active_class "bg-indigo-600 text-white"
+ @slider_input ~s/input[name="slider"]/
+ @slider_value "#slider-value"
+
+ @growth_plan_box "#growth-plan-box"
+ @growth_plan_tooltip "#growth-plan-box .tooltip-content"
+ @growth_price_tag_amount "#growth-price-tag-amount"
+ @growth_price_tag_interval "#growth-price-tag-interval"
+ @growth_highlight_pill "#{@growth_plan_box} #highlight-pill"
+ @growth_checkout_button "#growth-checkout"
+
+ @business_plan_box "#business-plan-box"
+ @business_price_tag_amount "#business-price-tag-amount"
+ @business_price_tag_interval "#business-price-tag-interval"
+ @business_highlight_pill "#{@business_plan_box} #highlight-pill"
+ @business_checkout_button "#business-checkout"
+
+ @enterprise_plan_box "#enterprise-plan-box"
+ @enterprise_highlight_pill "#enterprise-highlight-pill"
+
+ @slider_volumes ["10k", "100k", "200k", "500k", "1M", "2M", "5M", "10M", "10M+"]
+
+ describe "for a user with no subscription" do
+ setup [:create_user, :disable_starter_tier, :create_site, :log_in]
+
+ test "displays basic page content", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert doc =~ "Upgrade your account"
+ assert doc =~ "You have used"
+ assert doc =~ "0"
+ assert doc =~ "billable pageviews in the last 30 days"
+ assert doc =~ "Questions?"
+ assert doc =~ "What happens if I go over my page views limit?"
+ assert doc =~ "Enterprise"
+ assert doc =~ "+ VAT if applicable"
+ end
+
+ test "does not render any global notices", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+ check_notice_titles(doc, [])
+ end
+
+ test "displays plan benefits", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ growth_box = text_of_element(doc, @growth_plan_box)
+ business_box = text_of_element(doc, @business_plan_box)
+ enterprise_box = text_of_element(doc, @enterprise_plan_box)
+
+ assert growth_box =~ "Up to 3 team members"
+ assert growth_box =~ "Up to 10 sites"
+ assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert growth_box =~ "Email/Slack reports"
+ assert growth_box =~ "Google Analytics import"
+ assert growth_box =~ "Goals and custom events"
+
+ assert business_box =~ "Everything in Growth"
+ assert business_box =~ "Up to 10 team members"
+ assert business_box =~ "Up to 50 sites"
+ assert business_box =~ "Stats API (600 requests per hour)"
+ assert business_box =~ "Looker Studio Connector"
+ assert business_box =~ "Custom Properties"
+ assert business_box =~ "Funnels"
+ assert business_box =~ "Ecommerce revenue attribution"
+ assert business_box =~ "Priority support"
+
+ refute business_box =~ "Goals and custom events"
+
+ assert enterprise_box =~ "Everything in Business"
+ assert enterprise_box =~ "10+ team members"
+ assert enterprise_box =~ "50+ sites"
+ assert enterprise_box =~ "600+ Stats API requests per hour"
+ assert enterprise_box =~ "Sites API access for"
+ assert enterprise_box =~ "Technical onboarding"
+
+ assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
+ "https://plausible.io/white-label-web-analytics"
+ end
+
+ test "default billing interval is monthly, and can switch to yearly", %{conn: conn} do
+ {:ok, lv, doc} = get_liveview(conn)
+
+ assert class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
+ refute class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+
+ doc = element(lv, @yearly_interval_button) |> render_click()
+
+ refute class_of_element(doc, @monthly_interval_button) =~ @interval_button_active_class
+ assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ end
+
+ test "default pageview limit is 10k", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @slider_value) == "10k"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€9"
+ assert text_of_element(doc, @business_price_tag_amount) == "€19"
+ end
+
+ test "pageview slider changes selected volume and prices shown", %{conn: conn} do
+ {:ok, lv, _doc} = get_liveview(conn)
+
+ doc = set_slider(lv, "100k")
+ assert text_of_element(doc, @slider_value) == "100k"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€19"
+ assert text_of_element(doc, @business_price_tag_amount) == "€39"
+
+ doc = set_slider(lv, "200k")
+ assert text_of_element(doc, @slider_value) == "200k"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€29"
+ assert text_of_element(doc, @business_price_tag_amount) == "€59"
+
+ doc = set_slider(lv, "500k")
+ assert text_of_element(doc, @slider_value) == "500k"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€49"
+ assert text_of_element(doc, @business_price_tag_amount) == "€99"
+
+ doc = set_slider(lv, "1M")
+ assert text_of_element(doc, @slider_value) == "1M"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€69"
+ assert text_of_element(doc, @business_price_tag_amount) == "€139"
+
+ doc = set_slider(lv, "2M")
+ assert text_of_element(doc, @slider_value) == "2M"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€89"
+ assert text_of_element(doc, @business_price_tag_amount) == "€179"
+
+ doc = set_slider(lv, "5M")
+ assert text_of_element(doc, @slider_value) == "5M"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€129"
+ assert text_of_element(doc, @business_price_tag_amount) == "€259"
+
+ doc = set_slider(lv, "10M")
+ assert text_of_element(doc, @slider_value) == "10M"
+ assert text_of_element(doc, @growth_price_tag_amount) == "€169"
+ assert text_of_element(doc, @business_price_tag_amount) == "€339"
+ end
+
+ test "renders contact links for business and growth tiers when enterprise-level volume selected",
+ %{
+ conn: conn
+ } do
+ {:ok, lv, _doc} = get_liveview(conn)
+
+ doc = set_slider(lv, "10M+")
+
+ assert text_of_element(doc, "#growth-custom-price") =~ "Custom"
+ assert text_of_element(doc, @growth_plan_box) =~ "Contact us"
+ assert text_of_element(doc, "#business-custom-price") =~ "Custom"
+ assert text_of_element(doc, @business_plan_box) =~ "Contact us"
+
+ doc = set_slider(lv, "10M")
+
+ refute text_of_element(doc, "#growth-custom-price") =~ "Custom"
+ refute text_of_element(doc, @growth_plan_box) =~ "Contact us"
+ refute text_of_element(doc, "#business-custom-price") =~ "Custom"
+ refute text_of_element(doc, @business_plan_box) =~ "Contact us"
+ end
+
+ test "switching billing interval changes business and growth prices", %{conn: conn} do
+ {:ok, lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @growth_price_tag_amount) == "€9"
+ assert text_of_element(doc, @growth_price_tag_interval) == "/month"
+
+ assert text_of_element(doc, @business_price_tag_amount) == "€19"
+ assert text_of_element(doc, @business_price_tag_interval) == "/month"
+
+ doc = element(lv, @yearly_interval_button) |> render_click()
+
+ assert text_of_element(doc, @growth_price_tag_amount) == "€90"
+ assert text_of_element(doc, @growth_price_tag_interval) == "/year"
+
+ assert text_of_element(doc, @business_price_tag_amount) == "€190"
+ assert text_of_element(doc, @business_price_tag_interval) == "/year"
+ end
+
+ test "checkout buttons are 'paddle buttons' with dynamic onclick attribute", %{
+ conn: conn,
+ user: user
+ } do
+ {:ok, lv, _doc} = get_liveview(conn)
+ {:ok, team} = Plausible.Teams.get_by_owner(user)
+
+ set_slider(lv, "200k")
+ doc = element(lv, @yearly_interval_button) |> render_click()
+
+ assert %{
+ "disableLogout" => true,
+ "email" => user.email,
+ "passthrough" => "ee:true;user:#{user.id};team:#{team.id}",
+ "product" => @v4_growth_200k_yearly_plan_id,
+ "success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
+ "theme" => "none"
+ } == get_paddle_checkout_params(find(doc, @growth_checkout_button))
+
+ set_slider(lv, "5M")
+ doc = element(lv, @monthly_interval_button) |> render_click()
+
+ assert get_paddle_checkout_params(find(doc, @business_checkout_button))["product"] ==
+ @v4_business_5m_monthly_plan_id
+ end
+
+ test "warns about losing access to a feature", %{conn: conn, site: site} do
+ Plausible.Props.allow(site, ["author"])
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~
+ "if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {Paddle.Checkout.open"
+ end
+
+ test "recommends Growth tier when no premium features were used", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @growth_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @business_highlight_pill)
+ end
+
+ test "recommends Business when Revenue Goals used during trial", %{conn: conn, site: site} do
+ insert(:goal, site: site, currency: :USD, event_name: "Purchase")
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @business_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ end
+
+ test "recommends Business when pending ownership site used a premium feature", %{
+ conn: conn,
+ user: user
+ } do
+ previous_owner = insert(:user)
+ site = new_site(owner: previous_owner)
+
+ insert(:goal, site: site, currency: :USD, event_name: "Purchase")
+
+ invite_transfer(site, user, inviter: previous_owner)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @business_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ end
+
+ test "recommends Business when team member limit for Growth exceeded due to pending ownerships",
+ %{conn: conn, user: user} do
+ owned_site = new_site(owner: user)
+ add_guest(owned_site, role: :editor)
+ add_guest(owned_site, role: :editor)
+
+ previous_owner = new_user()
+
+ pending_ownership_site = new_site(owner: previous_owner)
+ add_guest(pending_ownership_site, role: :viewer)
+
+ invite_transfer(pending_ownership_site, user, inviter: previous_owner)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @business_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ end
+
+ test "recommends Business when Growth site limit exceeded due to a pending ownership", %{
+ conn: conn,
+ user: user
+ } do
+ for _ <- 1..9, do: new_site(owner: user)
+ assert user |> team_of() |> Plausible.Teams.Billing.site_usage() == 10
+
+ another_user = new_user()
+ pending_ownership_site = new_site(owner: another_user)
+
+ invite_transfer(pending_ownership_site, user, inviter: another_user)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert text_of_element(doc, @business_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ end
+
+ @tag :slow
+ test "allows upgrade to a 100k plan with a pageview allowance margin of 0.15 when trial is active",
+ %{conn: conn, site: site} do
+ generate_usage_for(site, 115_000)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "100k")
+
+ refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+ refute element_exists?(doc, @growth_plan_tooltip)
+
+ generate_usage_for(site, 1)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "100k")
+
+ assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+
+ assert text_of_element(doc, @growth_plan_tooltip) ==
+ "Your usage exceeds the following limit(s): Monthly pageview limit"
+ end
+
+ test "allows upgrade to a 10k plan with a pageview allowance margin of 0.3 when trial ended 10 days ago",
+ %{conn: conn, site: site, user: user} do
+ user
+ |> team_of()
+ |> Ecto.Changeset.change(trial_expiry_date: Date.shift(Date.utc_today(), day: -10))
+ |> Repo.update!()
+
+ generate_usage_for(site, 13_000)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "10k")
+
+ refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+
+ generate_usage_for(site, 1)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "10k")
+
+ assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+ end
+
+ test "pageview allowance margin on upgrade is 0.1 when trial ended more than 10 days ago", %{
+ conn: conn,
+ site: site,
+ user: user
+ } do
+ user
+ |> team_of()
+ |> Ecto.Changeset.change(trial_expiry_date: Date.shift(Date.utc_today(), day: -11))
+ |> Repo.update!()
+
+ generate_usage_for(site, 11_000)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "10k")
+
+ refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ refute class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+
+ generate_usage_for(site, 1)
+
+ {:ok, lv, _doc} = get_liveview(conn)
+ doc = set_slider(lv, "10k")
+
+ assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
+ assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none"
+ end
+ end
+
+ describe "for a user with an active v4 growth subscription plan" do
+ setup [:create_user, :disable_starter_tier, :create_site, :log_in, :subscribe_v4_growth]
+
+ test "displays basic page content", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert doc =~ "Change subscription plan"
+ assert doc =~ "Questions?"
+ refute doc =~ "What happens if I go over my page views limit?"
+ end
+
+ test "does not render any global notices", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+ check_notice_titles(doc, [])
+ end
+
+ test "displays plan benefits", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ growth_box = text_of_element(doc, @growth_plan_box)
+ business_box = text_of_element(doc, @business_plan_box)
+ enterprise_box = text_of_element(doc, @enterprise_plan_box)
+
+ assert growth_box =~ "Up to 3 team members"
+ assert growth_box =~ "Up to 10 sites"
+ assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard"
+ assert growth_box =~ "Email/Slack reports"
+ assert growth_box =~ "Google Analytics import"
+ assert growth_box =~ "Goals and custom events"
+ assert growth_box =~ "3 years of data retention"
+
+ assert business_box =~ "Everything in Growth"
+ assert business_box =~ "Up to 10 team members"
+ assert business_box =~ "Up to 50 sites"
+ assert business_box =~ "Stats API (600 requests per hour)"
+ assert business_box =~ "Looker Studio Connector"
+ assert business_box =~ "Custom Properties"
+ assert business_box =~ "Funnels"
+ assert business_box =~ "Ecommerce revenue attribution"
+ assert business_box =~ "Priority support"
+ assert business_box =~ "5 years of data retention"
+
+ refute business_box =~ "Goals and custom events"
+
+ assert enterprise_box =~ "Everything in Business"
+ assert enterprise_box =~ "10+ team members"
+ assert enterprise_box =~ "50+ sites"
+ assert enterprise_box =~ "600+ Stats API requests per hour"
+ assert enterprise_box =~ "Sites API access for"
+ assert enterprise_box =~ "Technical onboarding"
+ assert enterprise_box =~ "5+ years of data retention"
+
+ assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~
+ "https://plausible.io/white-label-web-analytics"
+ end
+
+ test "displays usage in the last cycle", %{conn: conn, site: site} do
+ yesterday = NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :day)
+
+ populate_stats(site, [
+ build(:pageview, timestamp: yesterday),
+ build(:pageview, timestamp: yesterday)
+ ])
+
+ {:ok, _lv, doc} = get_liveview(conn)
+ assert doc =~ "You have used"
+ assert doc =~ "2"
+ assert doc =~ "billable pageviews in the last billing cycle"
+ end
+
+ test "renders notice about pending ownerships and counts their usage", %{
+ conn: conn,
+ user: user,
+ site: site
+ } do
+ yesterday = NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :day)
+
+ populate_stats(site, [
+ build(:pageview, timestamp: yesterday)
+ ])
+
+ another_user = new_user()
+
+ pending_site = new_site(owner: another_user)
+ add_guest(pending_site, role: :editor)
+ add_guest(pending_site, role: :viewer)
+ add_guest(pending_site, role: :viewer)
+
+ populate_stats(pending_site, [
+ build(:pageview, timestamp: yesterday)
+ ])
+
+ invite_transfer(pending_site, user, inviter: another_user)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ check_notice_titles(doc, [Billing.pending_site_ownerships_notice_title()])
+ assert doc =~ "Your account has been invited to become the owner of a site"
+
+ assert text_of_element(doc, @growth_plan_tooltip) ==
+ "Your usage exceeds the following limit(s): Team member limit"
+
+ assert doc =~ "2"
+ assert doc =~ "billable pageviews in the last billing cycle"
+ end
+
+ test "warns about losing access to a feature used by a pending ownership site and recommends business tier",
+ %{
+ conn: conn,
+ user: user
+ } do
+ another_user = new_user()
+ pending_site = new_site(owner: another_user)
+
+ Plausible.Props.allow(pending_site, ["author"])
+
+ invite_transfer(pending_site, user, inviter: another_user)
+
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ assert doc =~ "Your account has been invited to become the owner of a site"
+
+ assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~
+ "if (confirm(\"This plan does not support Custom Properties, which you have been using. By subscribing to this plan, you will not have access to this feature.\")) {window.location ="
+
+ assert text_of_element(doc, @business_highlight_pill) == "Recommended"
+ refute element_exists?(doc, @growth_highlight_pill)
+ end
+
+ test "gets default selected interval from current subscription plan", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+ assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
+ end
+
+ test "sets pageview slider according to last cycle usage", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+ assert text_of_element(doc, @slider_value) == "10k"
+ end
+
+ test "pageview slider changes selected volume", %{conn: conn} do
+ {:ok, lv, _doc} = get_liveview(conn)
+
+ doc = set_slider(lv, "100k")
+ assert text_of_element(doc, @slider_value) == "100k"
+
+ doc = set_slider(lv, "10k")
+ assert text_of_element(doc, @slider_value) == "10k"
+ end
+
+ test "makes it clear that the user is currently on a growth tier", %{conn: conn} do
+ {:ok, _lv, doc} = get_liveview(conn)
+
+ class = class_of_element(doc, @growth_plan_box)
+
+ assert class =~ "ring-2"
+ assert class =~ "ring-indigo-600"
+ assert text_of_element(doc, @growth_highlight_pill) == "Current"
+ end
+
+ test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do
+ {:ok, lv, _doc} = get_liveview(conn)
+
+ doc = set_slider(lv, "200k")
+
+ assert text_of_element(doc, @growth_checkout_button) == "Currently on this plan"
+ assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
+ assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
+
+ doc = element(lv, @monthly_interval_button) |> render_click()
+
+ assert text_of_element(doc, @growth_checkout_button) == "Change billing interval"
+ assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
+
+ doc = set_slider(lv, "1M")
+
+ assert text_of_element(doc, @growth_checkout_button) == "Upgrade"
+ assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
+
+ doc = set_slider(lv, "100k")
+
+ assert text_of_element(doc, @growth_checkout_button) == "Downgrade"
+ assert text_of_element(doc, @business_checkout_button) == "Upgrade to Business"
+ end
+
+ test "checkout buttons are dynamic links to /billing/change-plan/preview/