diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex index 2f9065dea..bd7860fdf 100644 --- a/lib/plausible/billing/plans.ex +++ b/lib/plausible/billing/plans.ex @@ -1,7 +1,8 @@ defmodule Plausible.Billing.Plans do alias Plausible.Billing.Subscriptions use Plausible.Repo - alias Plausible.Billing.{Subscription, Plan, EnterprisePlan} + alias Plausible.Billing.{Quota, Subscription, Plan, EnterprisePlan} + alias Plausible.Billing.Feature.{StatsAPI, Props} alias Plausible.Auth.User for f <- [ @@ -26,6 +27,8 @@ defmodule Plausible.Billing.Plans do Module.put_attribute(__MODULE__, :external_resource, path) end + @business_tier_launch ~D[2023-11-07] + @spec growth_plans_for(User.t()) :: [Plan.t()] @doc """ Returns a list of growth plans available for the user to choose. @@ -63,10 +66,17 @@ defmodule Plausible.Billing.Plans do |> Enum.filter(&(&1.kind == :business)) end - def available_plans_with_prices(%User{} = user) do - (growth_plans_for(user) ++ business_plans_for(user)) - |> with_prices() - |> Enum.group_by(& &1.kind) + def available_plans_for(%User{} = user, opts \\ []) do + plans = growth_plans_for(user) ++ business_plans_for(user) + + plans = + if Keyword.get(opts, :with_prices) do + with_prices(plans) + else + plans + end + + Enum.group_by(plans, & &1.kind) end @spec yearly_product_ids() :: [String.t()] @@ -216,6 +226,21 @@ defmodule Plausible.Billing.Plans do Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit)) end + def suggest_tier(user) do + growth_features = + if Timex.before?(user.inserted_at, @business_tier_launch) do + [StatsAPI, Props] + else + [] + end + + if Enum.any?(Quota.features_usage(user), &(&1 not in growth_features)) do + :business + else + :growth + end + end + defp all() do @legacy_plans ++ @plans_v1 ++ @plans_v2 ++ @plans_v3 ++ @plans_v4 end diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index 55dda19d0..8c80ca4d8 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -28,11 +28,17 @@ defmodule PlausibleWeb.Live.ChoosePlan do |> assign_new(:owned_plan, fn %{user: %{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(:recommended_tier, fn %{owned_plan: owned_plan, user: user} -> + if owned_plan, do: nil, else: Plans.suggest_tier(user) + end) |> assign_new(:current_interval, fn %{user: user} -> current_user_subscription_interval(user.subscription) end) |> assign_new(:available_plans, fn %{user: user} -> - Plans.available_plans_with_prices(user) + Plans.available_plans_for(user, with_prices: true) end) |> assign_new(:available_volumes, fn %{available_plans: available_plans} -> get_available_volumes(available_plans) @@ -101,7 +107,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<.plan_box kind={:growth} - owned={@owned_plan && Map.get(@owned_plan, :kind) == :growth} + owned={@owned_tier == :growth} + recommended={@recommended_tier == :growth} plan_to_render={@growth_plan_to_render} benefits={@growth_benefits} available={!!@selected_growth_plan} @@ -109,7 +116,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do /> <.plan_box kind={:business} - owned={@owned_plan && Map.get(@owned_plan, :kind) == :business} + owned={@owned_tier == :business} + recommended={@recommended_tier == :business} plan_to_render={@business_plan_to_render} benefits={@business_benefits} available={!!@selected_business_plan} @@ -241,24 +249,33 @@ defmodule PlausibleWeb.Live.ChoosePlan do end defp plan_box(assigns) do + highlight = + cond do + assigns.owned -> "Current" + assigns.recommended -> "Recommended" + true -> nil + end + + assigns = assign(assigns, :highlight, highlight) + ~H"""

<%= String.capitalize(to_string(@kind)) %>

- <.current_label :if={@owned} /> + <.pill :if={@highlight} text={@highlight} />
<.render_price_info available={@available} {assigns} /> @@ -444,14 +461,14 @@ defmodule PlausibleWeb.Live.ChoosePlan do """ end - defp current_label(assigns) do + defp pill(assigns) do ~H"""

- Current + <%= @text %>

""" diff --git a/lib/plausible_web/templates/email/dashboard_locked.html.eex b/lib/plausible_web/templates/email/dashboard_locked.html.eex index 3e169056e..b54e7da4a 100644 --- a/lib/plausible_web/templates/email/dashboard_locked.html.eex +++ b/lib/plausible_web/templates/email/dashboard_locked.html.eex @@ -6,7 +6,7 @@ In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_for <%= if @suggested_plan == :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 the <%= @suggested_plan.volume %>/mo plan. +Based on that we recommend you select a <%= @suggested_plan.volume %>/mo plan.

Click here to go to your account settings. You can upgrade your subscription tier by clicking the 'Change plan' link. <% end %> diff --git a/lib/plausible_web/templates/email/over_limit.html.eex b/lib/plausible_web/templates/email/over_limit.html.eex index 4f5af3fdf..45daf4567 100644 --- a/lib/plausible_web/templates/email/over_limit.html.eex +++ b/lib/plausible_web/templates/email/over_limit.html.eex @@ -8,7 +8,7 @@ In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_for <%= if @suggested_plan == :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 the <%= @suggested_plan.volume %>/mo plan. +Based on that we recommend you select a <%= @suggested_plan.volume %>/mo plan.

You can upgrade your subscription using our self-serve platform. 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.

diff --git a/lib/plausible_web/templates/email/trial_upgrade_email.html.eex b/lib/plausible_web/templates/email/trial_upgrade_email.html.eex index 65ec109ec..3fc23add7 100644 --- a/lib/plausible_web/templates/email/trial_upgrade_email.html.eex +++ b/lib/plausible_web/templates/email/trial_upgrade_email.html.eex @@ -4,7 +4,7 @@ In the last month, your account has used <%= PlausibleWeb.AuthView.delimit_integ <%= if @suggested_plan == :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 the <%= @suggested_plan.volume %>/mo plan. +Based on that we recommend you select a <%= @suggested_plan.volume %>/mo plan.

<%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %>

diff --git a/test/plausible/billing/plans_test.exs b/test/plausible/billing/plans_test.exs index f98dfdbf5..c34bf7d55 100644 --- a/test/plausible/billing/plans_test.exs +++ b/test/plausible/billing/plans_test.exs @@ -68,10 +68,11 @@ defmodule Plausible.Billing.PlansTest do assert List.first(business_plans).monthly_product_id == @v4_business_plan_id end - test "available_plans_with_prices/1" do + test "available_plans returns all plans for user with prices when asked for" do user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id)) - %{growth: growth_plans, business: business_plans} = Plans.available_plans_with_prices(user) + %{growth: growth_plans, business: business_plans} = + Plans.available_plans_for(user, with_prices: true) assert Enum.find(growth_plans, fn plan -> (%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v2_plan_id @@ -82,6 +83,12 @@ defmodule Plausible.Billing.PlansTest do end) end + test "available_plans returns all plans without prices by default" do + user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id)) + + assert %{growth: [_ | _], business: [_ | _]} = Plans.available_plans_for(user) + end + test "latest_enterprise_plan_with_price/1" do user = insert(:user) insert(:enterprise_plan, user: user, paddle_plan_id: "123", inserted_at: Timex.now()) @@ -225,4 +232,36 @@ defmodule Plausible.Billing.PlansTest do ] == Plans.yearly_product_ids() end end + + describe "suggest_tier/1" do + test "suggests Business when user has used a premium feature" do + user = insert(:user, inserted_at: ~N[2024-01-01 10:00:00]) + insert(:api_key, user: user) + + assert Plans.suggest_tier(user) == :business + end + + test "suggests Growth when no premium features used" do + user = insert(:user, inserted_at: ~N[2024-01-01 10:00:00]) + site = insert(:site, members: [user]) + insert(:goal, site: site, event_name: "goals_is_not_premium") + + assert Plans.suggest_tier(user) == :growth + end + + test "suggests Growth tier for a user who used the Stats API, but signed up before it was considered a premium feature" do + user = insert(:user, inserted_at: ~N[2023-10-25 10:00:00]) + insert(:api_key, user: user) + + assert Plans.suggest_tier(user) == :growth + end + + test "suggests Business tier for a user who used the Revenue Goals, even when they signed up before Business tier release" do + user = insert(:user, inserted_at: ~N[2023-10-25 10:00:00]) + site = insert(:site, members: [user]) + insert(:goal, site: site, currency: :USD, event_name: "Purchase") + + assert Plans.suggest_tier(user) == :business + end + end end diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index f2c34a164..1c2e75058 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -19,13 +19,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do @growth_plan_box "#growth-plan-box" @growth_price_tag_amount "#growth-price-tag-amount" @growth_price_tag_interval "#growth-price-tag-interval" - @growth_current_label "#{@growth_plan_box} #current-label" + @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_current_label "#{@business_plan_box} #current-label" + @business_highlight_pill "#{@business_plan_box} #highlight-pill" @business_checkout_button "#business-checkout" @enterprise_plan_box "#enterprise-plan-box" @@ -229,6 +229,26 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~ "if (confirm(\"This plan does not support Custom Properties, which you are currently using. Please note that by subscribing to this plan you will lose 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_plan_box) =~ "Recommended" + refute text_of_element(doc, @business_plan_box) =~ "Recommended" + end + + test "recommends Business tier when Revenue Goals were used during trial", %{ + conn: conn, + user: user + } do + site = insert(:site, members: [user]) + insert(:goal, site: site, currency: :USD, 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 end describe "for a user with a v4 growth subscription plan" do @@ -318,7 +338,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert class =~ "ring-2" assert class =~ "ring-indigo-600" - assert text_of_element(doc, @growth_current_label) == "Current" + assert text_of_element(doc, @growth_highlight_pill) == "Current" end test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do @@ -379,7 +399,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert class =~ "ring-2" assert class =~ "ring-indigo-600" - assert text_of_element(doc, @business_current_label) == "Current" + assert text_of_element(doc, @business_highlight_pill) == "Current" end test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do @@ -568,16 +588,17 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do test "currently owned tier is highlighted if stats are still unlocked", %{conn: conn} do {:ok, _lv, doc} = get_liveview(conn) - assert text_of_element(doc, @growth_current_label) == "Current" + assert text_of_element(doc, @growth_highlight_pill) == "Current" end - test "currently owned tier is not highlighted if stats are locked", %{conn: conn, user: user} do + test "highlights recommended tier", %{conn: conn, user: user} do user.subscription |> Subscription.changeset(%{next_bill_date: Timex.shift(Timex.now(), months: -2)}) |> Repo.update() {:ok, _lv, doc} = get_liveview(conn) - refute element_exists?(doc, @growth_current_label) + assert text_of_element(doc, @growth_highlight_pill) == "Recommended" + refute text_of_element(doc, @business_highlight_pill) == "Recommended" end end @@ -656,10 +677,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do describe "for a free_10k subscription" do setup [:create_user, :log_in, :subscribe_free_10k] - test "does not highlight any tier", %{conn: conn} do + test "recommends growth tier when no premium features used", %{conn: conn} do {:ok, _lv, doc} = get_liveview(conn) - refute element_exists?(doc, @growth_current_label) - refute element_exists?(doc, @business_current_label) + 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, user: user} do + site = insert(:site, members: [user]) + 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 diff --git a/test/workers/check_usage_test.exs b/test/workers/check_usage_test.exs index c03843f3e..59605ccc1 100644 --- a/test/workers/check_usage_test.exs +++ b/test/workers/check_usage_test.exs @@ -166,7 +166,7 @@ defmodule Plausible.Workers.CheckUsageTest do }) # Should find 2 visiors - assert html_body =~ ~s(Based on that we recommend you select the 100k/mo plan.) + assert html_body =~ ~s(Based on that we recommend you select a 100k/mo plan.) end describe "enterprise customers" do diff --git a/test/workers/send_trial_notifications_test.exs b/test/workers/send_trial_notifications_test.exs index 2c2a89ac3..f21302526 100644 --- a/test/workers/send_trial_notifications_test.exs +++ b/test/workers/send_trial_notifications_test.exs @@ -147,56 +147,56 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0}) - assert email.html_body =~ "we recommend you select the 10k/mo plan." + assert email.html_body =~ "we recommend you select a 10k/mo plan." end test "suggests 100k/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {90_000, 0}) - assert email.html_body =~ "we recommend you select the 100k/mo plan." + assert email.html_body =~ "we recommend you select a 100k/mo plan." end test "suggests 200k/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {180_000, 0}) - assert email.html_body =~ "we recommend you select the 200k/mo plan." + assert email.html_body =~ "we recommend you select a 200k/mo plan." end test "suggests 500k/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {450_000, 0}) - assert email.html_body =~ "we recommend you select the 500k/mo plan." + assert email.html_body =~ "we recommend you select a 500k/mo plan." end test "suggests 1m/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {900_000, 0}) - assert email.html_body =~ "we recommend you select the 1M/mo plan." + assert email.html_body =~ "we recommend you select a 1M/mo plan." end test "suggests 2m/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {1_800_000, 0}) - assert email.html_body =~ "we recommend you select the 2M/mo plan." + assert email.html_body =~ "we recommend you select a 2M/mo plan." end test "suggests 5m/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {4_500_000, 0}) - assert email.html_body =~ "we recommend you select the 5M/mo plan." + assert email.html_body =~ "we recommend you select a 5M/mo plan." end test "suggests 10m/mo plan" do user = insert(:user) email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000_000, 0}) - assert email.html_body =~ "we recommend you select the 10M/mo plan." + assert email.html_body =~ "we recommend you select a 10M/mo plan." end test "does not suggest a plan above that" do