mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
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:
parent
6e6508a359
commit
df44f549d8
@ -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
|
||||
|
@ -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>
|
||||
"""
|
||||
|
@ -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 %>
|
||||
|
@ -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 />
|
||||
|
@ -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 />
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user