diff --git a/lib/plausible/billing/plan.ex b/lib/plausible/billing/plan.ex new file mode 100644 index 0000000000..6f939dfde1 --- /dev/null +++ b/lib/plausible/billing/plan.ex @@ -0,0 +1,95 @@ +defmodule Plausible.Billing.Plan do + @moduledoc false + + @derive Jason.Encoder + @enforce_keys ~w(kind generation site_limit monthly_pageview_limit team_member_limit features volume monthly_product_id yearly_product_id)a + defstruct @enforce_keys ++ [:monthly_cost, :yearly_cost] + + @type t() :: + %__MODULE__{ + kind: atom(), + generation: non_neg_integer(), + monthly_pageview_limit: non_neg_integer(), + site_limit: non_neg_integer(), + team_member_limit: non_neg_integer() | :unlimited, + volume: String.t(), + monthly_cost: Money.t() | nil, + yearly_cost: Money.t() | nil, + monthly_product_id: String.t() | nil, + yearly_product_id: String.t() | nil, + features: [atom()] + } + | :enterprise + + def build!(raw_params, file_name) when is_map(raw_params) do + raw_params + |> put_kind() + |> put_generation() + |> put_volume() + |> put_team_member_limit!(file_name) + |> put_features!(file_name) + |> new!() + end + + defp new!(params) do + struct!(__MODULE__, params) + end + + defp put_kind(params) do + Map.put(params, :kind, String.to_existing_atom(params.kind)) + end + + # Due to grandfathering, we sometimes need to check the "generation" + # (e.g. v1, v2, etc...) of a user's subscription plan. For instance, + # on prod, the users subscribed to a v2 plan are only supposed to + # see v2 plans when they go to the upgrade page. + # + # In the `dev` environment though, "sandbox" plans are used, which + # unlike production plans, contain multiple generations of plans in + # the same file for testing purposes. + defp put_generation(params) do + Map.put(params, :generation, params.generation) + end + + defp put_volume(params) do + volume = + params.monthly_pageview_limit + |> PlausibleWeb.StatsView.large_number_format() + + Map.put(params, :volume, volume) + end + + defp put_team_member_limit!(params, file_name) do + team_member_limit = + case params.team_member_limit do + number when is_integer(number) -> + number + + "unlimited" -> + :unlimited + + other -> + raise ArgumentError, + "Failed to parse team member limit #{inspect(other)} from #{file_name}.json" + end + + Map.put(params, :team_member_limit, team_member_limit) + end + + defp put_features!(params, file_name) do + features = + Plausible.Billing.Feature.list() + |> Enum.filter(fn module -> + to_string(module.name()) in params.features + end) + + if length(features) == length(params.features) do + Map.put(params, :features, features) + else + raise( + ArgumentError, + "Unrecognized feature(s) in #{inspect(params.features)} (#{file_name}.json)" + ) + end + end +end diff --git a/lib/plausible/billing/plans.ex b/lib/plausible/billing/plans.ex index 2566fe772e..93d2c84725 100644 --- a/lib/plausible/billing/plans.ex +++ b/lib/plausible/billing/plans.ex @@ -1,30 +1,3 @@ -defmodule Plausible.Billing.Plan do - @moduledoc false - - @derive Jason.Encoder - @enforce_keys ~w(kind site_limit monthly_pageview_limit team_member_limit features volume monthly_product_id yearly_product_id)a - defstruct @enforce_keys ++ [:monthly_cost, :yearly_cost] - - @type t() :: - %__MODULE__{ - kind: atom(), - monthly_pageview_limit: non_neg_integer(), - site_limit: non_neg_integer(), - team_member_limit: non_neg_integer() | :unlimited, - volume: String.t(), - monthly_cost: Money.t() | nil, - yearly_cost: Money.t() | nil, - monthly_product_id: String.t() | nil, - yearly_product_id: String.t() | nil, - features: [atom()] - } - | :enterprise - - def new(params) when is_map(params) do - struct!(__MODULE__, params) - end -end - defmodule Plausible.Billing.Plans do alias Plausible.Billing.Subscriptions use Plausible.Repo @@ -46,36 +19,7 @@ defmodule Plausible.Billing.Plans do path |> File.read!() |> Jason.decode!(keys: :atoms!) - |> Enum.map(fn raw -> - team_member_limit = - case raw.team_member_limit do - number when is_integer(number) -> number - "unlimited" -> :unlimited - _any -> raise ArgumentError, "Failed to parse team member limit from plan JSON files" - end - - features = - Plausible.Billing.Feature.list() - |> Enum.filter(fn module -> - to_string(module.name()) in raw.features - end) - - if length(features) != length(raw.features), - do: - raise( - ArgumentError, - "Unrecognized feature(s) in #{inspect(raw.features)} (#{f}.json)" - ) - - volume = PlausibleWeb.StatsView.large_number_format(raw.monthly_pageview_limit) - - raw - |> Map.put(:volume, volume) - |> Map.put(:kind, String.to_atom(raw.kind)) - |> Map.put(:team_member_limit, team_member_limit) - |> Map.put(:features, features) - |> Plan.new() - end) + |> Enum.map(&Plan.build!(&1, f)) Module.put_attribute(__MODULE__, f, plans_list) @@ -94,26 +38,34 @@ defmodule Plausible.Billing.Plans do def growth_plans_for(%User{} = user) do user = Plausible.Users.with_subscription(user) v4_available = FunWithFlags.enabled?(:business_tier, for: user) - owned_plan_id = user.subscription && user.subscription.paddle_plan_id + owned_plan = get_regular_plan(user.subscription) cond do - find(owned_plan_id, @plans_v1) -> @plans_v1 - find(owned_plan_id, @plans_v2) -> @plans_v2 - find(owned_plan_id, @plans_v3) -> @plans_v3 - find(owned_plan_id, plans_sandbox()) -> plans_sandbox() Application.get_env(:plausible, :environment) == "dev" -> plans_sandbox() - Timex.before?(user.inserted_at, ~D[2022-01-01]) -> @plans_v2 - v4_available -> Enum.filter(@plans_v4, &(&1.kind == :growth)) - true -> @plans_v3 + !owned_plan -> if v4_available, do: @plans_v4, else: @plans_v3 + owned_plan.kind == :business -> @plans_v4 + owned_plan.generation == 1 -> @plans_v1 + owned_plan.generation == 2 -> @plans_v2 + owned_plan.generation == 3 -> @plans_v3 + owned_plan.generation == 4 -> @plans_v4 end + |> Enum.filter(&(&1.kind == :growth)) end - def business_plans() do - Enum.filter(@plans_v4, &(&1.kind == :business)) + def business_plans_for(%User{} = user) do + user = Plausible.Users.with_subscription(user) + owned_plan = get_regular_plan(user.subscription) + + cond do + Application.get_env(:plausible, :environment) == "dev" -> plans_sandbox() + owned_plan && owned_plan.generation < 4 -> @plans_v3 + true -> @plans_v4 + end + |> Enum.filter(&(&1.kind == :business)) end def available_plans_with_prices(%User{} = user) do - (growth_plans_for(user) ++ business_plans()) + (growth_plans_for(user) ++ business_plans_for(user)) |> with_prices() |> Enum.group_by(& &1.kind) end @@ -261,7 +213,7 @@ defmodule Plausible.Billing.Plans do available_plans = if business_tier?(user.subscription), - do: business_plans(), + do: business_plans_for(user), else: growth_plans_for(user) Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit)) diff --git a/lib/plausible_web/live/choose_plan.ex b/lib/plausible_web/live/choose_plan.ex index b24887e31b..827de3c0eb 100644 --- a/lib/plausible_web/live/choose_plan.ex +++ b/lib/plausible_web/live/choose_plan.ex @@ -64,6 +64,24 @@ defmodule PlausibleWeb.Live.ChoosePlan do end def render(assigns) do + growth_plan_to_render = + assigns.selected_growth_plan || List.last(assigns.available_plans.growth) + + business_plan_to_render = + assigns.selected_business_plan || List.last(assigns.available_plans.business) + + growth_benefits = growth_benefits(growth_plan_to_render) + + business_benefits = business_benefits(business_plan_to_render, growth_benefits) + + assigns = + assigns + |> assign(:growth_plan_to_render, growth_plan_to_render) + |> assign(:business_plan_to_render, business_plan_to_render) + |> assign(:growth_benefits, growth_benefits) + |> assign(:business_benefits, business_benefits) + |> assign(:enterprise_benefits, enterprise_benefits(business_benefits)) + ~H"""
<.usage usage={@usage} /> @@ -275,30 +287,31 @@ defmodule PlausibleWeb.Live.ChoosePlan do <% end %>
+
Custom @@ -356,6 +369,18 @@ defmodule PlausibleWeb.Live.ChoosePlan do """ end + slot :inner_block, required: true + attr :icon_color, :string, default: "indigo-600" + + defp plan_benefit(assigns) do + ~H""" +
@@ -385,25 +413,9 @@ defmodule PlausibleWeb.Live.ChoosePlan do
role="list"
class="mt-8 space-y-3 text-sm leading-6 xl:mt-10 text-gray-300 dark:text-gray-100"
>
-
+ Sites API access for + <.link + class="text-indigo-500 hover:text-indigo-400" + href="https://plausible.io/white-label-web-analytics" + > + reselling + +
+ """ + end + defp contact_link(), do: @contact_link defp billing_faq_link(), do: @billing_faq_link diff --git a/priv/plans_v1.json b/priv/plans_v1.json index d337280f0d..0f38a2ee03 100644 --- a/priv/plans_v1.json +++ b/priv/plans_v1.json @@ -1,6 +1,7 @@ [ { "kind":"growth", + "generation":1, "monthly_pageview_limit":10000, "monthly_product_id":"558018", "yearly_product_id":"572810", @@ -10,6 +11,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":100000, "monthly_product_id":"558745", "yearly_product_id":"590752", @@ -19,6 +21,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":200000, "monthly_product_id":"597485", "yearly_product_id":"597486", @@ -28,6 +31,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":500000, "monthly_product_id":"597487", "yearly_product_id":"597488", @@ -37,6 +41,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":1000000, "monthly_product_id":"597642", "yearly_product_id":"597643", @@ -46,6 +51,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":2000000, "monthly_product_id":"597309", "yearly_product_id":"597310", @@ -55,6 +61,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":5000000, "monthly_product_id":"597311", "yearly_product_id":"597312", @@ -64,6 +71,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":10000000, "monthly_product_id":"642352", "yearly_product_id":"642354", @@ -73,6 +81,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":20000000, "monthly_product_id":"642355", "yearly_product_id":"642356", @@ -82,6 +91,7 @@ }, { "kind":"growth", + "generation":1, "monthly_pageview_limit":50000000, "monthly_product_id":"650652", "yearly_product_id":"650653", diff --git a/priv/plans_v2.json b/priv/plans_v2.json index 265fcfafb4..c1f8d4729f 100644 --- a/priv/plans_v2.json +++ b/priv/plans_v2.json @@ -1,6 +1,7 @@ [ { "kind":"growth", + "generation":2, "monthly_pageview_limit":10000, "monthly_product_id":"654177", "yearly_product_id":"653232", @@ -10,6 +11,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":100000, "monthly_product_id":"654178", "yearly_product_id":"653234", @@ -19,6 +21,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":200000, "monthly_product_id":"653237", "yearly_product_id":"653236", @@ -28,6 +31,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":500000, "monthly_product_id":"653238", "yearly_product_id":"653239", @@ -37,6 +41,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":1000000, "monthly_product_id":"653240", "yearly_product_id":"653242", @@ -46,6 +51,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":2000000, "monthly_product_id":"653253", "yearly_product_id":"653254", @@ -55,6 +61,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":5000000, "monthly_product_id":"653255", "yearly_product_id":"653256", @@ -64,6 +71,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":10000000, "monthly_product_id":"654181", "yearly_product_id":"653257", @@ -73,6 +81,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":20000000, "monthly_product_id":"654182", "yearly_product_id":"653258", @@ -82,6 +91,7 @@ }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":50000000, "monthly_product_id":"654183", "yearly_product_id":"653259", diff --git a/priv/plans_v3.json b/priv/plans_v3.json index d80e878eb9..e0d2485732 100644 --- a/priv/plans_v3.json +++ b/priv/plans_v3.json @@ -1,6 +1,7 @@ [ { "kind":"growth", + "generation":3, "monthly_pageview_limit":10000, "monthly_product_id":"749342", "yearly_product_id":"749343", @@ -10,6 +11,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":100000, "monthly_product_id":"749344", "yearly_product_id":"749345", @@ -19,6 +21,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":200000, "monthly_product_id":"749346", "yearly_product_id":"749347", @@ -28,6 +31,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":500000, "monthly_product_id":"749348", "yearly_product_id":"749349", @@ -37,6 +41,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":1000000, "monthly_product_id":"749350", "yearly_product_id":"749352", @@ -46,6 +51,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":2000000, "monthly_product_id":"749353", "yearly_product_id":"749355", @@ -55,6 +61,7 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":5000000, "monthly_product_id":"749356", "yearly_product_id":"749357", @@ -64,11 +71,92 @@ }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":10000000, "monthly_product_id":"749358", "yearly_product_id":"749359", "site_limit":50, "team_member_limit":"unlimited", "features":["goals","props","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":10000, + "monthly_product_id":"857481", + "yearly_product_id":"857482", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":100000, + "monthly_product_id":"857483", + "yearly_product_id":"857484", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":200000, + "monthly_product_id":"857486", + "yearly_product_id":"857487", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":500000, + "monthly_product_id":"857490", + "yearly_product_id":"857491", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":1000000, + "monthly_product_id":"857493", + "yearly_product_id":"857494", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":2000000, + "monthly_product_id":"857495", + "yearly_product_id":"857496", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":5000000, + "monthly_product_id":"857498", + "yearly_product_id":"857500", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] + }, + { + "kind":"business", + "generation":3, + "monthly_pageview_limit":10000000, + "monthly_product_id":"857501", + "yearly_product_id":"857502", + "site_limit":50, + "team_member_limit":"unlimited", + "features":["goals","props","revenue_goals","funnels","stats_api"] } ] diff --git a/priv/plans_v4.json b/priv/plans_v4.json index 353dccd67e..8fd801898c 100644 --- a/priv/plans_v4.json +++ b/priv/plans_v4.json @@ -1,146 +1,162 @@ [ { "kind":"growth", + "generation":4, "monthly_pageview_limit":10000, "monthly_product_id":"857097", "yearly_product_id":"857079", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":100000, "monthly_product_id":"857098", "yearly_product_id":"857080", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":200000, "monthly_product_id":"857099", "yearly_product_id":"857081", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":500000, "monthly_product_id":"857100", "yearly_product_id":"857082", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":1000000, "monthly_product_id":"857101", "yearly_product_id":"857083", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":2000000, "monthly_product_id":"857102", "yearly_product_id":"857084", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":5000000, "monthly_product_id":"857103", "yearly_product_id":"857085", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":10000000, "monthly_product_id":"857104", "yearly_product_id":"857086", "site_limit":10, - "team_member_limit":5, + "team_member_limit":3, "features":["goals"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":10000, "monthly_product_id":"857105", "yearly_product_id":"857087", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":100000, "monthly_product_id":"857106", "yearly_product_id":"857088", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":200000, "monthly_product_id":"857107", "yearly_product_id":"857089", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":500000, "monthly_product_id":"857108", "yearly_product_id":"857090", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":1000000, "monthly_product_id":"857109", "yearly_product_id":"857091", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":2000000, "monthly_product_id":"857110", "yearly_product_id":"857092", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":5000000, "monthly_product_id":"857111", "yearly_product_id":"857093", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":10000000, "monthly_product_id":"857112", "yearly_product_id":"857094", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props","revenue_goals","funnels","stats_api"] } ] diff --git a/priv/sandbox_plans.json b/priv/sandbox_plans.json index b0af288193..2d2f8871e1 100644 --- a/priv/sandbox_plans.json +++ b/priv/sandbox_plans.json @@ -1,146 +1,162 @@ [ { "kind":"growth", + "generation":1, "monthly_pageview_limit":10000, "monthly_product_id":"63842", "yearly_product_id":"63859", - "site_limit":10, - "team_member_limit":5, + "site_limit":50, + "team_member_limit":"unlimited", "features":["goals","props","stats_api"] }, { "kind":"growth", + "generation":2, "monthly_pageview_limit":100000, "monthly_product_id":"63843", "yearly_product_id":"63860", - "site_limit":10, - "team_member_limit":5, + "site_limit":50, + "team_member_limit":"unlimited", "features":["goals","props","stats_api"] }, { "kind":"growth", + "generation":3, "monthly_pageview_limit":200000, "monthly_product_id":"63844", "yearly_product_id":"63861", - "site_limit":10, - "team_member_limit":5, + "site_limit":50, + "team_member_limit":"unlimited", "features":["goals","props","stats_api"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":500000, "monthly_product_id":"63845", "yearly_product_id":"63862", "site_limit":10, - "team_member_limit":5, - "features":["goals","props","stats_api"] + "team_member_limit":3, + "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":1000000, "monthly_product_id":"63846", "yearly_product_id":"63863", "site_limit":10, - "team_member_limit":5, - "features":["goals","props","stats_api"] + "team_member_limit":3, + "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":2000000, "monthly_product_id":"63847", "yearly_product_id":"63864", "site_limit":10, - "team_member_limit":5, - "features":["goals","props","stats_api"] + "team_member_limit":3, + "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":5000000, "monthly_product_id":"63848", "yearly_product_id":"63865", "site_limit":10, - "team_member_limit":5, - "features":["goals","props","stats_api"] + "team_member_limit":3, + "features":["goals"] }, { "kind":"growth", + "generation":4, "monthly_pageview_limit":10000000, "monthly_product_id":"63849", "yearly_product_id":"63866", "site_limit":10, - "team_member_limit":5, - "features":["goals","props","stats_api"] + "team_member_limit":3, + "features":["goals"] }, { "kind":"business", + "generation":3, "monthly_pageview_limit":10000, "monthly_product_id":"63850", "yearly_product_id":"63867", "site_limit":50, - "team_member_limit":50, + "team_member_limit":"unlimited", "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":3, "monthly_pageview_limit":100000, "monthly_product_id":"63851", "yearly_product_id":"63868", "site_limit":50, - "team_member_limit":50, + "team_member_limit":"unlimited", "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":3, "monthly_pageview_limit":200000, "monthly_product_id":"63852", "yearly_product_id":"63869", "site_limit":50, - "team_member_limit":50, + "team_member_limit":"unlimited", "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":500000, "monthly_product_id":"63853", "yearly_product_id":"63870", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":1000000, "monthly_product_id":"63854", "yearly_product_id":"63871", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":2000000, "monthly_product_id":"63855", "yearly_product_id":"63872", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":5000000, "monthly_product_id":"63856", "yearly_product_id":"63873", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props", "revenue_goals", "funnels","stats_api"] }, { "kind":"business", + "generation":4, "monthly_pageview_limit":10000000, "monthly_product_id":"63857", "yearly_product_id":"63874", "site_limit":50, - "team_member_limit":50, + "team_member_limit":10, "features":["goals","props", "revenue_goals", "funnels","stats_api"] } ] diff --git a/priv/unlisted_plans_v1.json b/priv/unlisted_plans_v1.json index e660b2bc69..b7fd14b27a 100644 --- a/priv/unlisted_plans_v1.json +++ b/priv/unlisted_plans_v1.json @@ -1,6 +1,7 @@ [ { "kind":"growth", + "generation":1, "monthly_pageview_limit":150000000, "yearly_product_id":"648089", "monthly_product_id":null, diff --git a/priv/unlisted_plans_v2.json b/priv/unlisted_plans_v2.json index 998d48a5c2..ff635879b7 100644 --- a/priv/unlisted_plans_v2.json +++ b/priv/unlisted_plans_v2.json @@ -1,6 +1,7 @@ [ { "kind":"growth", + "generation":2, "monthly_pageview_limit":10000000, "monthly_product_id":"655350", "yearly_product_id":null, diff --git a/test/plausible/billing/plans_test.exs b/test/plausible/billing/plans_test.exs index a0ce1a4b3d..d0f18e32ad 100644 --- a/test/plausible/billing/plans_test.exs +++ b/test/plausible/billing/plans_test.exs @@ -5,30 +5,22 @@ defmodule Plausible.Billing.PlansTest do @v1_plan_id "558018" @v2_plan_id "654177" @v4_plan_id "857097" + @v3_business_plan_id "857481" @v4_business_plan_id "857105" describe "getting subscription plans for user" do - test "growth_plans_for/1 shows v1 pricing for users who are already on v1 pricing" do + test "growth_plans_for/1 returns v1 plans for users who are already on v1 pricing" do user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id)) - assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id end - test "growth_plans_for/1 shows v2 pricing for users who are already on v2 pricing" do + test "growth_plans_for/1 returns v2 plans for users who are already on v2 pricing" do user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id)) - assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id end - test "growth_plans_for/1 shows v2 pricing for users who signed up in 2021" do - user = insert(:user, inserted_at: ~N[2021-12-31 00:00:00]) - - assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id - end - - test "growth_plans_for/1 shows v4 pricing for everyone else" do + test "growth_plans_for/1 returns v4 plans for everyone else" do user = insert(:user) - assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id end @@ -41,11 +33,28 @@ defmodule Plausible.Billing.PlansTest do end) end - test "business_plans/0 returns only v4 business plans" do - Plans.business_plans() - |> Enum.each(fn plan -> - assert plan.kind == :business - end) + test "growth_plans_for/1 returns the latest generation of growth plans for a user with a business subscription" do + user = + insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id)) + + assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id + end + + test "business_plans_for/1 returns v3 business plans for a legacy subscriber" do + user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id)) + + business_plans = Plans.business_plans_for(user) + + assert Enum.all?(business_plans, &(&1.kind == :business)) + assert List.first(business_plans).monthly_product_id == @v3_business_plan_id + end + + test "business_plans_for/1 returns v4 business plans for everyone else" do + user = insert(:user) + business_plans = Plans.business_plans_for(user) + + assert Enum.all?(business_plans, &(&1.kind == :business)) + assert List.first(business_plans).monthly_product_id == @v4_business_plan_id end test "available_plans_with_prices/1" do @@ -58,7 +67,7 @@ defmodule Plausible.Billing.PlansTest do end) assert Enum.find(business_plans, fn plan -> - (%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v4_business_plan_id + (%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v3_business_plan_id end) end @@ -177,6 +186,14 @@ defmodule Plausible.Billing.PlansTest do "749355", "749357", "749359", + "857482", + "857484", + "857487", + "857491", + "857494", + "857496", + "857500", + "857502", "857079", "857080", "857081", diff --git a/test/plausible/billing/quota_test.exs b/test/plausible/billing/quota_test.exs index a42c01ae6d..36542ba4cf 100644 --- a/test/plausible/billing/quota_test.exs +++ b/test/plausible/billing/quota_test.exs @@ -6,6 +6,7 @@ defmodule Plausible.Billing.QuotaTest do @v1_plan_id "558018" @v2_plan_id "654177" @v3_plan_id "749342" + @v3_business_plan_id "857481" describe "site_limit/1" do test "returns 50 when user is on an old plan" do @@ -356,8 +357,15 @@ defmodule Plausible.Billing.QuotaTest do user_on_business = insert(:user, subscription: build(:business_subscription)) - assert 5 == Quota.team_member_limit(user_on_growth) - assert 50 == Quota.team_member_limit(user_on_business) + assert 3 == Quota.team_member_limit(user_on_growth) + assert 10 == Quota.team_member_limit(user_on_business) + end + + test "returns unlimited when user is on a v3 business plan" do + user = + insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id)) + + assert :unlimited == Quota.team_member_limit(user) end end diff --git a/test/plausible_web/live/choose_plan_test.exs b/test/plausible_web/live/choose_plan_test.exs index 7b58423395..9583b5bebf 100644 --- a/test/plausible_web/live/choose_plan_test.exs +++ b/test/plausible_web/live/choose_plan_test.exs @@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do @v1_10k_yearly_plan_id "572810" @v4_growth_200k_yearly_plan_id "857081" @v4_business_5m_monthly_plan_id "857111" + @v3_business_10k_monthly_plan_id "857481" @monthly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="monthly"]/ @yearly_interval_button ~s/label[phx-click="set_interval"][phx-value-interval="yearly"]/ @@ -27,6 +28,8 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do @business_current_label "#{@business_plan_box} #current-label" @business_checkout_button "#business-checkout" + @enterprise_plan_box "#enterprise-plan-box" + describe "for a user with no subscription" do setup [:create_user, :log_in] @@ -41,6 +44,41 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do assert doc =~ "+ VAT if applicable" end + test "displays plan benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + growth_box = text_of_element(doc, @growth_plan_box) + business_box = text_of_element(doc, @business_plan_box) + enterprise_box = text_of_element(doc, @enterprise_plan_box) + + assert growth_box =~ "Up to 3 team members" + assert growth_box =~ "Up to 10 sites" + assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard" + assert growth_box =~ "Email/Slack reports" + assert growth_box =~ "Google Analytics import" + assert growth_box =~ "Goals and custom events" + + assert business_box =~ "Everything in Growth" + assert business_box =~ "Up to 10 team members" + assert business_box =~ "Up to 50 sites" + assert business_box =~ "Stats API" + assert business_box =~ "Custom Properties" + assert business_box =~ "Funnels" + assert business_box =~ "Ecommerce revenue attribution" + assert business_box =~ "Priority support" + + refute business_box =~ "Goals and custom events" + + assert enterprise_box =~ "Everything in Business" + assert enterprise_box =~ "10+ team members" + assert enterprise_box =~ "50+ sites" + assert enterprise_box =~ "Sites API access for" + assert enterprise_box =~ "Technical onboarding" + + assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~ + "https://plausible.io/white-label-web-analytics" + end + test "default billing interval is monthly, and can switch to yearly", %{conn: conn} do {:ok, lv, doc} = get_liveview(conn) @@ -107,16 +145,16 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do doc = lv |> element(@slider_input) |> render_change(%{slider: 8}) - assert text_of_element(doc, @growth_plan_box) =~ "Custom" + assert text_of_element(doc, "#growth-custom-price") =~ "Custom" assert text_of_element(doc, @growth_plan_box) =~ "Contact us" - assert text_of_element(doc, @business_plan_box) =~ "Custom" + assert text_of_element(doc, "#business-custom-price") =~ "Custom" assert text_of_element(doc, @business_plan_box) =~ "Contact us" doc = lv |> element(@slider_input) |> render_change(%{slider: 7}) - refute text_of_element(doc, @growth_plan_box) =~ "Custom" + refute text_of_element(doc, "#growth-custom-price") =~ "Custom" refute text_of_element(doc, @growth_plan_box) =~ "Contact us" - refute text_of_element(doc, @business_plan_box) =~ "Custom" + refute text_of_element(doc, "#business-custom-price") =~ "Custom" refute text_of_element(doc, @business_plan_box) =~ "Contact us" end @@ -175,6 +213,41 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do refute doc =~ "What happens if I go over my page views limit?" end + test "displays plan benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + growth_box = text_of_element(doc, @growth_plan_box) + business_box = text_of_element(doc, @business_plan_box) + enterprise_box = text_of_element(doc, @enterprise_plan_box) + + assert growth_box =~ "Up to 3 team members" + assert growth_box =~ "Up to 10 sites" + assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard" + assert growth_box =~ "Email/Slack reports" + assert growth_box =~ "Google Analytics import" + assert growth_box =~ "Goals and custom events" + + assert business_box =~ "Everything in Growth" + assert business_box =~ "Up to 10 team members" + assert business_box =~ "Up to 50 sites" + assert business_box =~ "Stats API" + assert business_box =~ "Custom Properties" + assert business_box =~ "Funnels" + assert business_box =~ "Ecommerce revenue attribution" + assert business_box =~ "Priority support" + + refute business_box =~ "Goals and custom events" + + assert enterprise_box =~ "Everything in Business" + assert enterprise_box =~ "10+ team members" + assert enterprise_box =~ "50+ sites" + assert enterprise_box =~ "Sites API access for" + assert enterprise_box =~ "Technical onboarding" + + assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~ + "https://plausible.io/white-label-web-analytics" + end + test "displays usage", %{conn: conn, user: user} do site = insert(:site, members: [user]) @@ -302,6 +375,51 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do end end + describe "for a user with a v3 business (unlimited team members) subscription plan" do + setup [:create_user, :log_in] + + setup %{user: user} = context do + create_subscription_for(user, paddle_plan_id: @v3_business_10k_monthly_plan_id) + {:ok, context} + end + + test "displays plan benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + growth_box = text_of_element(doc, @growth_plan_box) + business_box = text_of_element(doc, @business_plan_box) + enterprise_box = text_of_element(doc, @enterprise_plan_box) + + assert growth_box =~ "Up to 3 team members" + assert growth_box =~ "Up to 10 sites" + assert growth_box =~ "Intuitive, fast and privacy-friendly dashboard" + assert growth_box =~ "Email/Slack reports" + assert growth_box =~ "Google Analytics import" + assert growth_box =~ "Goals and custom events" + + assert business_box =~ "Everything in Growth" + assert business_box =~ "Unlimited team members" + assert business_box =~ "Up to 50 sites" + assert business_box =~ "Stats API" + assert business_box =~ "Custom Properties" + assert business_box =~ "Funnels" + assert business_box =~ "Ecommerce revenue attribution" + assert business_box =~ "Priority support" + + refute business_box =~ "Goals and custom events" + + assert enterprise_box =~ "Everything in Business" + assert enterprise_box =~ "50+ sites" + assert enterprise_box =~ "Sites API access for" + assert enterprise_box =~ "Technical onboarding" + + refute enterprise_box =~ "team members" + + assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~ + "https://plausible.io/white-label-web-analytics" + end + end + describe "for a user with a past_due subscription" do setup [:create_user, :log_in, :create_past_due_subscription] @@ -391,10 +509,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do describe "for a grandfathered user" do setup [:create_user, :log_in] - test "on a v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not", - %{conn: conn, user: user} do + setup %{user: user} = context do create_subscription_for(user, paddle_plan_id: @v1_10k_yearly_plan_id) + {:ok, context} + end + test "on a v1 plan, Growth tiers are available at 20M, 50M, 50M+, but Business tiers are not", + %{conn: conn} do {:ok, lv, _doc} = get_liveview(conn) doc = lv |> element(@slider_input) |> render_change(%{slider: 8}) @@ -419,6 +540,42 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do refute text_of_element(doc, @business_plan_box) =~ "Contact us" refute text_of_element(doc, @growth_plan_box) =~ "Contact us" end + + test "displays grandfathering notice in the Growth box instead of benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + growth_box = text_of_element(doc, @growth_plan_box) + assert growth_box =~ "Your subscription has been grandfathered" + refute growth_box =~ "Intuitive, fast and privacy-friendly dashboard" + end + + test "displays business and enterprise plan benefits", %{conn: conn} do + {:ok, _lv, doc} = get_liveview(conn) + + business_box = text_of_element(doc, @business_plan_box) + enterprise_box = text_of_element(doc, @enterprise_plan_box) + + assert business_box =~ "Everything in Growth" + assert business_box =~ "Funnels" + assert business_box =~ "Ecommerce revenue attribution" + assert business_box =~ "Priority support" + + refute business_box =~ "Goals and custom events" + refute business_box =~ "Unlimited team members" + refute business_box =~ "Up to 50 sites" + refute business_box =~ "Stats API" + refute business_box =~ "Custom Properties" + + assert enterprise_box =~ "Everything in Business" + assert enterprise_box =~ "50+ sites" + assert enterprise_box =~ "Sites API access for" + assert enterprise_box =~ "Technical onboarding" + + assert text_of_attr(find(doc, "#{@enterprise_plan_box} p a"), "href") =~ + "https://plausible.io/white-label-web-analytics" + + refute enterprise_box =~ "10+ team members" + refute enterprise_box =~ "Unlimited team members" + end end describe "for a free_10k subscription" do