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

View File

@ -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
<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
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"""
<div
id={"#{@kind}-plan-box"}
class={[
"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",
@owned && "ring-2 ring-indigo-600 dark:ring-indigo-300"
!@highlight && "dark:ring-gray-600",
@highlight && "ring-2 ring-indigo-600 dark:ring-indigo-300"
]}
>
<div class="flex items-center justify-between gap-x-4">
<h3 class={[
"text-lg font-semibold leading-8",
!@owned && "text-gray-900 dark:text-gray-100",
@owned && "text-indigo-600 dark:text-indigo-300"
!@highlight && "text-gray-900 dark:text-gray-100",
@highlight && "text-indigo-600 dark:text-indigo-300"
]}>
<%= String.capitalize(to_string(@kind)) %>
</h3>
<.current_label :if={@owned} />
<.pill :if={@highlight} text={@highlight} />
</div>
<div>
<.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"""
<div class="flex items-center justify-between gap-x-4">
<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"
>
Current
<%= @text %>
</p>
</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 %>
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.
<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.
<% 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 %>
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.
<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.
<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 %>
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.
<br /><br />
<%= link("Upgrade now", to: "#{plausible_url()}/billing/upgrade") %>
<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
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

View File

@ -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

View File

@ -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

View File

@ -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