Always recommend a suitable plan on the choose-plan page (#4222)

* pull last_bill_date from paddle sandbox in mix task

* move cycle usage checks to Quota module

* move quota.ex to a subfolder

* split up Quota module

* set choose-plan pageview slider according to usage

* silence credo
This commit is contained in:
RobertJoonas 2024-06-17 09:25:46 +03:00 committed by GitHub
parent 86d7031336
commit dd1d74ccb7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 486 additions and 446 deletions

View File

@ -48,6 +48,7 @@ defmodule Mix.Tasks.PullSandboxSubscription do
update_url: res["update_url"],
user_id: user.id,
status: res["state"],
last_bill_date: res["last_payment"]["date"],
next_bill_date: res["next_payment"]["date"],
next_bill_amount: res["next_payment"]["amount"] |> to_string(),
currency_code: res["next_payment"]["currency"]

View File

@ -132,7 +132,7 @@ defmodule Plausible.Billing.Feature do
def check_availability(%Plausible.Auth.User{} = user) do
cond do
free?() -> :ok
__MODULE__ in Quota.allowed_features_for(user) -> :ok
__MODULE__ in Quota.Limits.allowed_features_for(user) -> :ok
true -> {:error, :upgrade_required}
end
end

View File

@ -235,7 +235,7 @@ defmodule Plausible.Billing.Plans do
[]
end
if Enum.any?(Quota.features_usage(user), &(&1 not in growth_features)) do
if Enum.any?(Quota.Usage.features_usage(user), &(&1 not in growth_features)) do
:business
else
:growth

View File

@ -0,0 +1,120 @@
defmodule Plausible.Billing.Quota.Limits do
@moduledoc false
use Plausible
alias Plausible.Users
alias Plausible.Auth.User
alias Plausible.Billing.{Plan, Plans, Subscription, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI}
@type over_limits_error() :: {:over_plan_limits, [limit()]}
@typep limit() :: :site_limit | :pageview_limit | :team_member_limit
@pageview_allowance_margin 0.1
on_ee do
@limit_sites_since ~D[2021-05-05]
@site_limit_for_trials 10
@team_member_limit_for_trials 3
@spec site_limit(User.t()) :: non_neg_integer() | :unlimited
def site_limit(user) do
if Timex.before?(user.inserted_at, @limit_sites_since) do
:unlimited
else
get_site_limit_from_plan(user)
end
end
defp get_site_limit_from_plan(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{site_limit: site_limit} -> site_limit
:free_10k -> 50
nil -> @site_limit_for_trials
end
end
@spec team_member_limit(User.t()) :: non_neg_integer()
def team_member_limit(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{team_member_limit: limit} -> limit
:free_10k -> :unlimited
nil -> @team_member_limit_for_trials
end
end
else
def site_limit(_) do
:unlimited
end
def team_member_limit(_) do
:unlimited
end
end
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
@spec monthly_pageview_limit(User.t() | Subscription.t()) ::
non_neg_integer() | :unlimited
def monthly_pageview_limit(%User{} = user) do
user = Users.with_subscription(user)
monthly_pageview_limit(user.subscription)
end
def monthly_pageview_limit(subscription) do
case Plans.get_subscription_plan(subscription) do
%EnterprisePlan{monthly_pageview_limit: limit} ->
limit
%Plan{monthly_pageview_limit: limit} ->
limit
:free_10k ->
@monthly_pageview_limit_for_free_10k
_any ->
if subscription do
Sentry.capture_message("Unknown monthly pageview limit for plan",
extra: %{paddle_plan_id: subscription.paddle_plan_id}
)
end
@monthly_pageview_limit_for_trials
end
end
def pageview_limit_with_margin(limit, margin \\ nil) do
margin = if margin, do: margin, else: @pageview_allowance_margin
ceil(limit * (1 + margin))
end
@doc """
Returns a list of features the user can use. Trial users have the
ability to use all features during their trial.
"""
def allowed_features_for(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%EnterprisePlan{features: features} ->
features
%Plan{features: features} ->
features
:free_10k ->
[Goals, Props, StatsAPI]
nil ->
if Users.on_trial?(user) do
Feature.list()
else
[Goals]
end
end
end
end

View File

@ -0,0 +1,123 @@
defmodule Plausible.Billing.Quota do
@moduledoc """
This module provides functions to work with plans usage and limits.
"""
use Plausible
alias Plausible.Users
alias Plausible.Auth.User
alias Plausible.Billing.{Plan, Plans, EnterprisePlan}
alias Plausible.Billing.Quota.{Usage, Limits}
@doc """
Enterprise plans are always allowed to add more sites (even when
over limit) to avoid service disruption. Their usage is checked
in a background job instead (see `check_usage.ex`).
"""
def ensure_can_add_new_site(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%EnterprisePlan{} ->
:ok
_ ->
usage = Usage.site_usage(user)
limit = Limits.site_limit(user)
if below_limit?(usage, limit), do: :ok, else: {:error, {:over_limit, limit}}
end
end
@doc """
Ensures that the given user (or the usage map) is within the limits
of the given plan.
An `opts` argument can be passed with `ignore_pageview_limit: true`
which bypasses the pageview limit check and returns `:ok` as long as
the other limits are not exceeded.
"""
@spec ensure_within_plan_limits(User.t() | map(), struct() | atom() | nil, Keyword.t()) ::
:ok | {:error, Limits.over_limits_error()}
def ensure_within_plan_limits(user_or_usage, plan, opts \\ [])
def ensure_within_plan_limits(%User{} = user, %plan_mod{} = plan, opts)
when plan_mod in [Plan, EnterprisePlan] do
ensure_within_plan_limits(Usage.usage(user), plan, opts)
end
def ensure_within_plan_limits(usage, %plan_mod{} = plan, opts)
when plan_mod in [Plan, EnterprisePlan] do
case exceeded_limits(usage, plan, opts) do
[] -> :ok
exceeded_limits -> {:error, {:over_plan_limits, exceeded_limits}}
end
end
def ensure_within_plan_limits(_, _, _), do: :ok
defp exceeded_limits(usage, plan, opts) do
for {limit, exceeded?} <- [
{:team_member_limit, not within_limit?(usage.team_members, plan.team_member_limit)},
{:site_limit, not within_limit?(usage.sites, plan.site_limit)},
{:monthly_pageview_limit,
exceeds_monthly_pageview_limit?(usage.monthly_pageviews, plan, opts)}
],
exceeded? do
limit
end
end
defp exceeds_monthly_pageview_limit?(usage, plan, opts) do
if Keyword.get(opts, :ignore_pageview_limit) do
false
else
case usage do
%{last_30_days: %{total: total}} ->
margin = Keyword.get(opts, :pageview_allowance_margin)
limit = Limits.pageview_limit_with_margin(plan.monthly_pageview_limit, margin)
!within_limit?(total, limit)
cycles_usage ->
exceeds_last_two_usage_cycles?(cycles_usage, plan.monthly_pageview_limit)
end
end
end
@spec exceeds_last_two_usage_cycles?(Usage.cycles_usage(), non_neg_integer()) :: boolean()
def exceeds_last_two_usage_cycles?(cycles_usage, allowed_volume) do
exceeded = exceeded_cycles(cycles_usage, allowed_volume)
:penultimate_cycle in exceeded && :last_cycle in exceeded
end
@spec exceeded_cycles(Usage.cycles_usage(), non_neg_integer()) :: list()
def exceeded_cycles(cycles_usage, allowed_volume) do
limit = Limits.pageview_limit_with_margin(allowed_volume)
Enum.reduce(cycles_usage, [], fn {cycle, %{total: total}}, exceeded_cycles ->
if below_limit?(total, limit) do
exceeded_cycles
else
exceeded_cycles ++ [cycle]
end
end)
end
@spec below_limit?(non_neg_integer(), non_neg_integer() | :unlimited) :: boolean()
@doc """
Returns whether the usage is below the limit or not.
Returns false if usage is equal to the limit.
"""
def below_limit?(usage, limit) do
if limit == :unlimited, do: true, else: usage < limit
end
@spec within_limit?(non_neg_integer(), non_neg_integer() | :unlimited) :: boolean()
@doc """
Returns whether the usage is within the limit or not.
Returns true if usage is equal to the limit.
"""
def within_limit?(usage, limit) do
if limit == :unlimited, do: true, else: usage <= limit
end
end

View File

@ -1,34 +1,26 @@
defmodule Plausible.Billing.Quota do
@moduledoc """
This module provides functions to work with plans usage and limits.
"""
defmodule Plausible.Billing.Quota.Usage do
@moduledoc false
use Plausible
import Ecto.Query
alias Plausible.Users
alias Plausible.Auth.User
alias Plausible.Site
alias Plausible.Billing.{Plan, Plans, Subscription, Subscriptions, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, RevenueGoals, Funnels, Props, StatsAPI}
alias Plausible.Billing.{Subscriptions}
alias Plausible.Billing.Feature.{RevenueGoals, Funnels, Props, StatsAPI}
@type limit() :: :site_limit | :pageview_limit | :team_member_limit
@type cycles_usage() :: %{cycle() => usage_cycle()}
@type over_limits_error() :: {:over_plan_limits, [limit()]}
@typep cycle :: :current_cycle | :last_cycle | :penultimate_cycle
@typep last_30_days_usage() :: %{:last_30_days => usage_cycle()}
@typep monthly_pageview_usage() :: cycles_usage() | last_30_days_usage()
@type monthly_pageview_usage() :: %{period() => usage_cycle()}
@type period :: :last_30_days | :current_cycle | :last_cycle | :penultimate_cycle
@type usage_cycle :: %{
date_range: Date.Range.t(),
pageviews: non_neg_integer(),
custom_events: non_neg_integer(),
total: non_neg_integer()
}
@pageview_allowance_margin 0.1
def pageview_allowance_margin(), do: @pageview_allowance_margin
@typep usage_cycle :: %{
date_range: Date.Range.t(),
pageviews: non_neg_integer(),
custom_events: non_neg_integer(),
total: non_neg_integer()
}
def usage(user, opts \\ []) do
basic_usage = %{
@ -45,50 +37,6 @@ defmodule Plausible.Billing.Quota do
end
end
on_ee do
@limit_sites_since ~D[2021-05-05]
@site_limit_for_trials 10
@team_member_limit_for_trials 3
@spec site_limit(User.t()) :: non_neg_integer() | :unlimited
def site_limit(user) do
if Timex.before?(user.inserted_at, @limit_sites_since) do
:unlimited
else
get_site_limit_from_plan(user)
end
end
defp get_site_limit_from_plan(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{site_limit: site_limit} -> site_limit
:free_10k -> 50
nil -> @site_limit_for_trials
end
end
@spec team_member_limit(User.t()) :: non_neg_integer()
def team_member_limit(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{team_member_limit: limit} -> limit
:free_10k -> :unlimited
nil -> @team_member_limit_for_trials
end
end
else
def site_limit(_) do
:unlimited
end
def team_member_limit(_) do
:unlimited
end
end
@spec site_usage(User.t()) :: non_neg_integer()
@doc """
Returns the number of sites the given user owns.
@ -97,58 +45,6 @@ defmodule Plausible.Billing.Quota do
Plausible.Sites.owned_sites_count(user)
end
@doc """
Enterprise plans are always allowed to add more sites (even when
over limit) to avoid service disruption. Their usage is checked
in a background job instead (see `check_usage.ex`).
"""
def ensure_can_add_new_site(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%EnterprisePlan{} ->
:ok
_ ->
usage = site_usage(user)
limit = site_limit(user)
if below_limit?(usage, limit), do: :ok, else: {:error, {:over_limit, limit}}
end
end
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
@spec monthly_pageview_limit(User.t() | Subscription.t()) ::
non_neg_integer() | :unlimited
def monthly_pageview_limit(%User{} = user) do
user = Users.with_subscription(user)
monthly_pageview_limit(user.subscription)
end
def monthly_pageview_limit(subscription) do
case Plans.get_subscription_plan(subscription) do
%EnterprisePlan{monthly_pageview_limit: limit} ->
limit
%Plan{monthly_pageview_limit: limit} ->
limit
:free_10k ->
@monthly_pageview_limit_for_free_10k
_any ->
if subscription do
Sentry.capture_message("Unknown monthly pageview limit for plan",
extra: %{paddle_plan_id: subscription.paddle_plan_id}
)
end
@monthly_pageview_limit_for_trials
end
end
@doc """
Queries the ClickHouse database for the monthly pageview usage. If the given user's
subscription is `active`, `past_due`, or a `deleted` (but not yet expired), a map
@ -195,8 +91,7 @@ defmodule Plausible.Billing.Quota do
end
end
@spec usage_cycle(User.t(), period(), list() | nil, Date.t()) :: usage_cycle()
@spec usage_cycle(User.t(), :last_30_days | cycle(), list() | nil, Date.t()) :: usage_cycle()
def usage_cycle(user, cycle, owned_site_ids \\ nil, today \\ Timex.today())
def usage_cycle(user, cycle, nil, today) do
@ -398,114 +293,9 @@ defmodule Plausible.Billing.Quota do
for {f_mod, used?} <- used_features, used?, f_mod.enabled?(site), do: f_mod
end
@doc """
Ensures that the given user (or the usage map) is within the limits
of the given plan.
An `opts` argument can be passed with `ignore_pageview_limit: true`
which bypasses the pageview limit check and returns `:ok` as long as
the other limits are not exceeded.
"""
@spec ensure_within_plan_limits(User.t() | map(), struct() | atom() | nil, Keyword.t()) ::
:ok | {:error, over_limits_error()}
def ensure_within_plan_limits(user_or_usage, plan, opts \\ [])
def ensure_within_plan_limits(%User{} = user, %plan_mod{} = plan, opts)
when plan_mod in [Plan, EnterprisePlan] do
ensure_within_plan_limits(usage(user), plan, opts)
end
def ensure_within_plan_limits(usage, %plan_mod{} = plan, opts)
when plan_mod in [Plan, EnterprisePlan] do
case exceeded_limits(usage, plan, opts) do
[] -> :ok
exceeded_limits -> {:error, {:over_plan_limits, exceeded_limits}}
end
end
def ensure_within_plan_limits(_, _, _), do: :ok
defp exceeded_limits(usage, plan, opts) do
for {limit, exceeded?} <- [
{:team_member_limit, not within_limit?(usage.team_members, plan.team_member_limit)},
{:site_limit, not within_limit?(usage.sites, plan.site_limit)},
{:monthly_pageview_limit,
exceeds_monthly_pageview_limit?(usage.monthly_pageviews, plan, opts)}
],
exceeded? do
limit
end
end
defp exceeds_monthly_pageview_limit?(usage, plan, opts) do
if Keyword.get(opts, :ignore_pageview_limit) do
false
else
case usage do
%{last_30_days: %{total: total}} ->
!within_limit?(total, pageview_limit_with_margin(plan, opts))
billing_cycles_usage ->
Plausible.Workers.CheckUsage.exceeds_last_two_usage_cycles?(
billing_cycles_usage,
plan.monthly_pageview_limit
)
end
end
end
defp pageview_limit_with_margin(%{monthly_pageview_limit: limit}, opts) do
margin = Keyword.get(opts, :pageview_allowance_margin, @pageview_allowance_margin)
ceil(limit * (1 + margin))
end
@doc """
Returns a list of features the user can use. Trial users have the
ability to use all features during their trial.
"""
def allowed_features_for(user) do
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%EnterprisePlan{features: features} ->
features
%Plan{features: features} ->
features
:free_10k ->
[Goals, Props, StatsAPI]
nil ->
if Users.on_trial?(user) do
Feature.list()
else
[Goals]
end
end
end
defp owned_sites_query(user) do
from sm in Site.Membership,
where: sm.role == :owner and sm.user_id == ^user.id,
select: %{site_id: sm.site_id}
end
@spec below_limit?(non_neg_integer(), non_neg_integer() | :unlimited) :: boolean()
@doc """
Returns whether the usage is below the limit or not.
Returns false if usage is equal to the limit.
"""
def below_limit?(usage, limit) do
if limit == :unlimited, do: true, else: usage < limit
end
@spec within_limit?(non_neg_integer(), non_neg_integer() | :unlimited) :: boolean()
@doc """
Returns whether the usage is within the limit or not.
Returns true if usage is equal to the limit.
"""
def within_limit?(usage, limit) do
if limit == :unlimited, do: true, else: usage <= limit
end
end

View File

@ -68,7 +68,7 @@ defmodule Plausible.Billing.SiteLocker do
@spec send_grace_period_end_email(Plausible.Auth.User.t()) :: Plausible.Mailer.result()
def send_grace_period_end_email(user) do
usage = Plausible.Billing.Quota.monthly_pageview_usage(user)
usage = Plausible.Billing.Quota.Usage.monthly_pageview_usage(user)
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.last_cycle.total)
PlausibleWeb.Email.dashboard_locked(user, usage, suggested_plan)

View File

@ -26,7 +26,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
@spec transfer_ownership(Site.t(), Auth.User.t()) ::
{:ok, Site.Membership.t()}
| {:error,
Billing.Quota.over_limits_error()
Billing.Quota.Limits.over_limits_error()
| Ecto.Changeset.t()
| :transfer_to_self
| :no_plan}
@ -54,7 +54,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
{:ok, Site.Membership.t()}
| {:error,
:invitation_not_found
| Billing.Quota.over_limits_error()
| Billing.Quota.Limits.over_limits_error()
| Ecto.Changeset.t()
| :no_plan}
def accept_invitation(invitation_id, user) do

View File

@ -41,7 +41,7 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
{:ok, [Membership.t()]}
| {:error,
invite_error()
| Quota.over_limits_error()}
| Quota.Limits.over_limits_error()}
def bulk_transfer_ownership_direct(sites, new_owner) do
Plausible.Repo.transaction(fn ->
for site <- sites do
@ -134,8 +134,8 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
defp check_team_member_limit(site, _role, invitee_email) do
site = Plausible.Repo.preload(site, :owner)
limit = Quota.team_member_limit(site.owner)
usage = Quota.team_member_usage(site.owner, exclude_emails: invitee_email)
limit = Quota.Limits.team_member_limit(site.owner)
usage = Quota.Usage.team_member_usage(site.owner, exclude_emails: invitee_email)
if Quota.below_limit?(usage, limit),
do: :ok,

View File

@ -66,7 +66,7 @@ defmodule Plausible.Site.Memberships.Invitations do
on_ee do
@spec ensure_can_take_ownership(Site.t(), Auth.User.t()) ::
:ok | {:error, Quota.over_limits_error() | :no_plan}
:ok | {:error, Quota.Limits.over_limits_error() | :no_plan}
def ensure_can_take_ownership(site, new_owner) do
site = Repo.preload(site, :owner)
new_owner = Plausible.Users.with_subscription(new_owner)
@ -78,7 +78,7 @@ defmodule Plausible.Site.Memberships.Invitations do
usage_after_transfer = %{
monthly_pageviews: monthly_pageview_usage_after_transfer(site, new_owner),
team_members: team_member_usage_after_transfer(site, new_owner),
sites: Quota.site_usage(new_owner) + 1
sites: Quota.Usage.site_usage(new_owner) + 1
}
Quota.ensure_within_plan_limits(usage_after_transfer, plan)
@ -88,8 +88,8 @@ defmodule Plausible.Site.Memberships.Invitations do
end
defp team_member_usage_after_transfer(site, new_owner) do
current_usage = Quota.team_member_usage(new_owner)
site_usage = Quota.team_member_usage(site.owner, site: site)
current_usage = Quota.Usage.team_member_usage(new_owner)
site_usage = Quota.Usage.team_member_usage(site.owner, site: site)
extra_usage =
if Plausible.Sites.is_member?(new_owner.id, site), do: 0, else: 1
@ -99,7 +99,7 @@ defmodule Plausible.Site.Memberships.Invitations do
defp monthly_pageview_usage_after_transfer(site, new_owner) do
site_ids = Plausible.Sites.owned_site_ids(new_owner) ++ [site.id]
Quota.monthly_pageview_usage(new_owner, site_ids)
Quota.Usage.monthly_pageview_usage(new_owner, site_ids)
end
else
@spec ensure_can_take_ownership(Site.t(), Auth.User.t()) :: :ok
@ -117,7 +117,7 @@ defmodule Plausible.Site.Memberships.Invitations do
def check_feature_access(site, new_owner, false = _selfhost?) do
missing_features =
site
|> Quota.features_usage()
|> Quota.Usage.features_usage()
|> Enum.filter(&(&1.check_availability(new_owner) != :ok))
if missing_features == [] do

View File

@ -9,12 +9,12 @@ defmodule PlausibleWeb.AdminController do
|> String.to_integer()
|> Plausible.Users.with_subscription()
usage = Quota.usage(user, with_features: true)
usage = Quota.Usage.usage(user, with_features: true)
limits = %{
monthly_pageviews: Quota.monthly_pageview_limit(user),
sites: Quota.site_limit(user),
team_members: Quota.team_member_limit(user)
monthly_pageviews: Quota.Limits.monthly_pageview_limit(user),
sites: Quota.Limits.site_limit(user),
team_members: Quota.Limits.team_member_limit(user)
}
html_response = usage_and_limits_html(user, usage, limits)

View File

@ -633,12 +633,12 @@ defmodule PlausibleWeb.AuthController do
subscription: user.subscription,
invoices: Plausible.Billing.paddle_api().get_invoices(user.subscription),
theme: user.theme || "system",
team_member_limit: Quota.team_member_limit(user),
team_member_usage: Quota.team_member_usage(user),
site_limit: Quota.site_limit(user),
site_usage: Quota.site_usage(user),
pageview_limit: Quota.monthly_pageview_limit(user),
pageview_usage: Quota.monthly_pageview_usage(user),
team_member_limit: Quota.Limits.team_member_limit(user),
team_member_usage: Quota.Usage.team_member_usage(user),
site_limit: Quota.Limits.site_limit(user),
site_usage: Quota.Usage.site_usage(user),
pageview_limit: Quota.Limits.monthly_pageview_limit(user),
pageview_usage: Quota.Usage.monthly_pageview_usage(user),
totp_enabled?: Auth.TOTP.enabled?(user)
)
end

View File

@ -29,8 +29,8 @@ defmodule PlausibleWeb.Site.MembershipController do
|> Sites.get_for_user!(conn.assigns.site.domain)
|> Plausible.Repo.preload(:owner)
limit = Plausible.Billing.Quota.team_member_limit(site.owner)
usage = Plausible.Billing.Quota.team_member_usage(site.owner)
limit = Plausible.Billing.Quota.Limits.team_member_limit(site.owner)
usage = Plausible.Billing.Quota.Usage.team_member_usage(site.owner)
below_limit? = Plausible.Billing.Quota.below_limit?(usage, limit)
render(

View File

@ -18,8 +18,8 @@ defmodule PlausibleWeb.SiteController do
render(conn, "new.html",
changeset: Plausible.Site.changeset(%Plausible.Site{}),
first_site?: Quota.site_usage(current_user) == 0,
site_limit: Quota.site_limit(current_user),
first_site?: Quota.Usage.site_usage(current_user) == 0,
site_limit: Quota.Limits.site_limit(current_user),
site_limit_exceeded?: Quota.ensure_can_add_new_site(current_user) != :ok,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
@ -27,7 +27,7 @@ defmodule PlausibleWeb.SiteController do
def create_site(conn, %{"site" => site_params}) do
user = conn.assigns[:current_user]
first_site? = Quota.site_usage(user) == 0
first_site? = Quota.Usage.site_usage(user) == 0
case Sites.create(user, site_params) do
{:ok, %{site: site}} ->
@ -53,7 +53,7 @@ defmodule PlausibleWeb.SiteController do
render(conn, "new.html",
changeset: changeset,
first_site?: first_site?,
site_limit: Quota.site_limit(user),
site_limit: Quota.Limits.site_limit(user),
site_limit_exceeded?: false,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)

View File

@ -10,7 +10,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
alias PlausibleWeb.Components.Billing.{PlanBox, PlanBenefits, Notice, PageviewSlider}
alias Plausible.Site
alias Plausible.Users
alias Plausible.Billing.{Plans, Plan, Quota}
alias Plausible.Billing.{Plans, Quota}
@contact_link "https://plausible.io/contact"
@billing_faq_link "https://plausible.io/docs/billing"
@ -22,13 +22,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
Users.with_subscription(user_id)
end)
|> assign_new(:usage, fn %{user: user} ->
Quota.usage(user, with_features: true)
end)
|> assign_new(:last_30_days_usage, fn %{user: user, usage: usage} ->
case usage do
%{last_30_days: usage_cycle} -> usage_cycle.total
_ -> Quota.usage_cycle(user, :last_30_days).total
end
Quota.Usage.usage(user, with_features: true)
end)
|> assign_new(:owned_plan, fn %{user: %{subscription: subscription}} ->
Plans.get_regular_plan(subscription, only_non_expired: true)
@ -63,11 +57,10 @@ defmodule PlausibleWeb.Live.ChoosePlan do
get_available_volumes(available_plans)
end)
|> assign_new(:selected_volume, fn %{
owned_plan: owned_plan,
last_30_days_usage: last_30_days_usage,
usage: usage,
available_volumes: available_volumes
} ->
default_selected_volume(owned_plan, last_30_days_usage, available_volumes)
default_selected_volume(usage.monthly_pageviews, available_volumes)
end)
|> assign_new(:selected_interval, fn %{current_interval: current_interval} ->
current_interval || :monthly
@ -149,8 +142,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<PlanBox.enterprise benefits={@enterprise_benefits} />
</div>
<p class="mx-auto mt-8 max-w-2xl text-center text-lg leading-8 text-gray-600 dark:text-gray-400">
You have used <b><%= PlausibleWeb.AuthView.delimit_integer(@last_30_days_usage) %></b>
billable pageviews in the last 30 days
<.render_usage pageview_usage={@usage.monthly_pageviews} />
</p>
<.pageview_limit_notice :if={!@owned_plan} />
<.help_links />
@ -160,6 +152,22 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
end
defp render_usage(assigns) do
case assigns.pageview_usage do
%{last_30_days: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_30_days.total) %></b> billable pageviews in the last 30 days
"""
%{last_cycle: _} ->
~H"""
You have used
<b><%= PlausibleWeb.AuthView.delimit_integer(@pageview_usage.last_cycle.total) %></b> billable pageviews in the last billing cycle
"""
end
end
def handle_event("set_interval", %{"interval" => interval}, socket) do
new_interval =
case interval do
@ -189,10 +197,14 @@ defmodule PlausibleWeb.Live.ChoosePlan do
)}
end
defp default_selected_volume(%Plan{monthly_pageview_limit: limit}, _, _), do: limit
defp default_selected_volume(pageview_usage, available_volumes) do
total =
case pageview_usage do
%{last_30_days: usage} -> usage.total
%{last_cycle: usage} -> usage.total
end
defp default_selected_volume(_, last_30_days_usage, available_volumes) do
Enum.find(available_volumes, &(last_30_days_usage < &1)) || :enterprise
Enum.find(available_volumes, &(total < &1)) || :enterprise
end
defp current_user_subscription_interval(subscription) do

View File

@ -10,7 +10,7 @@ defmodule PlausibleWeb.AuthView do
def subscription_quota(subscription, options) do
subscription
|> Plausible.Billing.Quota.monthly_pageview_limit()
|> Plausible.Billing.Quota.Limits.monthly_pageview_limit()
|> PlausibleWeb.StatsView.large_number_format()
|> then(fn quota ->
if Keyword.get(options, :format) == :long,

View File

@ -33,7 +33,7 @@ defmodule Plausible.Workers.CheckUsage do
end
@impl Oban.Worker
def perform(_job, quota_mod \\ Quota, today \\ Timex.today()) do
def perform(_job, usage_mod \\ Quota.Usage, today \\ Timex.today()) do
yesterday = today |> Timex.shift(days: -1)
active_subscribers =
@ -56,13 +56,13 @@ defmodule Plausible.Workers.CheckUsage do
for subscriber <- active_subscribers do
case {subscriber.grace_period, subscriber.enterprise_plan} do
{nil, nil} ->
check_regular_subscriber(subscriber, quota_mod)
check_regular_subscriber(subscriber, usage_mod)
{nil, _} ->
check_enterprise_subscriber(subscriber, quota_mod)
check_enterprise_subscriber(subscriber, usage_mod)
{_, nil} ->
maybe_remove_grace_period(subscriber, quota_mod)
maybe_remove_grace_period(subscriber, usage_mod)
_ ->
:skip
@ -72,29 +72,9 @@ defmodule Plausible.Workers.CheckUsage do
:ok
end
@spec exceeds_last_two_usage_cycles?(Quota.monthly_pageview_usage(), non_neg_integer()) ::
boolean()
def exceeds_last_two_usage_cycles?(usage, limit) when is_integer(limit) do
limit = ceil(limit * (1 + Quota.pageview_allowance_margin()))
Enum.all?([usage.last_cycle, usage.penultimate_cycle], fn usage ->
not Quota.below_limit?(usage.total, limit)
end)
end
@spec last_usage_cycle_below_limit?(Quota.monthly_pageview_usage(), non_neg_integer()) ::
boolean()
def last_usage_cycle_below_limit?(usage, limit) when is_integer(limit) do
limit = ceil(limit * (1 + Quota.pageview_allowance_margin()))
Quota.below_limit?(usage.last_cycle.total, limit)
end
defp check_site_usage_for_enterprise(subscriber) do
limit = subscriber.enterprise_plan.site_limit
usage = Quota.site_usage(subscriber)
usage = Quota.Usage.site_usage(subscriber)
if Quota.below_limit?(usage, limit) do
{:below_limit, {usage, limit}}
@ -103,8 +83,8 @@ defmodule Plausible.Workers.CheckUsage do
end
end
def maybe_remove_grace_period(subscriber, quota_mod) do
case check_pageview_usage_last_cycle(subscriber, quota_mod) do
def maybe_remove_grace_period(subscriber, usage_mod) do
case check_pageview_usage_last_cycle(subscriber, usage_mod) do
{:below_limit, _} ->
subscriber
|> Plausible.Auth.GracePeriod.remove_changeset()
@ -115,8 +95,8 @@ defmodule Plausible.Workers.CheckUsage do
end
end
defp check_regular_subscriber(subscriber, quota_mod) do
case check_pageview_usage_two_cycles(subscriber, quota_mod) do
defp check_regular_subscriber(subscriber, usage_mod) do
case check_pageview_usage_two_cycles(subscriber, usage_mod) do
{:over_limit, pageview_usage} ->
suggested_plan =
Plausible.Billing.Plans.suggest(subscriber, pageview_usage.last_cycle.total)
@ -133,8 +113,8 @@ defmodule Plausible.Workers.CheckUsage do
end
end
def check_enterprise_subscriber(subscriber, quota_mod) do
pageview_usage = check_pageview_usage_two_cycles(subscriber, quota_mod)
def check_enterprise_subscriber(subscriber, usage_mod) do
pageview_usage = check_pageview_usage_two_cycles(subscriber, usage_mod)
site_usage = check_site_usage_for_enterprise(subscriber)
case {pageview_usage, site_usage} do
@ -156,25 +136,25 @@ defmodule Plausible.Workers.CheckUsage do
end
end
defp check_pageview_usage_two_cycles(subscriber, quota_mod) do
usage = quota_mod.monthly_pageview_usage(subscriber)
limit = Quota.monthly_pageview_limit(subscriber)
defp check_pageview_usage_two_cycles(subscriber, usage_mod) do
usage = usage_mod.monthly_pageview_usage(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber)
if exceeds_last_two_usage_cycles?(usage, limit) do
if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
{:over_limit, usage}
else
{:below_limit, usage}
end
end
defp check_pageview_usage_last_cycle(subscriber, quota_mod) do
usage = quota_mod.monthly_pageview_usage(subscriber)
limit = Quota.monthly_pageview_limit(subscriber)
defp check_pageview_usage_last_cycle(subscriber, usage_mod) do
usage = usage_mod.monthly_pageview_usage(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber)
if last_usage_cycle_below_limit?(usage, limit) do
{:below_limit, usage}
else
if :last_cycle in Quota.exceeded_cycles(usage, limit) do
{:over_limit, usage}
else
{:below_limit, usage}
end
end
end

View File

@ -55,14 +55,14 @@ defmodule Plausible.Workers.SendTrialNotifications do
end
defp send_tomorrow_reminder(user) do
usage = Plausible.Billing.Quota.usage_cycle(user, :last_30_days)
usage = Plausible.Billing.Quota.Usage.usage_cycle(user, :last_30_days)
PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", usage)
|> Plausible.Mailer.send()
end
defp send_today_reminder(user) do
usage = Plausible.Billing.Quota.usage_cycle(user, :last_30_days)
usage = Plausible.Billing.Quota.Usage.usage_cycle(user, :last_30_days)
PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|> Plausible.Mailer.send()

View File

@ -24,14 +24,14 @@ defmodule Plausible.Billing.QuotaTest do
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
assert 50 == Quota.site_limit(user_on_v1)
assert 50 == Quota.site_limit(user_on_v2)
assert 50 == Quota.site_limit(user_on_v3)
assert 50 == Quota.Limits.site_limit(user_on_v1)
assert 50 == Quota.Limits.site_limit(user_on_v2)
assert 50 == Quota.Limits.site_limit(user_on_v3)
end
test "returns 50 when user is on free_10k plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
assert 50 == Quota.site_limit(user)
assert 50 == Quota.Limits.site_limit(user)
end
test "returns the configured site limit for enterprise plan" do
@ -40,7 +40,7 @@ defmodule Plausible.Billing.QuotaTest do
enterprise_plan = insert(:enterprise_plan, user_id: user.id, site_limit: 500)
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
assert enterprise_plan.site_limit == Quota.site_limit(user)
assert enterprise_plan.site_limit == Quota.Limits.site_limit(user)
end
test "returns 10 when user in on trial" do
@ -49,7 +49,7 @@ defmodule Plausible.Billing.QuotaTest do
trial_expiry_date: Timex.shift(Timex.now(), days: 7)
)
assert 10 == Quota.site_limit(user)
assert 10 == Quota.Limits.site_limit(user)
end
test "returns the subscription limit for enterprise users who have not paid yet" do
@ -59,7 +59,7 @@ defmodule Plausible.Billing.QuotaTest do
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
)
assert 50 == Quota.site_limit(user)
assert 50 == Quota.Limits.site_limit(user)
end
test "returns 10 for enterprise users who have not upgraded yet and are on trial" do
@ -69,7 +69,7 @@ defmodule Plausible.Billing.QuotaTest do
subscription: nil
)
assert 10 == Quota.site_limit(user)
assert 10 == Quota.Limits.site_limit(user)
end
end
@ -79,7 +79,7 @@ defmodule Plausible.Billing.QuotaTest do
insert(:site, memberships: [build(:site_membership, user: user, role: :admin)])
insert(:site, memberships: [build(:site_membership, user: user, role: :viewer)])
assert Quota.site_usage(user) == 3
assert Quota.Usage.site_usage(user) == 3
end
describe "below_limit?/2" do
@ -209,19 +209,19 @@ defmodule Plausible.Billing.QuotaTest do
test "is based on the plan if user is on a legacy plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
assert Quota.monthly_pageview_limit(user.subscription) == 1_000_000
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 1_000_000
end
test "is based on the plan if user is on a standard plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
assert Quota.monthly_pageview_limit(user.subscription) == 10_000
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 10_000
end
test "free_10k has 10k monthly_pageview_limit" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
assert Quota.monthly_pageview_limit(user.subscription) == 10_000
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 10_000
end
test "is based on the enterprise plan if user is on an enterprise plan" do
@ -233,14 +233,14 @@ defmodule Plausible.Billing.QuotaTest do
subscription =
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
assert Quota.monthly_pageview_limit(subscription) == 100_000
assert Quota.Limits.monthly_pageview_limit(subscription) == 100_000
end
test "does not limit pageviews when user has a pending enterprise plan" do
user = insert(:user)
subscription = insert(:subscription, user_id: user.id, paddle_plan_id: "pending-enterprise")
assert Quota.monthly_pageview_limit(subscription) == :unlimited
assert Quota.Limits.monthly_pageview_limit(subscription) == :unlimited
end
end
@ -282,7 +282,7 @@ defmodule Plausible.Billing.QuotaTest do
]
)
assert Quota.team_member_usage(me) == 3
assert Quota.Usage.team_member_usage(me) == 3
end
test "counts the same email address as one team member" do
@ -310,7 +310,7 @@ defmodule Plausible.Billing.QuotaTest do
insert(:invitation, site: site_i_own_3, inviter: me, email: "joe@plausible.test")
assert Quota.team_member_usage(me) == 2
assert Quota.Usage.team_member_usage(me) == 2
end
test "counts pending invitations as team members" do
@ -332,7 +332,7 @@ defmodule Plausible.Billing.QuotaTest do
insert(:invitation, site: site_i_own, inviter: member)
insert(:invitation, site: site_i_have_access, inviter: me)
assert Quota.team_member_usage(me) == 3
assert Quota.Usage.team_member_usage(me) == 3
end
test "does not count ownership transfer as a team member" do
@ -341,12 +341,12 @@ defmodule Plausible.Billing.QuotaTest do
insert(:invitation, site: site_i_own, inviter: me, role: :owner)
assert Quota.team_member_usage(me) == 0
assert Quota.Usage.team_member_usage(me) == 0
end
test "returns zero when user does not have any site" do
me = insert(:user)
assert Quota.team_member_usage(me) == 0
assert Quota.Usage.team_member_usage(me) == 0
end
test "does not count email report recipients as team members" do
@ -358,7 +358,7 @@ defmodule Plausible.Billing.QuotaTest do
recipients: ["adam@plausible.test", "vini@plausible.test"]
)
assert Quota.team_member_usage(me) == 0
assert Quota.Usage.team_member_usage(me) == 0
end
test "excludes specific emails from limit calculation" do
@ -377,11 +377,13 @@ defmodule Plausible.Billing.QuotaTest do
insert(:invitation, site: site_i_own, inviter: member)
invitation = insert(:invitation, site: site_i_own, inviter: me, email: "foo@example.com")
assert Quota.team_member_usage(me) == 4
assert Quota.team_member_usage(me, exclude_emails: "arbitrary@example.com") == 4
assert Quota.team_member_usage(me, exclude_emails: member.email) == 3
assert Quota.team_member_usage(me, exclude_emails: invitation.email) == 3
assert Quota.team_member_usage(me, exclude_emails: [member.email, invitation.email]) == 2
assert Quota.Usage.team_member_usage(me) == 4
assert Quota.Usage.team_member_usage(me, exclude_emails: "arbitrary@example.com") == 4
assert Quota.Usage.team_member_usage(me, exclude_emails: member.email) == 3
assert Quota.Usage.team_member_usage(me, exclude_emails: invitation.email) == 3
assert Quota.Usage.team_member_usage(me, exclude_emails: [member.email, invitation.email]) ==
2
end
end
@ -392,14 +394,14 @@ defmodule Plausible.Billing.QuotaTest do
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
assert :unlimited == Quota.team_member_limit(user_on_v1)
assert :unlimited == Quota.team_member_limit(user_on_v2)
assert :unlimited == Quota.team_member_limit(user_on_v3)
assert :unlimited == Quota.Limits.team_member_limit(user_on_v1)
assert :unlimited == Quota.Limits.team_member_limit(user_on_v2)
assert :unlimited == Quota.Limits.team_member_limit(user_on_v3)
end
test "returns unlimited when user is on free_10k plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
assert :unlimited == Quota.team_member_limit(user)
assert :unlimited == Quota.Limits.team_member_limit(user)
end
test "returns 5 when user in on trial" do
@ -408,7 +410,7 @@ defmodule Plausible.Billing.QuotaTest do
trial_expiry_date: Timex.shift(Timex.now(), days: 7)
)
assert 3 == Quota.team_member_limit(user)
assert 3 == Quota.Limits.team_member_limit(user)
end
test "returns the enterprise plan limit" do
@ -419,7 +421,7 @@ defmodule Plausible.Billing.QuotaTest do
subscription: build(:subscription, paddle_plan_id: "123321")
)
assert 27 == Quota.team_member_limit(user)
assert 27 == Quota.Limits.team_member_limit(user)
end
test "reads from json file when the user is on a v4 plan" do
@ -427,22 +429,22 @@ defmodule Plausible.Billing.QuotaTest do
user_on_business = insert(:user, subscription: build(:business_subscription))
assert 3 == Quota.team_member_limit(user_on_growth)
assert 10 == Quota.team_member_limit(user_on_business)
assert 3 == Quota.Limits.team_member_limit(user_on_growth)
assert 10 == Quota.Limits.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)
assert :unlimited == Quota.Limits.team_member_limit(user)
end
end
describe "features_usage/1" do
test "returns an empty list for a user/site who does not use any feature" do
assert [] == Quota.features_usage(insert(:user))
assert [] == Quota.features_usage(insert(:site))
assert [] == Quota.Usage.features_usage(insert(:user))
assert [] == Quota.Usage.features_usage(insert(:site))
end
test "returns [Props] when user/site uses custom props" do
@ -454,8 +456,8 @@ defmodule Plausible.Billing.QuotaTest do
memberships: [build(:site_membership, user: user, role: :owner)]
)
assert [Props] == Quota.features_usage(site)
assert [Props] == Quota.features_usage(user)
assert [Props] == Quota.Usage.features_usage(site)
assert [Props] == Quota.Usage.features_usage(user)
end
on_ee do
@ -467,8 +469,8 @@ defmodule Plausible.Billing.QuotaTest do
steps = Enum.map(goals, &%{"goal_id" => &1.id})
Plausible.Funnels.create(site, "dummy", steps)
assert [Funnels] == Quota.features_usage(site)
assert [Funnels] == Quota.features_usage(user)
assert [Funnels] == Quota.Usage.features_usage(site)
assert [Funnels] == Quota.Usage.features_usage(user)
end
test "returns [RevenueGoals] when user/site uses revenue goals" do
@ -476,8 +478,8 @@ defmodule Plausible.Billing.QuotaTest do
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
insert(:goal, currency: :USD, site: site, event_name: "Purchase")
assert [RevenueGoals] == Quota.features_usage(site)
assert [RevenueGoals] == Quota.features_usage(user)
assert [RevenueGoals] == Quota.Usage.features_usage(site)
assert [RevenueGoals] == Quota.Usage.features_usage(user)
end
end
@ -485,7 +487,7 @@ defmodule Plausible.Billing.QuotaTest do
user = insert(:user)
insert(:api_key, user: user)
assert [StatsAPI] == Quota.features_usage(user)
assert [StatsAPI] == Quota.Usage.features_usage(user)
end
on_ee do
@ -504,8 +506,8 @@ defmodule Plausible.Billing.QuotaTest do
steps = Enum.map(goals, &%{"goal_id" => &1.id})
Plausible.Funnels.create(site, "dummy", steps)
assert [Props, Funnels, RevenueGoals] == Quota.features_usage(site)
assert [Props, Funnels, RevenueGoals] == Quota.features_usage(user)
assert [Props, Funnels, RevenueGoals] == Quota.Usage.features_usage(site)
assert [Props, Funnels, RevenueGoals] == Quota.Usage.features_usage(user)
end
end
@ -517,7 +519,7 @@ defmodule Plausible.Billing.QuotaTest do
memberships: [build(:site_membership, user: user, role: :admin)]
)
assert [] == Quota.features_usage(user)
assert [] == Quota.Usage.features_usage(user)
end
end
@ -525,7 +527,7 @@ defmodule Plausible.Billing.QuotaTest do
on_ee do
test "users with expired trials have no access to subscription features" do
user = insert(:user, trial_expiry_date: ~D[2023-01-01])
assert [Goals] == Quota.allowed_features_for(user)
assert [Goals] == Quota.Limits.allowed_features_for(user)
end
end
@ -534,14 +536,14 @@ defmodule Plausible.Billing.QuotaTest do
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
assert [Goals, Props, StatsAPI] == Quota.allowed_features_for(user_on_v1)
assert [Goals, Props, StatsAPI] == Quota.allowed_features_for(user_on_v2)
assert [Goals, Props, StatsAPI] == Quota.allowed_features_for(user_on_v3)
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v1)
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v2)
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v3)
end
test "returns [Goals, Props, StatsAPI] when user is on free_10k plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
assert [Goals, Props, StatsAPI] == Quota.allowed_features_for(user)
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user)
end
on_ee do
@ -560,14 +562,14 @@ defmodule Plausible.Billing.QuotaTest do
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
assert [Plausible.Billing.Feature.StatsAPI, Plausible.Billing.Feature.Funnels] ==
Quota.allowed_features_for(user)
Quota.Limits.allowed_features_for(user)
end
end
test "returns all features when user in on trial" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 7))
assert Plausible.Billing.Feature.list() == Quota.allowed_features_for(user)
assert Plausible.Billing.Feature.list() == Quota.Limits.allowed_features_for(user)
end
test "returns previous plan limits for enterprise users who have not paid yet" do
@ -577,7 +579,7 @@ defmodule Plausible.Billing.QuotaTest do
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
)
assert [Goals, Props, StatsAPI] == Quota.allowed_features_for(user)
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user)
end
test "returns all features for enterprise users who have not upgraded yet and are on trial" do
@ -587,7 +589,7 @@ defmodule Plausible.Billing.QuotaTest do
subscription: nil
)
assert Plausible.Billing.Feature.list() == Quota.allowed_features_for(user)
assert Plausible.Billing.Feature.list() == Quota.Limits.allowed_features_for(user)
end
test "returns old plan features for enterprise customers who are due to change a plan" do
@ -602,7 +604,7 @@ defmodule Plausible.Billing.QuotaTest do
)
insert(:enterprise_plan, user_id: user.id, paddle_plan_id: "new-paddle-plan-id")
assert [Plausible.Billing.Feature.StatsAPI] == Quota.allowed_features_for(user)
assert [Plausible.Billing.Feature.StatsAPI] == Quota.Limits.allowed_features_for(user)
end
end
@ -619,7 +621,7 @@ defmodule Plausible.Billing.QuotaTest do
pageviews: 0,
date_range: date_range
}
} = Quota.monthly_pageview_usage(user)
} = Quota.Usage.monthly_pageview_usage(user)
assert date_range.last == Date.utc_today()
assert Date.compare(date_range.first, date_range.last) == :lt
@ -650,7 +652,7 @@ defmodule Plausible.Billing.QuotaTest do
pageviews: 3,
date_range: %{}
}
} = Quota.monthly_pageview_usage(user)
} = Quota.Usage.monthly_pageview_usage(user)
end
test "returns usage for user with subscription and a site" do
@ -693,7 +695,7 @@ defmodule Plausible.Billing.QuotaTest do
pageviews: 0,
date_range: %{}
}
} = Quota.monthly_pageview_usage(user)
} = Quota.Usage.monthly_pageview_usage(user)
end
test "returns usage for only a subset of site IDs" do
@ -740,7 +742,7 @@ defmodule Plausible.Billing.QuotaTest do
pageviews: 0,
date_range: %{}
}
} = Quota.monthly_pageview_usage(user, [site1.id, site3.id])
} = Quota.Usage.monthly_pageview_usage(user, [site1.id, site3.id])
end
end
@ -777,13 +779,13 @@ defmodule Plausible.Billing.QuotaTest do
insert(:subscription, user_id: user.id, last_bill_date: last_bill_date)
assert %{date_range: penultimate_cycle, pageviews: 2, custom_events: 3, total: 5} =
Quota.usage_cycle(user, :penultimate_cycle, nil, today)
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
assert %{date_range: last_cycle, pageviews: 3, custom_events: 2, total: 5} =
Quota.usage_cycle(user, :last_cycle, nil, today)
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
assert %{date_range: current_cycle, pageviews: 0, custom_events: 3, total: 3} =
Quota.usage_cycle(user, :current_cycle, nil, today)
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
assert penultimate_cycle == Date.range(~D[2023-04-03], ~D[2023-05-02])
assert last_cycle == Date.range(~D[2023-05-03], ~D[2023-06-02])
@ -794,7 +796,7 @@ defmodule Plausible.Billing.QuotaTest do
today = ~D[2023-06-01]
assert %{date_range: last_30_days, pageviews: 4, custom_events: 1, total: 5} =
Quota.usage_cycle(user, :last_30_days, nil, today)
Quota.Usage.usage_cycle(user, :last_30_days, nil, today)
assert last_30_days == Date.range(~D[2023-05-02], ~D[2023-06-01])
end
@ -817,7 +819,7 @@ defmodule Plausible.Billing.QuotaTest do
insert(:subscription, user_id: user.id, last_bill_date: last_bill_date)
assert %{date_range: last_cycle, pageviews: 3, custom_events: 2, total: 5} =
Quota.usage_cycle(user, :last_cycle, nil, today)
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
assert last_cycle == Date.range(~D[2023-05-03], ~D[2023-06-02])
end
@ -829,13 +831,13 @@ defmodule Plausible.Billing.QuotaTest do
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
assert %{date_range: penultimate_cycle} =
Quota.usage_cycle(user, :penultimate_cycle, nil, today)
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
assert %{date_range: last_cycle} =
Quota.usage_cycle(user, :last_cycle, nil, today)
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
assert %{date_range: current_cycle} =
Quota.usage_cycle(user, :current_cycle, nil, today)
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
assert penultimate_cycle == Date.range(~D[2020-12-01], ~D[2020-12-31])
assert last_cycle == Date.range(~D[2021-01-01], ~D[2021-01-31])
@ -849,13 +851,13 @@ defmodule Plausible.Billing.QuotaTest do
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
assert %{date_range: penultimate_cycle, total: 0} =
Quota.usage_cycle(user, :penultimate_cycle, nil, today)
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
assert %{date_range: last_cycle, total: 0} =
Quota.usage_cycle(user, :last_cycle, nil, today)
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
assert %{date_range: current_cycle, total: 0} =
Quota.usage_cycle(user, :current_cycle, nil, today)
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
assert penultimate_cycle == Date.range(~D[2020-11-01], ~D[2020-11-30])
assert last_cycle == Date.range(~D[2020-12-01], ~D[2020-12-31])

View File

@ -364,7 +364,7 @@ defmodule PlausibleWeb.SiteControllerTest do
})
assert redirected_to(conn) == "/example.com/snippet?site_created=true"
assert Plausible.Billing.Quota.site_usage(user) == 3
assert Plausible.Billing.Quota.Usage.site_usage(user) == 3
end
for url <- ["https://Example.com/", "HTTPS://EXAMPLE.COM/", "/Example.com/", "//Example.com/"] do

View File

@ -8,6 +8,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
alias Plausible.{Repo, Billing.Subscription}
@v1_10k_yearly_plan_id "572810"
@v4_growth_10k_yearly_plan_id "857079"
@v4_growth_200k_yearly_plan_id "857081"
@v4_business_5m_monthly_plan_id "857111"
@v3_business_10k_monthly_plan_id "857481"
@ -358,16 +359,18 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
"https://plausible.io/white-label-web-analytics"
end
test "displays usage", %{conn: conn, site: site} do
test "displays usage in the last cycle", %{conn: conn, site: site} do
yesterday = NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :day)
populate_stats(site, [
build(:pageview),
build(:pageview)
build(:pageview, timestamp: yesterday),
build(:pageview, timestamp: yesterday)
])
{:ok, _lv, doc} = get_liveview(conn)
assert doc =~ "You have used"
assert doc =~ "<b>2</b>"
assert doc =~ "billable pageviews in the last 30 days"
assert doc =~ "billable pageviews in the last billing cycle"
end
test "gets default selected interval from current subscription plan", %{conn: conn} do
@ -375,9 +378,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
end
test "gets default pageview limit from current subscription plan", %{conn: conn} do
test "sets pageview slider according to last cycle usage", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @slider_value) == "200k"
assert text_of_element(doc, @slider_value) == "10k"
end
test "pageview slider changes selected volume", %{conn: conn} do
@ -401,7 +404,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
end
test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "200k")
assert text_of_element(doc, @growth_checkout_button) == "Currently on this plan"
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
@ -431,7 +436,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
growth_checkout_button = find(doc, @growth_checkout_button)
assert text_of_attr(growth_checkout_button, "onclick") =~
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_growth_200k_yearly_plan_id)}'}"
"if (true) {window.location = '#{Routes.billing_path(conn, :change_plan_preview, @v4_growth_10k_yearly_plan_id)}'}"
set_slider(lv, "5M")
doc = element(lv, @monthly_interval_button) |> render_click()
@ -446,9 +451,9 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a user with a v4 business subscription plan" do
setup [:create_user, :create_site, :log_in, :subscribe_v4_business]
test "gets default pageview limit from current subscription plan", %{conn: conn} do
test "sets pageview slider according to last cycle usage", %{conn: conn} do
{:ok, _lv, doc} = get_liveview(conn)
assert text_of_element(doc, @slider_value) == "5M"
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
@ -462,11 +467,12 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
end
test "checkout button text and click-disabling CSS classes are dynamic", %{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "5M")
assert text_of_element(doc, @business_checkout_button) == "Currently on this plan"
assert class_of_element(doc, @business_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, @growth_checkout_button) == "Downgrade to Growth"
doc = element(lv, @yearly_interval_button) |> render_click()
@ -634,7 +640,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "checkout buttons are disabled + notice about billing details (unless plan owned already)",
%{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "200k")
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, @growth_checkout_button) =~ "Currently on this plan"
refute element_exists?(doc, "#{@growth_checkout_button} + p")
@ -664,7 +673,10 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
test "checkout buttons are disabled + notice about billing details when plan not owned already",
%{conn: conn} do
{:ok, lv, doc} = get_liveview(conn)
{:ok, lv, _doc} = get_liveview(conn)
doc = set_slider(lv, "200k")
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none bg-gray-400"
assert text_of_element(doc, @growth_checkout_button) =~ "Currently on this plan"
refute element_exists?(doc, "#{@growth_checkout_button} + p")

View File

@ -31,8 +31,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "does not send an email if account has been over the limit for one billing month", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 9_000},
@ -46,7 +46,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period == nil
@ -55,8 +55,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "does not send an email if account is over the limit by less than 10%", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 10_999},
@ -70,7 +70,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period == nil
@ -79,8 +79,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "sends an email when an account is over their limit for two consecutive billing months", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -94,7 +94,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_email_delivered_with(
to: [user],
@ -107,8 +107,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "sends an email suggesting enterprise plan when usage is greater than 10M ", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000_000},
@ -122,7 +122,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_delivered_email_matches(%{html_body: html_body})
@ -136,8 +136,8 @@ defmodule Plausible.Workers.CheckUsageTest do
|> Plausible.Auth.GracePeriod.start_changeset()
|> Repo.update!()
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -151,7 +151,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period.id == existing_grace_period.id
@ -160,8 +160,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "recommends a plan to upgrade to", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -175,7 +175,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_delivered_email_matches(%{
html_body: html_body
@ -185,8 +185,8 @@ defmodule Plausible.Workers.CheckUsageTest do
end
test "clears grace period when plan is applicable again", %{user: user} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -200,11 +200,11 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -212,7 +212,7 @@ defmodule Plausible.Workers.CheckUsageTest do
}
end)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
refute user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
end
@ -223,8 +223,8 @@ defmodule Plausible.Workers.CheckUsageTest do
|> Plausible.Auth.GracePeriod.start_manual_lock_changeset()
|> Repo.update!()
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
@ -240,7 +240,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period.id == existing_grace_period.id
@ -250,8 +250,8 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
@ -267,7 +267,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_email_delivered_with(
to: [{nil, "enterprise@plausible.io"}],
@ -279,8 +279,8 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1},
@ -300,7 +300,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_email_delivered_with(
to: [{nil, "enterprise@plausible.io"}],
@ -309,8 +309,8 @@ defmodule Plausible.Workers.CheckUsageTest do
end
test "starts grace period when plan is outgrown", %{user: user} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
@ -326,7 +326,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
end
end
@ -335,8 +335,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "checks usage one day after the last_bill_date", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -350,7 +350,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, quota_stub)
CheckUsage.perform(nil, usage_stub)
assert_email_delivered_with(
to: [user],
@ -361,8 +361,8 @@ defmodule Plausible.Workers.CheckUsageTest do
test "does not check exactly one month after last_bill_date", %{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -376,7 +376,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: ~D[2021-03-28]
)
CheckUsage.perform(nil, quota_stub, ~D[2021-03-28])
CheckUsage.perform(nil, usage_stub, ~D[2021-03-28])
assert_no_emails_delivered()
end
@ -385,8 +385,8 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
quota_stub =
Plausible.Billing.Quota
usage_stub =
Plausible.Billing.Quota.Usage
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
@ -400,7 +400,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: ~D[2021-06-29]
)
CheckUsage.perform(nil, quota_stub, ~D[2021-08-30])
CheckUsage.perform(nil, usage_stub, ~D[2021-08-30])
assert_email_delivered_with(
to: [user],