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""" + + """ + end + + @doc """ + This function takes a growth plan and returns a list representing + the different benefits a user gets when subscribing to this plan. + """ + def for_growth(plan) do + [ + team_member_limit_benefit(plan), + site_limit_benefit(plan), + data_retention_benefit(plan), + "Intuitive, fast and privacy-friendly dashboard", + "Email/Slack reports", + "Google Analytics import" + ] + |> Kernel.++(feature_benefits(plan)) + |> Kernel.++(["Saved Segments"]) + |> Enum.filter(& &1) + end + + @doc """ + Returns Business benefits for the given Business plan. + + A second argument is also required - list of Growth benefits. This + is because we don't want to list the same benefits in both Growth + and Business. Everything in Growth is also included in Business. + """ + def for_business(plan, growth_benefits) do + [ + "Everything in Growth", + team_member_limit_benefit(plan), + site_limit_benefit(plan), + data_retention_benefit(plan) + ] + |> Kernel.++(feature_benefits(plan)) + |> Kernel.--(growth_benefits) + |> Kernel.++(["Priority support"]) + |> Enum.filter(& &1) + end + + @doc """ + This function only takes a list of business benefits. Since all + limits and features of enterprise plans are configurable, we can + say on the upgrade page that enterprise plans include everything + in Business. + """ + def for_enterprise(business_benefits) do + team_members = + if "Up to 10 team members" in business_benefits, do: "10+ team members" + + data_retention = + if "5 years of data retention" in business_benefits, do: "5+ years of data retention" + + [ + "Everything in Business", + team_members, + "50+ sites", + "600+ Stats API requests per hour", + &sites_api_benefit/1, + data_retention, + "Technical onboarding" + ] + |> Enum.filter(& &1) + end + + defp data_retention_benefit(%Plan{} = plan) do + if plan.data_retention_in_years, do: "#{plan.data_retention_in_years} years of data retention" + end + + defp team_member_limit_benefit(%Plan{} = plan) do + case plan.team_member_limit do + :unlimited -> "Unlimited team members" + number -> "Up to #{number} team members" + end + end + + defp site_limit_benefit(%Plan{} = plan), do: "Up to #{plan.site_limit} sites" + + defp feature_benefits(%Plan{} = plan) do + Enum.flat_map(plan.features, fn feature_mod -> + case feature_mod.name() do + :goals -> ["Goals and custom events"] + :teams -> [] + :shared_links -> [] + :stats_api -> ["Stats API (600 requests per hour)", "Looker Studio Connector"] + :revenue_goals -> ["Ecommerce revenue attribution"] + _ -> [feature_mod.display_name()] + end + end) + end + + defp sites_api_benefit(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""" +
+
+

+ {String.capitalize(to_string(@kind))} +

+ <.pill :if={@highlight} text={@highlight} /> +
+
+ <.render_price_info available={@available} {assigns} /> + <%= if @available do %> + <.checkout id={"#{@kind}-checkout"} {assigns} /> + <% else %> + <.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" /> + <% end %> +
+ <%= if @owned && @kind == :growth && @plan_to_render.generation < 4 do %> + + <% else %> + + <% end %> +
+ """ + end + + def enterprise(assigns) do + ~H""" +
+
+

+ Enterprise +

+ + Recommended + +
+

+ + Custom + +

+

+ <.contact_button class="" /> + +
+ """ + end + + defp pill(assigns) do + ~H""" +
+

+ {@text} +

+
+ """ + end + + defp render_price_info(%{available: false} = assigns) do + ~H""" +

+ + 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 %> + + Upgrade + + <% end %> + <.tooltip :if={@exceeded_plan_limits != [] && @disabled_message}> +
+ {@disabled_message} +
+ <:tooltip_content> + Your usage exceeds the following limit(s):

+

+ {Phoenix.Naming.humanize(limit)}
+

+ + +
+ {@disabled_message} +
+ """ + end + + defp check_usage_within_plan_limits(%{available: false}) do + {:error, :plan_unavailable} + end + + defp check_usage_within_plan_limits(%{ + available: true, + usage: usage, + current_team: current_team, + plan_to_render: plan + }) do + # At this point, the user is *not guaranteed* to have a team, + # with ongoing trial. + trial_active_or_ended_recently? = + not is_nil(current_team) and not is_nil(current_team.trial_expiry_date) and + Plausible.Teams.trial_days_left(current_team) >= -10 + + limit_checking_opts = + cond do + current_team && current_team.allow_next_upgrade_override -> + [ignore_pageview_limit: true] + + trial_active_or_ended_recently? && plan.volume == "10k" -> + [pageview_allowance_margin: 0.3] + + trial_active_or_ended_recently? -> + [pageview_allowance_margin: 0.15] + + true -> + [] + end + + Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts) + end + + defp get_paddle_product_id(%Plan{monthly_product_id: plan_id}, :monthly), do: plan_id + defp get_paddle_product_id(%Plan{yearly_product_id: plan_id}, :yearly), do: plan_id + + defp change_plan_link_text( + %{ + owned_plan: %Plan{kind: from_kind, monthly_pageview_limit: from_volume}, + plan_to_render: %Plan{kind: to_kind, monthly_pageview_limit: to_volume}, + current_interval: from_interval, + selected_interval: to_interval + } = _assigns + ) do + cond do + from_kind == :business && to_kind == :growth -> + "Downgrade to Growth" + + from_kind == :growth && to_kind == :business -> + "Upgrade to Business" + + from_volume == to_volume && from_interval == to_interval -> + "Currently on this plan" + + from_volume == to_volume -> + "Change billing interval" + + from_volume > to_volume -> + "Downgrade" + + true -> + "Upgrade" + end + end + + defp change_plan_link_text(_), do: nil + + defp change_plan_link(assigns) do + confirmed = + if assigns.confirm_message, do: "confirm(\"#{assigns.confirm_message}\")", else: "true" + + assigns = assign(assigns, :confirmed, confirmed) + + ~H""" + + """ + end + + defp losing_features_message(:ok), do: nil + + defp losing_features_message({:error, {:unavailable_features, features}}) do + features_list_str = + features + |> Enum.map(fn feature_mod -> feature_mod.display_name() end) + |> PlausibleWeb.TextHelpers.pretty_join() + + "This plan does not support #{features_list_str}, which you have been using. By subscribing to this plan, you will not have access to #{if length(features) == 1, do: "this feature", else: "these features"}." + end + + defp contact_button(assigns) do + ~H""" + <.link + href="https://plausible.io/contact" + class={[ + "mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 bg-gray-800 hover:bg-gray-700 text-white dark:bg-indigo-600 dark:hover:bg-indigo-500", + @class + ]} + > + Contact us + + """ + end +end diff --git a/lib/plausible_web/components/billing/notice.ex b/lib/plausible_web/components/billing/notice.ex index 408e97bdcd30..2c8ecb8be7f4 100644 --- a/lib/plausible_web/components/billing/notice.ex +++ b/lib/plausible_web/components/billing/notice.ex @@ -264,8 +264,8 @@ defmodule PlausibleWeb.Components.Billing.Notice do def growth_grandfathered(assigns) do ~H""" -
- Your subscription has been grandfathered in at the same rate and terms as when you first joined. If you don't need the "Business" features, you're welcome to stay on this plan. You can adjust the pageview limit or change the billing frequency of this grandfathered plan. If you're interested in business features, you can upgrade to the new "Business" plan. +
+ Your subscription has been grandfathered in at the same rate and terms as when you first joined. If you don't need the "Business" features, you're welcome to stay on this plan. You can adjust the pageview limit or change the billing frequency of this grandfathered plan. If you're interested in business features, you can upgrade to a "Business" plan.
""" end diff --git a/lib/plausible_web/components/billing/plan_benefits.ex b/lib/plausible_web/components/billing/plan_benefits.ex index eedd191a29b0..0a4f6847cc30 100644 --- a/lib/plausible_web/components/billing/plan_benefits.ex +++ b/lib/plausible_web/components/billing/plan_benefits.ex @@ -13,7 +13,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do @doc """ This function takes a list of benefits returned by either one of: - * `for_growth/1` + * `for_starter/1` + * `for_growth/2` * `for_business/2` * `for_enterprise/1`. @@ -25,9 +26,9 @@ defmodule PlausibleWeb.Components.Billing.PlanBenefits do """ def render(assigns) do ~H""" -
@@ -106,8 +114,8 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do defp render_price_info(%{available: false} = assigns) do ~H""" -

- +

+ Custom

@@ -117,56 +125,100 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do 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 - + <.price_tag kind={@kind} selected_interval={@selected_interval} plan_to_render={@plan_to_render} /> +
+ + VAT + +
""" end defp price_tag(%{selected_interval: :monthly} = assigns) do + monthly_cost = + case assigns.plan_to_render do + %{monthly_cost: nil} -> "N/A" + %{monthly_cost: monthly_cost} -> Plausible.Billing.format_price(monthly_cost) + end + + assigns = assign(assigns, :monthly_cost, monthly_cost) + ~H""" - - {@plan_to_render.monthly_cost |> Plausible.Billing.format_price()} - - - /month - +

+ + {@monthly_cost} + + + /month + +

""" end defp price_tag(%{selected_interval: :yearly} = assigns) do + monthly_cost = + case assigns.plan_to_render do + %{monthly_cost: nil} -> "N/A" + %{monthly_cost: monthly_cost} -> Plausible.Billing.format_price(monthly_cost) + end + + {yearly_cost, monthly_cost_with_discount} = + case assigns.plan_to_render do + %{yearly_cost: nil} -> + {"N/A", "N/A"} + + %{yearly_cost: yearly_cost} -> + { + Plausible.Billing.format_price(yearly_cost), + Plausible.Billing.format_price(Money.div!(yearly_cost, 12)) + } + end + + assigns = + assigns + |> assign(:monthly_cost, monthly_cost) + |> assign(:yearly_cost, yearly_cost) + |> assign(:monthly_cost_with_discount, monthly_cost_with_discount) + ~H""" - - {@plan_to_render.monthly_cost |> Money.mult!(12) |> Plausible.Billing.format_price()} - - - {@plan_to_render.yearly_cost |> Plausible.Billing.format_price()} - - - /year - +
+ + {@yearly_cost} + + + + /year + + +
+ + {@monthly_cost} + + + {@monthly_cost_with_discount} + +
+ + + /month + +
""" end @@ -237,7 +289,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do <% end %> <.tooltip :if={@exceeded_plan_limits != [] && @disabled_message}> -
+
{@disabled_message}
@@ -303,10 +355,16 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do } = _assigns ) do cond do + from_kind in [:growth, :business] && to_kind == :starter -> + "Downgrade to Starter" + from_kind == :business && to_kind == :growth -> "Downgrade to Growth" - from_kind == :growth && to_kind == :business -> + from_kind == :starter && to_kind == :growth -> + "Upgrade to Growth" + + from_kind in [:starter, :growth] && to_kind == :business -> "Upgrade to Business" from_volume == to_volume && from_interval == to_interval -> diff --git a/lib/plausible_web/components/generic.ex b/lib/plausible_web/components/generic.ex index 150708a7c320..978709f20622 100644 --- a/lib/plausible_web/components/generic.ex +++ b/lib/plausible_web/components/generic.ex @@ -483,6 +483,63 @@ defmodule PlausibleWeb.Components.Generic do """ end + slot :inner_block, required: true + + def accordion_menu(assigns) do + ~H""" +
+ {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""" +
+
+ +
+
+ {render_slot(@inner_block)} +
+
+ """ + end + attr(:rest, :global, include: ~w(fill stroke stroke-width)) attr(:name, :atom, required: true) attr(:outline, :boolean, default: true) diff --git a/lib/plausible_web/controllers/billing_controller.ex b/lib/plausible_web/controllers/billing_controller.ex index cec491d030bf..eb8f509933de 100644 --- a/lib/plausible_web/controllers/billing_controller.ex +++ b/lib/plausible_web/controllers/billing_controller.ex @@ -19,10 +19,19 @@ defmodule PlausibleWeb.BillingController do def choose_plan(conn, _params) do team = conn.assigns.current_team + {live_module, hide_header?} = + if FunWithFlags.enabled?(:starter_tier, for: conn.assigns.current_user) do + {PlausibleWeb.Live.ChoosePlan, true} + else + {PlausibleWeb.Live.LegacyChoosePlan, false} + end + if Plausible.Teams.Billing.enterprise_configured?(team) do redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan)) else render(conn, "choose_plan.html", + live_module: live_module, + hide_header?: hide_header?, disable_global_notices?: true, skip_plausible_tracking: true, connect_live_socket: true diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex index c822425f41a1..fd7919b74fa2 100644 --- a/lib/plausible_web/email.ex +++ b/lib/plausible_web/email.ex @@ -95,7 +95,7 @@ defmodule PlausibleWeb.Email do |> render("trial_one_week_reminder.html", user: user, team: team) end - def trial_upgrade_email(user, team, day, usage, suggested_plan) do + def trial_upgrade_email(user, team, day, usage, suggested_volume) do base_email() |> to(user) |> tag("trial-upgrade-email") @@ -106,7 +106,7 @@ defmodule PlausibleWeb.Email do day: day, custom_events: usage.custom_events, usage: usage.total, - suggested_plan: suggested_plan + suggested_volume: suggested_volume ) end @@ -157,7 +157,7 @@ defmodule PlausibleWeb.Email do }) end - def over_limit_email(user, team, usage, suggested_plan) do + def over_limit_email(user, team, usage, suggested_volume) do priority_email() |> to(user) |> tag("over-limit") @@ -166,7 +166,7 @@ defmodule PlausibleWeb.Email do user: user, team: team, usage: usage, - suggested_plan: suggested_plan + suggested_volume: suggested_volume }) end @@ -183,7 +183,7 @@ defmodule PlausibleWeb.Email do }) end - def dashboard_locked(user, team, usage, suggested_plan) do + def dashboard_locked(user, team, usage, suggested_volume) do priority_email() |> to(user) |> tag("dashboard-locked") @@ -192,7 +192,7 @@ defmodule PlausibleWeb.Email do user: user, team: team, usage: usage, - suggested_plan: suggested_plan + suggested_volume: suggested_volume }) end diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index 2a5af59e43b6..485fb4e283a3 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do alias PlausibleWeb.Components.Billing.{PlanBox, PlanBenefits, Notice, PageviewSlider} alias Plausible.Billing.{Plans, Quota} + alias PlausibleWeb.Router.Helpers, as: Routes @contact_link "https://plausible.io/contact" @billing_faq_link "https://plausible.io/docs/billing" @@ -47,9 +48,17 @@ defmodule PlausibleWeb.Live.ChoosePlan do available_plans: available_plans, owned_tier: owned_tier } -> + highest_starter_plan = List.last(available_plans.starter) highest_growth_plan = List.last(available_plans.growth) highest_business_plan = List.last(available_plans.business) - Quota.suggest_tier(usage, highest_growth_plan, highest_business_plan, owned_tier) + + Quota.suggest_tier( + usage, + highest_starter_plan, + highest_growth_plan, + highest_business_plan, + owned_tier + ) end) |> assign_new(:available_volumes, fn %{available_plans: available_plans} -> get_available_volumes(available_plans) @@ -61,7 +70,13 @@ defmodule PlausibleWeb.Live.ChoosePlan do default_selected_volume(usage.monthly_pageviews, available_volumes) end) |> assign_new(:selected_interval, fn %{current_interval: current_interval} -> - current_interval || :monthly + current_interval || :yearly + end) + |> assign_new(:selected_starter_plan, fn %{ + available_plans: available_plans, + selected_volume: selected_volume + } -> + get_plan_by_volume(available_plans.starter, selected_volume) end) |> assign_new(:selected_growth_plan, fn %{ available_plans: available_plans, @@ -80,24 +95,32 @@ defmodule PlausibleWeb.Live.ChoosePlan do end def render(assigns) do + starter_plan_to_render = + assigns.selected_starter_plan || List.last(assigns.available_plans.starter) + growth_plan_to_render = assigns.selected_growth_plan || List.last(assigns.available_plans.growth) business_plan_to_render = assigns.selected_business_plan || List.last(assigns.available_plans.business) + starter_benefits = + PlanBenefits.for_starter(starter_plan_to_render) + growth_benefits = - PlanBenefits.for_growth(growth_plan_to_render) + PlanBenefits.for_growth(growth_plan_to_render, starter_benefits) business_benefits = - PlanBenefits.for_business(business_plan_to_render, growth_benefits) + PlanBenefits.for_business(business_plan_to_render, growth_benefits, starter_benefits) enterprise_benefits = PlanBenefits.for_enterprise(business_benefits) assigns = assigns + |> assign(:starter_plan_to_render, starter_plan_to_render) |> assign(:growth_plan_to_render, growth_plan_to_render) |> assign(:business_plan_to_render, business_plan_to_render) + |> assign(:starter_benefits, starter_benefits) |> assign(:growth_benefits, growth_benefits) |> assign(:business_benefits, business_benefits) |> assign(:enterprise_benefits, enterprise_benefits) @@ -112,21 +135,51 @@ defmodule PlausibleWeb.Live.ChoosePlan do -
-

- {if @owned_plan, - do: "Change subscription plan", - else: "Upgrade your account"} -

+ +
+ +
+

+ Traffic based plans that match your growth +

+

+ {if @owned_plan, + do: "Change your subscription plan", + else: "Upgrade your trial to a paid plan"} +

+
+
+ -
+
<.interval_picker selected_interval={@selected_interval} />
-
+
+
-

- <.render_usage pageview_usage={@usage.monthly_pageviews} /> -

- <.pageview_limit_notice :if={!@owned_plan} /> +
+ <.accordion_menu> + <.accordion_item + open_by_default={true} + id="usage" + title="What's my current usage?" + title_class="text-gray-900 dark:text-gray-200" + > +

+ <.render_usage pageview_usage={@usage.monthly_pageviews} /> +

+ + + <.accordion_item + id="over-limit" + title="What happens if I go over my monthly pageview limit?" + title_class="text-gray-900 dark:text-gray-200" + > +

+ 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. +

+ + +
<.help_links />
@@ -162,19 +235,23 @@ defmodule PlausibleWeb.Live.ChoosePlan do end defp render_usage(assigns) do - case assigns.pageview_usage do - %{last_30_days: _} -> - ~H""" - You have used - <%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %> billable pageviews in the last 30 days - """ - - %{last_cycle: _} -> - ~H""" - You have used - <%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total) %> billable pageviews in the last billing cycle - """ - end + ~H""" + You have used + + <%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %> billable pageviews in the last 30 days. + + + {PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total)} + billable pageviews in the last billing cycle. + + Please see your full usage report (including sites and team members) under the + + "Subscription" section + in your account settings. + """ end def handle_event("set_interval", %{"interval" => interval}, socket) do @@ -201,6 +278,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do {:noreply, assign(socket, selected_volume: new_volume, + selected_starter_plan: get_plan_by_volume(available_plans.starter, new_volume), selected_growth_plan: get_plan_by_volume(available_plans.growth, new_volume), selected_business_plan: get_plan_by_volume(available_plans.business, new_volume) )} @@ -266,30 +344,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do """ end - defp pageview_limit_notice(assigns) do - ~H""" -
-
-

- - What happens if I go over my page views limit? - -

-
-
-
- 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 page views 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. -
-
-
- """ - end - defp help_links(assigns) do ~H""" -
- Questions? Contact us - or see billing FAQ +
+ Any other questions? + Contact us + or see billing FAQ
""" end diff --git a/lib/plausible_web/live/legacy_choose_plan.ex b/lib/plausible_web/live/legacy_choose_plan.ex new file mode 100644 index 000000000000..69d292b09d48 --- /dev/null +++ b/lib/plausible_web/live/legacy_choose_plan.ex @@ -0,0 +1,321 @@ +defmodule PlausibleWeb.Live.LegacyChoosePlan do + @moduledoc """ + [DEPRECATED] This file is essentially a copy of + `PlausibleWeb.Live.ChoosePlan` with the + intent of keeping the old behaviour in place for the users without + the `starter_tier` feature flag enabled. + """ + use PlausibleWeb, :live_view + + require Plausible.Billing.Subscription.Status + + alias PlausibleWeb.Components.Billing.{ + LegacyPlanBox, + LegacyPlanBenefits, + Notice, + PageviewSlider + } + + alias Plausible.Billing.{Plans, Quota} + + @contact_link "https://plausible.io/contact" + @billing_faq_link "https://plausible.io/docs/billing" + + def mount(_params, %{"remote_ip" => remote_ip}, socket) do + socket = + socket + |> assign_new(:pending_ownership_site_ids, fn %{current_user: current_user} -> + Plausible.Teams.Memberships.all_pending_site_transfers(current_user.email) + end) + |> assign_new(:usage, fn %{ + current_team: current_team, + pending_ownership_site_ids: pending_ownership_site_ids + } -> + Plausible.Teams.Billing.quota_usage(current_team, + with_features: true, + pending_ownership_site_ids: pending_ownership_site_ids + ) + end) + |> assign_new(:subscription, fn %{current_team: current_team} -> + Plausible.Teams.Billing.get_subscription(current_team) + end) + |> assign_new(:owned_plan, fn %{subscription: subscription} -> + Plans.get_regular_plan(subscription, only_non_expired: true) + end) + |> assign_new(:owned_tier, fn %{owned_plan: owned_plan} -> + if owned_plan, do: Map.get(owned_plan, :kind), else: nil + end) + |> assign_new(:current_interval, fn %{subscription: subscription} -> + current_user_subscription_interval(subscription) + end) + |> assign_new(:available_plans, fn %{subscription: subscription} -> + Plans.available_plans_for(subscription, + with_prices: true, + customer_ip: remote_ip, + legacy?: true + ) + end) + |> assign_new(:recommended_tier, fn %{ + usage: usage, + available_plans: available_plans, + owned_tier: owned_tier + } -> + highest_growth_plan = List.last(available_plans.growth) + highest_business_plan = List.last(available_plans.business) + Quota.legacy_suggest_tier(usage, highest_growth_plan, highest_business_plan, owned_tier) + end) + |> assign_new(:available_volumes, fn %{available_plans: available_plans} -> + get_available_volumes(available_plans) + end) + |> assign_new(:selected_volume, fn %{ + usage: usage, + available_volumes: available_volumes + } -> + default_selected_volume(usage.monthly_pageviews, available_volumes) + end) + |> assign_new(:selected_interval, fn %{current_interval: current_interval} -> + current_interval || :monthly + end) + |> assign_new(:selected_growth_plan, fn %{ + available_plans: available_plans, + selected_volume: selected_volume + } -> + get_plan_by_volume(available_plans.growth, selected_volume) + end) + |> assign_new(:selected_business_plan, fn %{ + available_plans: available_plans, + selected_volume: selected_volume + } -> + get_plan_by_volume(available_plans.business, selected_volume) + end) + + {:ok, socket} + end + + def render(assigns) do + growth_plan_to_render = + assigns.selected_growth_plan || List.last(assigns.available_plans.growth) + + business_plan_to_render = + assigns.selected_business_plan || List.last(assigns.available_plans.business) + + growth_benefits = + LegacyPlanBenefits.for_growth(growth_plan_to_render) + + business_benefits = + LegacyPlanBenefits.for_business(business_plan_to_render, growth_benefits) + + enterprise_benefits = LegacyPlanBenefits.for_enterprise(business_benefits) + + assigns = + assigns + |> assign(:growth_plan_to_render, growth_plan_to_render) + |> assign(:business_plan_to_render, business_plan_to_render) + |> assign(:growth_benefits, growth_benefits) + |> assign(:business_benefits, business_benefits) + |> assign(:enterprise_benefits, enterprise_benefits) + + ~H""" +
+
+ + + + +
+

+ {if @owned_plan, + do: "Change subscription plan", + else: "Upgrade your account"} +

+
+
+ <.interval_picker selected_interval={@selected_interval} /> + +
+
+ + + +
+

+ <.render_usage pageview_usage={@usage.monthly_pageviews} /> +

+ <.pageview_limit_notice :if={!@owned_plan} /> + <.help_links /> +
+
+ + """ + end + + defp render_usage(assigns) do + case assigns.pageview_usage do + %{last_30_days: _} -> + ~H""" + You have used + <%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %> billable pageviews in the last 30 days + """ + + %{last_cycle: _} -> + ~H""" + You have used + <%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total) %> billable pageviews in the last billing cycle + """ + end + end + + def handle_event("set_interval", %{"interval" => interval}, socket) do + new_interval = + case interval do + "yearly" -> :yearly + "monthly" -> :monthly + end + + {:noreply, assign(socket, selected_interval: new_interval)} + end + + def handle_event("slide", %{"slider" => index}, socket) do + index = String.to_integer(index) + %{available_plans: available_plans, available_volumes: available_volumes} = socket.assigns + + new_volume = + if index == length(available_volumes) do + :enterprise + else + Enum.at(available_volumes, index) + end + + {:noreply, + assign(socket, + selected_volume: new_volume, + selected_growth_plan: get_plan_by_volume(available_plans.growth, new_volume), + selected_business_plan: get_plan_by_volume(available_plans.business, new_volume) + )} + end + + defp default_selected_volume(pageview_usage, available_volumes) do + total = + case pageview_usage do + %{last_30_days: usage} -> usage.total + %{last_cycle: usage} -> usage.total + end + + Enum.find(available_volumes, &(total < &1)) || :enterprise + end + + defp current_user_subscription_interval(subscription) do + case Plans.subscription_interval(subscription) do + "yearly" -> :yearly + "monthly" -> :monthly + _ -> nil + end + end + + defp get_plan_by_volume(_, :enterprise), do: nil + + defp get_plan_by_volume(plans, volume) do + Enum.find(plans, &(&1.monthly_pageview_limit == volume)) + end + + defp interval_picker(assigns) do + ~H""" +
+
+ <.two_months_free /> +
+ + +
+
+
+ """ + end + + def two_months_free(assigns) do + ~H""" + + 2 months free + + """ + end + + defp pageview_limit_notice(assigns) do + ~H""" +
+
+

+ + What happens if I go over my page views limit? + +

+
+
+
+ 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 page views 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. +
+
+
+ """ + end + + defp help_links(assigns) do + ~H""" +
+ Questions? Contact us + or see billing FAQ +
+ """ + end + + defp get_available_volumes(%{business: business_plans, growth: growth_plans}) do + growth_volumes = Enum.map(growth_plans, & &1.monthly_pageview_limit) + business_volumes = Enum.map(business_plans, & &1.monthly_pageview_limit) + + (growth_volumes ++ business_volumes) + |> Enum.uniq() + end + + defp contact_link(), do: @contact_link + + defp billing_faq_link(), do: @billing_faq_link +end diff --git a/lib/plausible_web/templates/billing/choose_plan.html.heex b/lib/plausible_web/templates/billing/choose_plan.html.heex index bac613786706..17b2b567360a 100644 --- a/lib/plausible_web/templates/billing/choose_plan.html.heex +++ b/lib/plausible_web/templates/billing/choose_plan.html.heex @@ -1,4 +1,4 @@ -{live_render(@conn, PlausibleWeb.Live.ChoosePlan, +{live_render(@conn, @live_module, id: "choose-plan", session: %{"remote_ip" => PlausibleWeb.RemoteIP.get(@conn)} )} diff --git a/lib/plausible_web/templates/email/dashboard_locked.html.heex b/lib/plausible_web/templates/email/dashboard_locked.html.heex index 373cbe74cffc..69869b309682 100644 --- a/lib/plausible_web/templates/email/dashboard_locked.html.heex +++ b/lib/plausible_web/templates/email/dashboard_locked.html.heex @@ -9,10 +9,10 @@ During the last billing cycle ({PlausibleWeb.TextHelpers.format_date_range( )}), the usage was {PlausibleWeb.AuthView.delimit_integer(@usage.penultimate_cycle.total)} billable pageviews. Note that billable pageviews include both standard pageviews and custom events. In your "?__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/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/", %{ + conn: conn + } do + {:ok, lv, doc} = get_liveview(conn) + + 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)}'}" + + set_slider(lv, "5M") + doc = element(lv, @monthly_interval_button) |> render_click() + + 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)}'}" + end + end + + describe "for a user with an active v4 business subscription plan" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in, :subscribe_v4_business] + + 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 "does not render any global notices", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "highlights Business box as the 'Current' tier", %{ + conn: conn, + site: site + } do + insert(:goal, site: site, currency: :USD, event_name: "Purchase") + + {:ok, _lv, doc} = get_liveview(conn) + + class = class_of_element(doc, @business_plan_box) + + assert class =~ "ring-2" + assert class =~ "ring-indigo-600" + assert text_of_element(doc, @business_highlight_pill) == "Current" + + refute element_exists?(doc, @growth_highlight_pill) + end + + test "recommends Enterprise when site limit exceeds Business tier due to pending ownerships", + %{ + conn: conn, + user: user + } do + team = team_of(user) + + for _ <- 1..49 do + new_site(owner: user) + end + + assert 50 = Plausible.Teams.Billing.quota_usage(team).sites + + 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, @enterprise_highlight_pill) == "Recommended" + refute element_exists?(doc, @business_highlight_pill) + refute element_exists?(doc, @growth_highlight_pill) + 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, "5M") + + 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, @growth_checkout_button) == "Downgrade to Growth" + + doc = set_slider(lv, "10M") + + assert text_of_element(doc, @business_checkout_button) == "Upgrade" + assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth" + + doc = set_slider(lv, "100k") + + assert text_of_element(doc, @business_checkout_button) == "Downgrade" + assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth" + end + + test "checkout is disabled when team member usage exceeds rendered plan limit", %{ + conn: conn, + user: user + } do + site = new_site(owner: user) + for _ <- 1..4, do: add_guest(site, role: :viewer) + + {:ok, _lv, doc} = get_liveview(conn) + + assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan" + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + + assert text_of_element(doc, @growth_plan_tooltip) == + "Your usage exceeds the following limit(s): Team member limit" + end + + test "checkout is disabled when sites usage exceeds rendered plan limit", %{ + conn: conn, + user: user + } do + for _ <- 1..11, do: new_site(owner: user) + + {:ok, _lv, doc} = get_liveview(conn) + + assert text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan" + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + + assert text_of_element(doc, @growth_plan_tooltip) == + "Your usage exceeds the following limit(s): Site limit" + end + + test "when more than one limit is exceeded, the tooltip enumerates them", %{ + conn: conn, + user: user + } do + for _ <- 1..11, do: new_site(owner: user) + + site = new_site(owner: user) + for _ <- 1..4, do: add_guest(site, role: :viewer) + + {:ok, _lv, doc} = get_liveview(conn) + + assert text_of_element(doc, @growth_plan_tooltip) =~ "Team member limit" + assert text_of_element(doc, @growth_plan_tooltip) =~ "Site limit" + end + + test "checkout is not disabled when pageview usage exceeded but next upgrade allowed by override", + %{ + conn: conn, + user: user, + site: site + } do + now = NaiveDateTime.utc_now() + + generate_usage_for(site, 11_000, Timex.shift(now, days: -5)) + generate_usage_for(site, 11_000, Timex.shift(now, days: -35)) + + user + |> team_of() + |> Ecto.Changeset.change(allow_next_upgrade_override: true) + |> Plausible.Repo.update!() + + {:ok, lv, _doc} = get_liveview(conn) + + doc = set_slider(lv, "10k") + + refute text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan" + refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + end + + test "warns about losing access to a feature", %{conn: conn, user: user, site: site} do + Plausible.Props.allow(site, ["author"]) + insert(:goal, currency: :USD, site: site, event_name: "Purchase") + insert(:api_key, user: user) + + {: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, Revenue Goals and Stats API, which you have been using. By subscribing to this plan, you will not have access to these features.\")) {window.location = " + end + end + + describe "for a user with a v3 business (unlimited team members) subscription plan" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in] + + setup %{user: user} = context do + create_subscription_for(user, paddle_plan_id: @v3_business_10k_monthly_plan_id) + {:ok, context} + 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 =~ "Unlimited 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 =~ "50+ sites" + assert enterprise_box =~ "600+ Stats API requests per hour" + assert enterprise_box =~ "Sites API access for" + assert enterprise_box =~ "Technical onboarding" + + refute enterprise_box =~ "team members" + + assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~ + "https://plausible.io/white-label-web-analytics" + end + end + + describe "for a user with a past_due subscription" do + setup [ + :create_user, + :disable_starter_tier, + :create_site, + :log_in, + :create_past_due_subscription + ] + + test "renders failed payment notice and link to update billing details", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, [Billing.subscription_past_due_notice_title()]) + assert doc =~ "There was a problem with your latest payment" + assert doc =~ "https://update.billing.details" + end + + test "checkout buttons are disabled + notice about billing details (unless plan owned already)", + %{conn: conn} do + {:ok, lv, _doc} = get_liveview(conn) + + doc = set_slider(lv, "200k") + + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400" + assert text_of_element(doc, @growth_checkout_button) =~ "Currently on this plan" + refute element_exists?(doc, "#{@growth_checkout_button} + div") + + assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none bg-gray-400" + + assert text_of_element(doc, "#{@business_checkout_button} + div") =~ + "Please update your billing details first" + + doc = set_slider(lv, "1M") + + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400" + + assert text_of_element(doc, "#{@growth_checkout_button} + div") =~ + "Please update your billing details first" + end + end + + describe "for a user with a paused subscription" do + setup [ + :create_user, + :disable_starter_tier, + :create_site, + :log_in, + :create_paused_subscription + ] + + test "renders subscription paused notice and link to update billing details", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, [Billing.subscription_paused_notice_title()]) + assert doc =~ "Your subscription is paused due to failed payments" + assert doc =~ "https://update.billing.details" + end + + test "checkout buttons are disabled + notice about billing details when plan not owned already", + %{conn: conn} do + {:ok, lv, _doc} = get_liveview(conn) + + doc = set_slider(lv, "200k") + + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400" + assert text_of_element(doc, @growth_checkout_button) =~ "Currently on this plan" + refute element_exists?(doc, "#{@growth_checkout_button} + div") + + assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none bg-gray-400" + + assert text_of_element(doc, "#{@business_checkout_button} + div") =~ + "Please update your billing details first" + + doc = set_slider(lv, "1M") + + assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400" + + assert text_of_element(doc, "#{@growth_checkout_button} + div") =~ + "Please update your billing details first" + end + end + + describe "for a user with a cancelled, but still active subscription" do + setup [ + :create_user, + :disable_starter_tier, + :create_site, + :log_in, + :create_cancelled_subscription + ] + + test "does not render any global notices", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "checkout buttons are paddle buttons", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ "Paddle.Checkout.open" + + assert text_of_attr(find(doc, @business_checkout_button), "onclick") =~ + "Paddle.Checkout.open" + end + + test "currently owned tier is highlighted", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + assert text_of_element(doc, @growth_highlight_pill) == "Current" + end + + test "can subscribe again to the currently owned (but cancelled) plan", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none" + end + end + + describe "for a user with a cancelled and expired subscription" do + setup [ + :create_user, + :disable_starter_tier, + :create_site, + :log_in, + :create_cancelled_subscription + ] + + setup %{user: user} do + user + |> team_of() + |> Repo.preload(:subscription) + |> Map.fetch!(:subscription) + |> Subscription.changeset(%{next_bill_date: Timex.shift(Timex.now(), months: -2)}) + |> Repo.update!() + + :ok + end + + test "does not render any global notices", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "highlights recommended tier", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + assert text_of_element(doc, @growth_highlight_pill) == "Recommended" + refute text_of_element(doc, @business_highlight_pill) == "Recommended" + end + end + + describe "for a grandfathered user with a high volume plan" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in] + + test "does not render any global notices", %{conn: conn, user: user} do + create_subscription_for(user, paddle_plan_id: @v1_50m_yearly_plan_id) + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "on a 50M v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not", + %{conn: conn, user: user} do + create_subscription_for(user, paddle_plan_id: @v1_50m_yearly_plan_id) + + {:ok, lv, _doc} = get_liveview(conn) + + 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) == "€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, @business_plan_box) =~ "Contact us" + 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, @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, @business_plan_box) =~ "Contact us" + refute text_of_element(doc, @growth_plan_box) =~ "Contact us" + end + + test "on a 20M v2 plan, Growth tiers are available at 20M and 20M+, but not 50M", + %{conn: conn, user: user} do + create_subscription_for(user, paddle_plan_id: @v2_20m_yearly_plan_id) + + {:ok, lv, _doc} = get_liveview(conn) + + 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) == "€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 + + describe "for a grandfathered user on a v1 10k plan" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in] + + setup %{user: user} = context do + create_subscription_for(user, paddle_plan_id: @v1_10k_yearly_plan_id) + {:ok, context} + end + + test "does not render any global notices", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "v1 20M and 50M Growth plans are not available", + %{conn: conn} do + {:ok, lv, _doc} = get_liveview(conn) + + doc = set_slider(lv, 8) + assert text_of_element(doc, @slider_value) == "10M+" + assert text_of_element(doc, @growth_plan_box) =~ "Contact us" + end + + test "displays grandfathering notice in the Growth box instead of benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + growth_box = text_of_element(doc, @growth_plan_box) + assert growth_box =~ "Your subscription has been grandfathered" + refute growth_box =~ "Intuitive, fast and privacy-friendly dashboard" + end + + test "displays business and enterprise plan benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + business_box = text_of_element(doc, @business_plan_box) + enterprise_box = text_of_element(doc, @enterprise_plan_box) + + assert business_box =~ "Everything in Growth" + assert business_box =~ "Funnels" + assert business_box =~ "Ecommerce revenue attribution" + assert business_box =~ "Priority support" + + refute business_box =~ "Goals and custom events" + refute business_box =~ "Unlimited team members" + refute business_box =~ "Up to 50 sites" + refute business_box =~ "Stats API (600 requests per hour)" + refute business_box =~ "Looker Studio Connector" + refute business_box =~ "Custom Properties" + + assert enterprise_box =~ "Everything in Business" + 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" + + refute enterprise_box =~ "10+ team members" + refute enterprise_box =~ "Unlimited team members" + end + end + + describe "for a user without a trial_expiry_date (invited user) who owns a site (transferred)" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in] + + setup %{user: user} do + user + |> team_of() + |> Ecto.Changeset.change(trial_expiry_date: nil) + |> Repo.update!() + + :ok + end + + test "does not render any global notices", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, []) + end + + test "allows to upgrade", %{conn: conn} do + {: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" + end + end + + describe "for a free_10k subscription" do + setup [:create_user, :disable_starter_tier, :create_site, :log_in, :subscribe_free_10k] + + test "recommends growth tier when no premium features used", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + assert element_exists?(doc, @growth_highlight_pill) + refute element_exists?(doc, @business_highlight_pill) + end + + test "recommends Business tier when premium features used", %{conn: conn, site: site} do + insert(:goal, currency: :USD, site: site, event_name: "Purchase") + + {:ok, _lv, doc} = get_liveview(conn) + + assert text_of_element(doc, @business_plan_box) =~ "Recommended" + refute text_of_element(doc, @growth_plan_box) =~ "Recommended" + end + + test "renders Paddle upgrade buttons", %{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)) + end + end + + describe "for a user with no sites" do + setup [:create_user, :disable_starter_tier, :log_in] + + test "does not allow to subscribe and renders notice", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + 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, @growth_checkout_button) =~ "pointer-events-none" + assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none" + end + end + + describe "for a user with no sites but pending ownership transfer" do + setup [:create_user, :disable_starter_tier, :log_in] + + setup %{user: user} do + old_owner = new_user() + site = new_site(owner: old_owner) + invite_transfer(site, user, inviter: old_owner) + + :ok + end + + test "renders only the pending ownership transfer notice", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + check_notice_titles(doc, [Billing.pending_site_ownerships_notice_title()]) + end + + test "allows to subscribe", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + 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 + + # Checks the given HTML document for the presence of all possible billing + # notices. For those expected, we assert that only one is present. Others + # should not appear in the document. + defp check_notice_titles(doc, expected) do + [ + Billing.dashboard_locked_notice_title(), + Billing.active_grace_period_notice_title(), + Billing.subscription_cancelled_notice_title(), + Billing.subscription_past_due_notice_title(), + Billing.subscription_paused_notice_title(), + Billing.upgrade_ineligible_notice_title(), + Billing.pending_site_ownerships_notice_title() + ] + |> Enum.each(fn title -> + if title in expected do + assert length(String.split(doc, title)) == 2 + else + refute doc =~ title + end + end) + end + + defp subscribe_v4_growth(%{user: user}) do + create_subscription_for(user, paddle_plan_id: @v4_growth_200k_yearly_plan_id) + end + + defp subscribe_v4_business(%{user: user}) do + create_subscription_for(user, paddle_plan_id: @v4_business_5m_monthly_plan_id) + end + + defp create_past_due_subscription(%{user: user}) do + create_subscription_for(user, + paddle_plan_id: @v4_growth_200k_yearly_plan_id, + status: Subscription.Status.past_due(), + update_url: "https://update.billing.details" + ) + end + + defp create_paused_subscription(%{user: user}) do + create_subscription_for(user, + paddle_plan_id: @v4_growth_200k_yearly_plan_id, + status: Subscription.Status.paused(), + update_url: "https://update.billing.details" + ) + end + + defp create_cancelled_subscription(%{user: user}) do + create_subscription_for(user, + paddle_plan_id: @v4_growth_200k_yearly_plan_id, + status: Subscription.Status.deleted() + ) + end + + defp create_subscription_for(user, subscription_opts) do + {paddle_plan_id, subscription_opts} = Keyword.pop(subscription_opts, :paddle_plan_id) + + user = + subscribe_to_plan(user, paddle_plan_id, subscription_opts) + + {:ok, user: user} + end + + defp subscribe_free_10k(%{user: user}) do + user = subscribe_to_plan(user, "free_10k") + {:ok, user: user} + end + + defp get_liveview(conn) do + conn = assign(conn, :live_module, PlausibleWeb.Live.ChoosePlan) + {:ok, _lv, _doc} = live(conn, Routes.billing_path(conn, :choose_plan)) + end + + defp get_paddle_checkout_params(element) do + with onclick <- text_of_attr(element, "onclick"), + [[_, checkout_params_str]] <- Regex.scan(~r/Paddle\.Checkout\.open\((.*?)\)/, onclick), + {:ok, checkout_params} <- Jason.decode(checkout_params_str) do + checkout_params + end + end + + defp set_slider(lv, volume) when is_binary(volume) do + index = Enum.find_index(@slider_volumes, &(&1 == volume)) + set_slider(lv, index) + end + + defp set_slider(lv, index) do + lv + |> element(@slider_input) + |> render_change(%{slider: index}) + end + + defp disable_starter_tier(%{user: user}) do + FunWithFlags.disable(:starter_tier, for_actor: user) + :ok + end +end diff --git a/test/support/dev/billing/dev_paddle_api_mock.ex b/test/support/dev/billing/dev_paddle_api_mock.ex index 75d1c2746ba3..fab1b154f266 100644 --- a/test/support/dev/billing/dev_paddle_api_mock.ex +++ b/test/support/dev/billing/dev_paddle_api_mock.ex @@ -13,6 +13,9 @@ defmodule Plausible.Billing.DevPaddleApiMock do @prices_file_path Application.app_dir(:plausible, ["priv", "plan_prices.json"]) @prices File.read!(@prices_file_path) |> Jason.decode!() + # https://hexdocs.pm/elixir/1.15/Module.html#module-external_resource + @external_resource @prices_file_path + def all_prices() do enterprise_plan_prices = Repo.all(from p in EnterprisePlan, select: {p.paddle_plan_id, 123}) diff --git a/test/support/paddle_api_mock.ex b/test/support/test_paddle_api_mock.ex similarity index 74% rename from test/support/paddle_api_mock.ex rename to test/support/test_paddle_api_mock.ex index b0aa46333264..bd7f5de446fa 100644 --- a/test/support/paddle_api_mock.ex +++ b/test/support/test_paddle_api_mock.ex @@ -1,4 +1,6 @@ -defmodule Plausible.PaddleApi.Mock do +defmodule Plausible.Billing.TestPaddleApiMock do + @moduledoc false + def get_subscription(_) do {:ok, %{ @@ -72,20 +74,7 @@ defmodule Plausible.PaddleApi.Mock do end end - # to give a reasonable testing structure for monthly and yearly plan - # prices, this function returns prices with the following logic: - # 10, 100, 20, 200, 30, 300, ...and so on. - def fetch_prices([_ | _] = product_ids, _customer_ip) do - {prices, _index} = - Enum.reduce(product_ids, {%{}, 1}, fn p, {acc, i} -> - price = - if rem(i, 2) == 1, - do: ceil(i / 2.0) * 10.0, - else: ceil(i / 2.0) * 100.0 - - {Map.put(acc, p, Money.from_float!(:EUR, price)), i + 1} - end) - - {:ok, prices} + def fetch_prices(product_ids, customer_ip) do + Plausible.Billing.DevPaddleApiMock.fetch_prices(product_ids, customer_ip) end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 9db91e446bad..50934ab7aa9e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -8,6 +8,7 @@ Application.ensure_all_started(:double) FunWithFlags.enable(:channels) FunWithFlags.enable(:scroll_depth) +FunWithFlags.enable(:starter_tier) Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual) diff --git a/test/workers/send_trial_notifications_test.exs b/test/workers/send_trial_notifications_test.exs index 772fc87f302e..06dcad0996ee 100644 --- a/test/workers/send_trial_notifications_test.exs +++ b/test/workers/send_trial_notifications_test.exs @@ -77,7 +77,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 1)) site = new_site(owner: user) usage = %{total: 3, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) populate_stats(site, [ build(:pageview), @@ -88,7 +88,13 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do perform_job(SendTrialNotifications, %{}) assert_delivered_email( - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "tomorrow", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email( + user, + site.team, + "tomorrow", + usage, + suggested_volume + ) ) end @@ -96,7 +102,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user(trial_expiry_date: Date.utc_today()) site = new_site(owner: user) usage = %{total: 3, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) populate_stats(site, [ build(:pageview), @@ -107,7 +113,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do perform_job(SendTrialNotifications, %{}) assert_delivered_email( - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) ) end @@ -115,10 +121,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user(trial_expiry_date: Date.utc_today()) site = new_site(owner: user) usage = %{total: 9_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "In the last month, your account has used 9,000 billable pageviews." @@ -128,10 +134,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user(trial_expiry_date: Date.utc_today()) site = new_site(owner: user) usage = %{total: 9_100, custom_events: 100} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "In the last month, your account has used 9,100 billable pageviews and custom events in total." @@ -175,10 +181,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 9_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 10k/mo plan." end @@ -187,10 +193,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 90_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 100k/mo plan." end @@ -199,10 +205,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 180_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 200k/mo plan." end @@ -211,10 +217,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 450_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 500k/mo plan." end @@ -223,10 +229,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 900_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 1M/mo plan." end @@ -235,10 +241,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 1_800_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 2M/mo plan." end @@ -247,10 +253,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 4_500_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 5M/mo plan." end @@ -259,10 +265,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 9_000_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "we recommend you select a 10M/mo plan." end @@ -271,10 +277,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = new_user() site = new_site(owner: user) usage = %{total: 20_000_000, custom_events: 0} - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "please reply back to this email to get a quote for your volume" end @@ -284,10 +290,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do site = new_site(owner: user) usage = %{total: 10_000, custom_events: 0} subscribe_to_enterprise_plan(user, paddle_plan_id: "enterprise-plan-id") - suggested_plan = Plausible.Billing.Plans.suggest(site.team, usage.total) + suggested_volume = Plausible.Billing.Plans.suggest_volume(site.team, usage.total) email = - PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_plan) + PlausibleWeb.Email.trial_upgrade_email(user, site.team, "today", usage, suggested_volume) assert email.html_body =~ "please reply back to this email to get a quote for your volume" end