Fix recommending a tier on the upgrade page (#4287)

* fix recommending a plan

* Fix credo issue

* Review suggestions

* remove dead code
This commit is contained in:
RobertJoonas 2024-07-04 11:57:57 +03:00 committed by GitHub
parent 379714c133
commit 81047ba06d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 235 additions and 81 deletions

View File

@ -1,8 +1,7 @@
defmodule Plausible.Billing.Plans do
alias Plausible.Billing.Subscriptions
use Plausible.Repo
alias Plausible.Billing.{Quota, Subscription, Plan, EnterprisePlan}
alias Plausible.Billing.Feature.{StatsAPI, Props}
alias Plausible.Billing.{Subscription, Plan, EnterprisePlan}
alias Plausible.Auth.User
for f <- [
@ -235,21 +234,6 @@ 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.Usage.features_usage(user), &(&1 not in growth_features)) do
:business
else
:growth
end
end
def all() do
@legacy_plans ++ @plans_v1 ++ @plans_v2 ++ @plans_v3 ++ @plans_v4 ++ sandbox_plans()
end

View File

@ -58,6 +58,41 @@ defmodule Plausible.Billing.Quota do
def eligible_for_upgrade?(usage), do: usage.sites > 0
def ensure_feature_access(usage, plan) do
case usage.features -- plan.features do
[] -> :ok
features -> {:error, {:unavailable_features, features}}
end
end
@doc """
Suggests a suitable tier (Growth or Business) for the given usage map.
If even the highest Business plan does not accommodate the usage, then
`:custom` is returned. This means that this kind of usage should get on
a custom plan.
`nil` is returned if the usage is not eligible for upgrade.
"""
def suggest_tier(usage, highest_growth_plan, highest_business_plan) do
if eligible_for_upgrade?(usage) do
cond do
usage_fits_plan?(usage, highest_growth_plan) -> :growth
usage_fits_plan?(usage, highest_business_plan) -> :business
true -> :custom
end
end
end
defp usage_fits_plan?(usage, plan) do
with :ok <- ensure_within_plan_limits(usage, plan),
:ok <- ensure_feature_access(usage, plan) do
true
else
_ -> false
end
end
defp exceeded_limits(usage, plan, opts) do
for {limit, exceeded?} <- [
{:team_member_limit, not within_limit?(usage.team_members, plan.team_member_limit)},

View File

@ -10,7 +10,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
def standard(assigns) do
highlight =
cond do
assigns.owned -> "Current"
assigns.owned && assigns.recommended -> "Current"
assigns.recommended -> "Recommended"
true -> nil
end
@ -57,9 +57,28 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
~H"""
<div
id="enterprise-plan-box"
class="rounded-3xl px-6 sm:px-8 py-4 sm:py-6 bg-gray-900 shadow-xl dark:bg-gray-800 dark:ring-gray-600"
class={[
"rounded-3xl px-6 sm:px-8 py-4 sm:py-6 bg-gray-900 shadow-xl dark:bg-gray-800",
!@recommended && "dark:ring-gray-600",
@recommended && "ring-4 ring-indigo-500 dark:ring-2 dark:ring-indigo-300"
]}
>
<h3 class="text-lg font-semibold leading-8 text-white dark:text-gray-100">Enterprise</h3>
<div class="flex items-center justify-between gap-x-4">
<h3 class={[
"text-lg font-semibold leading-8",
!@recommended && "text-white dark:text-gray-100",
@recommended && "text-indigo-400 dark:text-indigo-300"
]}>
Enterprise
</h3>
<span
:if={@recommended}
id="enterprise-highlight-pill"
class="rounded-full ring-1 ring-indigo-500 px-2.5 py-1 text-xs font-semibold leading-5 text-indigo-400 dark:text-indigo-300 dark:ring-1 dark:ring-indigo-300/50"
>
Recommended
</span>
</div>
<p class="mt-6 flex items-baseline gap-x-1">
<span class="text-4xl font-bold tracking-tight text-white dark:text-gray-100">
Custom
@ -193,7 +212,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
[]
end
features_to_lose = assigns.usage.features -- assigns.plan_to_render.features
feature_usage_check = Quota.ensure_feature_access(assigns.usage, assigns.plan_to_render)
assigns =
assigns
@ -202,7 +221,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
|> assign(:checkout_disabled, checkout_disabled)
|> assign(:disabled_message, disabled_message)
|> assign(:exceeded_plan_limits, exceeded_plan_limits)
|> assign(:confirm_message, losing_features_message(features_to_lose))
|> assign(:confirm_message, losing_features_message(feature_usage_check))
~H"""
<%= if @owned_plan && Plausible.Billing.Subscriptions.resumable?(@user.subscription) do %>
@ -325,15 +344,15 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
"""
end
defp losing_features_message([]), do: nil
defp losing_features_message(:ok), do: nil
defp losing_features_message(features_to_lose) do
defp losing_features_message({:error, {:unavailable_features, features}}) do
features_list_str =
features_to_lose
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 are currently using. Please note that by subscribing to this plan you will lose access to #{if length(features_to_lose) == 1, do: "this feature", else: "these features"}."
"This plan does not support #{features_list_str}, which you are currently using. Please note that by subscribing to this plan you will lose access to #{if length(features) == 1, do: "this feature", else: "these features"}."
end
defp contact_button(assigns) do

View File

@ -41,23 +41,17 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|> 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,
usage: usage,
user: user
} ->
if owned_plan != nil or not Quota.eligible_for_upgrade?(usage) do
nil
else
Plans.suggest_tier(user)
end
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_for(user, with_prices: true, customer_ip: remote_ip)
end)
|> assign_new(:recommended_tier, fn %{usage: usage, available_plans: available_plans} ->
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)
end)
|> assign_new(:available_volumes, fn %{available_plans: available_plans} ->
get_available_volumes(available_plans)
end)
@ -148,7 +142,10 @@ defmodule PlausibleWeb.Live.ChoosePlan do
available={!!@selected_business_plan}
{assigns}
/>
<PlanBox.enterprise benefits={@enterprise_benefits} />
<PlanBox.enterprise
benefits={@enterprise_benefits}
recommended={@recommended_tier == :custom}
/>
</div>
<p class="mx-auto mt-8 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-gray-400">
<.render_usage pageview_usage={@usage.monthly_pageviews} />

View File

@ -284,39 +284,6 @@ defmodule Plausible.Billing.PlansTest do
end
end
describe "suggest_tier/1" do
test "suggests Business when user has used a premium feature" do
user = insert(:user)
insert(:api_key, user: user)
assert Plans.suggest_tier(user) == :business
end
test "suggests Growth when no premium features used" do
user = insert(:user)
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
@tag :ee_only
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
defp assert_generation(plans_list, generation) do
assert List.first(plans_list).generation == generation
end

View File

@ -15,6 +15,8 @@ defmodule Plausible.Billing.QuotaTest do
@v3_plan_id "749342"
@v3_business_plan_id "857481"
@v4_1m_plan_id "857101"
@v4_10m_growth_plan_id "857104"
@v4_10m_business_plan_id "857112"
describe "site_limit/1" do
@describetag :ee_only
@ -930,4 +932,26 @@ defmodule Plausible.Billing.QuotaTest do
assert current_cycle == Date.range(~D[2021-01-01], ~D[2021-01-31])
end
end
describe "suggest_tier/2" do
setup do
%{user: insert(:user) |> Plausible.Users.with_subscription()}
end
test "returns nil if the monthly pageview limit exceeds regular plans",
%{user: user} do
highest_growth_plan = Plausible.Billing.Plans.find(@v4_10m_growth_plan_id)
highest_business_plan = Plausible.Billing.Plans.find(@v4_10m_business_plan_id)
usage =
Quota.Usage.usage(user)
|> Map.replace!(:monthly_pageviews, %{last_30_days: %{total: 12_000_000}})
suggested_tier =
usage
|> Quota.suggest_tier(highest_growth_plan, highest_business_plan)
assert suggested_tier == nil
end
end
end

View File

@ -35,6 +35,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
@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+"]
@ -224,8 +225,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
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"
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
@ -233,8 +234,82 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @business_plan_box) =~ "Recommended"
refute text_of_element(doc, @growth_plan_box) =~ "Recommended"
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 = insert(:site, members: [previous_owner])
insert(:goal, site: site, currency: :USD, event_name: "Purchase")
insert(:invitation, email: user.email, inviter: previous_owner, role: :owner, site: site)
{: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 =
insert(:site,
memberships: [
build(:site_membership, role: :owner, user: user),
build(:site_membership, role: :admin, user: insert(:user)),
build(:site_membership, role: :admin, user: insert(:user))
]
)
previous_owner = insert(:user)
pending_ownership_site =
insert(:site,
memberships: [
build(:site_membership, role: :owner, user: previous_owner),
build(:site_membership, role: :viewer, user: insert(:user))
]
)
insert(:invitation,
email: user.email,
inviter: previous_owner,
role: :owner,
site: pending_ownership_site
)
{: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
insert_list(9, :site, members: [user])
assert 10 = Plausible.Billing.Quota.Usage.site_usage(user)
another_user = insert(:user)
pending_ownership_site = insert(:site, members: [another_user])
insert(:invitation,
email: user.email,
site: pending_ownership_site,
role: :owner,
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
@ -420,10 +495,11 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert doc =~ "billable pageviews in the last billing cycle"
end
test "warns about losing access to a feature used by a pending ownership site", %{
conn: conn,
user: user
} do
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 = insert(:user)
pending_site = insert(:site, members: [another_user])
@ -442,6 +518,9 @@ 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.\")) {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
@ -527,7 +606,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert text_of_element(doc, @slider_value) == "10k"
end
test "makes it clear that the user is currently on a business tier", %{conn: conn} do
test "highlights Business box as the 'Current' tier if it's suitable for their usage", %{
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)
@ -535,6 +619,47 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
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 "highlights Growth box as the 'Recommended' tier if it would accommodate their usage", %{
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) == "Recommended"
refute element_exists?(doc, @business_highlight_pill)
end
test "recommends Enterprise when site limit exceeds Business tier due to pending ownerships",
%{
conn: conn,
user: user
} do
insert_list(49, :site, members: [user])
assert 50 = Plausible.Billing.Quota.Usage.site_usage(user)
another_user = insert(:user)
pending_ownership_site = insert(:site, members: [another_user])
insert(:invitation,
email: user.email,
site: pending_ownership_site,
role: :owner,
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
@ -970,7 +1095,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a user with no sites but pending ownership transfer" do
setup [:create_user, :log_in]
test "allows to subscribe and does not render a notice", %{conn: conn, user: user} do
test "allows to subscribe and does not render the 'upgrade ineligible' notice", %{
conn: conn,
user: user
} do
old_owner = insert(:user)
site = insert(:site, members: [old_owner])
insert(:invitation, site_id: site.id, inviter: old_owner, email: user.email, role: :owner)