Recommending a plan (#3476)

* use a different article in the email copies

... for recommending a plan, since the user can choose between Growth
and Business.

* small refactoring improvement

Rename `Plans.available_plans_with_prices` to `Plans.available_plans_for`,
taking an optional `with_prices` argument.

* highlight recommended tier for trial users on the ugprade page

* review suggestion
This commit is contained in:
RobertJoonas 2023-11-02 14:46:14 +00:00 committed by GitHub
parent 6e6508a359
commit df44f549d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 152 additions and 40 deletions

View File

@ -1,7 +1,8 @@
defmodule Plausible.Billing.Plans do defmodule Plausible.Billing.Plans do
alias Plausible.Billing.Subscriptions alias Plausible.Billing.Subscriptions
use Plausible.Repo 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 alias Plausible.Auth.User
for f <- [ for f <- [
@ -26,6 +27,8 @@ defmodule Plausible.Billing.Plans do
Module.put_attribute(__MODULE__, :external_resource, path) Module.put_attribute(__MODULE__, :external_resource, path)
end end
@business_tier_launch ~D[2023-11-07]
@spec growth_plans_for(User.t()) :: [Plan.t()] @spec growth_plans_for(User.t()) :: [Plan.t()]
@doc """ @doc """
Returns a list of growth plans available for the user to choose. 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)) |> Enum.filter(&(&1.kind == :business))
end end
def available_plans_with_prices(%User{} = user) do def available_plans_for(%User{} = user, opts \\ []) do
(growth_plans_for(user) ++ business_plans_for(user)) plans = growth_plans_for(user) ++ business_plans_for(user)
|> with_prices()
|> Enum.group_by(& &1.kind) plans =
if Keyword.get(opts, :with_prices) do
with_prices(plans)
else
plans
end
Enum.group_by(plans, & &1.kind)
end end
@spec yearly_product_ids() :: [String.t()] @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)) Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit))
end 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 defp all() do
@legacy_plans ++ @plans_v1 ++ @plans_v2 ++ @plans_v3 ++ @plans_v4 @legacy_plans ++ @plans_v1 ++ @plans_v2 ++ @plans_v3 ++ @plans_v4
end end

View File

@ -28,11 +28,17 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|> assign_new(:owned_plan, fn %{user: %{subscription: subscription}} -> |> assign_new(:owned_plan, fn %{user: %{subscription: subscription}} ->
Plans.get_regular_plan(subscription, only_non_expired: true) Plans.get_regular_plan(subscription, only_non_expired: true)
end) 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} -> |> assign_new(:current_interval, fn %{user: user} ->
current_user_subscription_interval(user.subscription) current_user_subscription_interval(user.subscription)
end) end)
|> assign_new(:available_plans, fn %{user: user} -> |> assign_new(:available_plans, fn %{user: user} ->
Plans.available_plans_with_prices(user) Plans.available_plans_for(user, with_prices: true)
end) end)
|> assign_new(:available_volumes, fn %{available_plans: available_plans} -> |> assign_new(:available_volumes, fn %{available_plans: available_plans} ->
get_available_volumes(available_plans) get_available_volumes(available_plans)
@ -101,7 +107,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<div class="mt-6 isolate mx-auto grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-3"> <div class="mt-6 isolate mx-auto grid max-w-md grid-cols-1 gap-8 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<.plan_box <.plan_box
kind={:growth} 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} plan_to_render={@growth_plan_to_render}
benefits={@growth_benefits} benefits={@growth_benefits}
available={!!@selected_growth_plan} available={!!@selected_growth_plan}
@ -109,7 +116,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
/> />
<.plan_box <.plan_box
kind={:business} 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} plan_to_render={@business_plan_to_render}
benefits={@business_benefits} benefits={@business_benefits}
available={!!@selected_business_plan} available={!!@selected_business_plan}
@ -241,24 +249,33 @@ defmodule PlausibleWeb.Live.ChoosePlan do
end end
defp plan_box(assigns) do defp plan_box(assigns) do
highlight =
cond do
assigns.owned -> "Current"
assigns.recommended -> "Recommended"
true -> nil
end
assigns = assign(assigns, :highlight, highlight)
~H""" ~H"""
<div <div
id={"#{@kind}-plan-box"} id={"#{@kind}-plan-box"}
class={[ class={[
"shadow-lg bg-white rounded-3xl px-6 sm:px-8 py-4 sm:py-6 dark:bg-gray-800", "shadow-lg bg-white rounded-3xl px-6 sm:px-8 py-4 sm:py-6 dark:bg-gray-800",
!@owned && "dark:ring-gray-600", !@highlight && "dark:ring-gray-600",
@owned && "ring-2 ring-indigo-600 dark:ring-indigo-300" @highlight && "ring-2 ring-indigo-600 dark:ring-indigo-300"
]} ]}
> >
<div class="flex items-center justify-between gap-x-4"> <div class="flex items-center justify-between gap-x-4">
<h3 class={[ <h3 class={[
"text-lg font-semibold leading-8", "text-lg font-semibold leading-8",
!@owned && "text-gray-900 dark:text-gray-100", !@highlight && "text-gray-900 dark:text-gray-100",
@owned && "text-indigo-600 dark:text-indigo-300" @highlight && "text-indigo-600 dark:text-indigo-300"
]}> ]}>
<%= String.capitalize(to_string(@kind)) %> <%= String.capitalize(to_string(@kind)) %>
</h3> </h3>
<.current_label :if={@owned} /> <.pill :if={@highlight} text={@highlight} />
</div> </div>
<div> <div>
<.render_price_info available={@available} {assigns} /> <.render_price_info available={@available} {assigns} />
@ -444,14 +461,14 @@ defmodule PlausibleWeb.Live.ChoosePlan do
""" """
end end
defp current_label(assigns) do defp pill(assigns) do
~H""" ~H"""
<div class="flex items-center justify-between gap-x-4"> <div class="flex items-center justify-between gap-x-4">
<p <p
id="current-label" id="highlight-pill"
class="rounded-full bg-indigo-600/10 px-2.5 py-1 text-xs font-semibold leading-5 text-indigo-600 dark:text-indigo-300 dark:ring-1 dark:ring-indigo-300/50" class="rounded-full bg-indigo-600/10 px-2.5 py-1 text-xs font-semibold leading-5 text-indigo-600 dark:text-indigo-300 dark:ring-1 dark:ring-indigo-300/50"
> >
Current <%= @text %>
</p> </p>
</div> </div>
""" """

View File

@ -6,7 +6,7 @@ In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_for
<%= if @suggested_plan == :enterprise do %> <%= 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. This is more than our standard plans, so please reply back to this email to get a quote for your volume.
<% else %> <% 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.
<br /><br /> <br /><br />
<a href="https://plausible.io/settings">Click here</a> to go to your account settings. You can upgrade your subscription tier by clicking the 'Change plan' link. <a href="https://plausible.io/settings">Click here</a> to go to your account settings. You can upgrade your subscription tier by clicking the 'Change plan' link.
<% end %> <% end %>

View File

@ -8,7 +8,7 @@ In the last billing cycle (<%= date_format(@last_cycle.first) %> to <%= date_for
<%= if @suggested_plan == :enterprise do %> <%= 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. This is more than our standard plans, so please reply back to this email to get a quote for your volume.
<% else %> <% 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.
<br /><br /> <br /><br />
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. 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.
<br /><br /> <br /><br />

View File

@ -4,7 +4,7 @@ In the last month, your account has used <%= PlausibleWeb.AuthView.delimit_integ
<%= if @suggested_plan == :enterprise do %> <%= 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. This is more than our standard plans, so please reply back to this email to get a quote for your volume.
<% else %> <% 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.
<br /><br /> <br /><br />
<%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %> <%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %>
<br /><br /> <br /><br />

View File

@ -68,10 +68,11 @@ defmodule Plausible.Billing.PlansTest do
assert List.first(business_plans).monthly_product_id == @v4_business_plan_id assert List.first(business_plans).monthly_product_id == @v4_business_plan_id
end 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)) 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 -> assert Enum.find(growth_plans, fn plan ->
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v2_plan_id (%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v2_plan_id
@ -82,6 +83,12 @@ defmodule Plausible.Billing.PlansTest do
end) end)
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 test "latest_enterprise_plan_with_price/1" do
user = insert(:user) user = insert(:user)
insert(:enterprise_plan, user: user, paddle_plan_id: "123", inserted_at: Timex.now()) 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() ] == Plans.yearly_product_ids()
end end
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 end

View File

@ -19,13 +19,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@growth_plan_box "#growth-plan-box" @growth_plan_box "#growth-plan-box"
@growth_price_tag_amount "#growth-price-tag-amount" @growth_price_tag_amount "#growth-price-tag-amount"
@growth_price_tag_interval "#growth-price-tag-interval" @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" @growth_checkout_button "#growth-checkout"
@business_plan_box "#business-plan-box" @business_plan_box "#business-plan-box"
@business_price_tag_amount "#business-price-tag-amount" @business_price_tag_amount "#business-price-tag-amount"
@business_price_tag_interval "#business-price-tag-interval" @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" @business_checkout_button "#business-checkout"
@enterprise_plan_box "#enterprise-plan-box" @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") =~ 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" "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 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 end
describe "for a user with a v4 growth subscription plan" do 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-2"
assert class =~ "ring-indigo-600" assert class =~ "ring-indigo-600"
assert text_of_element(doc, @growth_current_label) == "Current" assert text_of_element(doc, @growth_highlight_pill) == "Current"
end end
test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do 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-2"
assert class =~ "ring-indigo-600" assert class =~ "ring-indigo-600"
assert text_of_element(doc, @business_current_label) == "Current" assert text_of_element(doc, @business_highlight_pill) == "Current"
end end
test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do 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 test "currently owned tier is highlighted if stats are still unlocked", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn) {: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 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 user.subscription
|> Subscription.changeset(%{next_bill_date: Timex.shift(Timex.now(), months: -2)}) |> Subscription.changeset(%{next_bill_date: Timex.shift(Timex.now(), months: -2)})
|> Repo.update() |> Repo.update()
{:ok, _lv, doc} = get_liveview(conn) {: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
end end
@ -656,10 +677,20 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a free_10k subscription" do describe "for a free_10k subscription" do
setup [:create_user, :log_in, :subscribe_free_10k] 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) {:ok, _lv, doc} = get_liveview(conn)
refute element_exists?(doc, @growth_current_label) assert element_exists?(doc, @growth_highlight_pill)
refute element_exists?(doc, @business_current_label) 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 end
test "renders Paddle upgrade buttons", %{conn: conn, user: user} do test "renders Paddle upgrade buttons", %{conn: conn, user: user} do

View File

@ -166,7 +166,7 @@ defmodule Plausible.Workers.CheckUsageTest do
}) })
# Should find 2 visiors # 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 end
describe "enterprise customers" do describe "enterprise customers" do

View File

@ -147,56 +147,56 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0}) 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 end
test "suggests 100k/mo plan" do test "suggests 100k/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {90_000, 0}) 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 end
test "suggests 200k/mo plan" do test "suggests 200k/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {180_000, 0}) 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 end
test "suggests 500k/mo plan" do test "suggests 500k/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {450_000, 0}) 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 end
test "suggests 1m/mo plan" do test "suggests 1m/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {900_000, 0}) 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 end
test "suggests 2m/mo plan" do test "suggests 2m/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {1_800_000, 0}) 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 end
test "suggests 5m/mo plan" do test "suggests 5m/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {4_500_000, 0}) 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 end
test "suggests 10m/mo plan" do test "suggests 10m/mo plan" do
user = insert(:user) user = insert(:user)
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000_000, 0}) 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 end
test "does not suggest a plan above that" do test "does not suggest a plan above that" do