Define a better monthly pageview usage (#3564)

* refactor asking for the monthly pageview usage

* add tests for usage and limits section in account settings

* display pageview usage per billing cycle for active subscribers

* disable cycle tabs if no usage

* make current billing cycle whole

...instead of capping it at today's date

* run queries for different cycles concurrently

* fix linebreak bug

* add calculate usage action into CRM

* change some names of assigns

* block subscribing to a plan by pageview usage

Depending on whether the customer has already subscribed or not, checking
their pageview usage is different:

* If they're not subscribed yet, we allow them to subscribe to a plan If
  it their last 30 days usage does not exceed the plan pageview limit by
  more than 15% (30% for when subscribing to a 10k plan)

* For existing subscribers, we'll use the exact same mechanism that we're
  using for locking sites - the last two billing cycles usage. If both
  cycles exceed the plan limit by more than 10% - we don't allow them to
  subscribe to the plan

* apply credo suggestion

* prevent highlight bar overflow

* move disabled classes to button element

* optimize for darkmode

* unify link and text styling on the same horizontal line

'Upgrade' & 'Update billing details' links + billing interval text were
positioned on the same line. The font size was similar, but not the same

* improve exceeded_limits function readability

* Refactor some tests and remove code duplication

* override allow upgrade when limits exceeded

In cases where limits are exceeded, we can set the boolean flag
`allow_next_upgrade_override` to `true` in the CRM. This will allow
the user to upgrade to any plan they want. After they've upgraded or
changed their plan - the flag will automatically reset to `false`.

* only apply upgrade override for exceeded pageview limit

* fix tests on the CI

* make current_cycle usage always displayed by default

* make pageview allowance margin more clear

* add comment
This commit is contained in:
RobertJoonas 2023-11-30 11:50:44 +00:00 committed by GitHub
parent bd7deb5631
commit 57188a402a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1215 additions and 534 deletions

View File

@ -31,6 +31,10 @@ defmodule Plausible.Auth.User do
field :email_verified, :boolean
field :previous_email, :string
# A field only used as a manual override - allow subscribing
# to any plan, even when exceeding its pageview limit
field :allow_next_upgrade_override, :boolean
# Fields for TOTP authentication. See `Plausible.Auth.TOTP`.
field :totp_enabled, :boolean, default: false
field :totp_secret, Plausible.Auth.TOTP.EncryptedBinary
@ -96,7 +100,14 @@ defmodule Plausible.Auth.User do
def changeset(user, attrs \\ %{}) do
user
|> cast(attrs, [:email, :name, :email_verified, :theme, :trial_expiry_date])
|> cast(attrs, [
:email,
:name,
:email_verified,
:theme,
:trial_expiry_date,
:allow_next_upgrade_override
])
|> validate_required([:email, :name, :email_verified])
|> unique_constraint(:email)
end

View File

@ -13,7 +13,8 @@ defmodule Plausible.Auth.UserAdmin do
name: nil,
email: nil,
previous_email: nil,
trial_expiry_date: nil
trial_expiry_date: nil,
allow_next_upgrade_override: nil
]
end
@ -42,6 +43,10 @@ defmodule Plausible.Auth.UserAdmin do
lock: %{
name: "Lock",
action: fn _, user -> lock(user) end
},
calculate_usage: %{
name: "Calculate usage",
action: fn _, user -> calculate_usage(user) end
}
]
end
@ -65,6 +70,42 @@ defmodule Plausible.Auth.UserAdmin do
end
end
@separator String.duplicate("_", 200)
def calculate_usage(user) do
user = Plausible.Users.with_subscription(user)
pageview_limit =
case Plausible.Billing.Quota.monthly_pageview_limit(user.subscription) do
:unlimited -> "unlimited"
integer -> PlausibleWeb.StatsView.large_number_format(integer)
end
pageview_usage =
user
|> Plausible.Billing.Quota.monthly_pageview_usage()
|> Enum.map_join(" #{@separator} ", fn {cycle, usage} ->
"#{cycle}: (#{PlausibleWeb.TextHelpers.format_date_range(usage.date_range)}): #{usage.total}"
end)
site_limit = Plausible.Billing.Quota.site_limit(user)
site_usage = Plausible.Billing.Quota.site_usage(user)
team_member_limit = Plausible.Billing.Quota.team_member_limit(user)
team_member_usage = Plausible.Billing.Quota.team_member_usage(user)
msg = """
TOTAL PAGEVIEWS (limit: #{pageview_limit})
#{@separator}
#{pageview_usage}
#{@separator}
SITES (#{site_usage} / #{site_limit}) #{@separator}
TEAM MEMBERS (#{team_member_usage} / #{team_member_limit})
"""
{:error, user, msg}
end
defp grace_period_status(%{grace_period: grace_period}) do
case grace_period do
nil ->

View File

@ -105,17 +105,15 @@ defmodule Plausible.Billing do
end
end
defp subscription_is_active?(%Subscription{status: Subscription.Status.active()}), do: true
defp subscription_is_active?(%Subscription{status: Subscription.Status.past_due()}), do: true
def subscription_is_active?(%Subscription{status: Subscription.Status.active()}), do: true
def subscription_is_active?(%Subscription{status: Subscription.Status.past_due()}), do: true
defp subscription_is_active?(
%Subscription{status: Subscription.Status.deleted()} = subscription
) do
def subscription_is_active?(%Subscription{status: Subscription.Status.deleted()} = subscription) do
subscription.next_bill_date && !Timex.before?(subscription.next_bill_date, Timex.today())
end
defp subscription_is_active?(%Subscription{}), do: false
defp subscription_is_active?(nil), do: false
def subscription_is_active?(%Subscription{}), do: false
def subscription_is_active?(nil), do: false
on_full_build do
def on_trial?(%Plausible.Auth.User{trial_expiry_date: nil}), do: false
@ -132,51 +130,6 @@ defmodule Plausible.Billing do
Timex.diff(user.trial_expiry_date, Timex.today(), :days)
end
@spec last_two_billing_months_usage(Plausible.Auth.User.t(), Date.t()) ::
{non_neg_integer(), non_neg_integer()}
def last_two_billing_months_usage(user, today \\ Timex.today()) do
{first, second} = last_two_billing_cycles(user, today)
site_ids = Plausible.Sites.owned_site_ids(user)
usage_for_sites = fn site_ids, date_range ->
{pageviews, custom_events} =
Plausible.Stats.Clickhouse.usage_breakdown(site_ids, date_range)
pageviews + custom_events
end
{
usage_for_sites.(site_ids, first),
usage_for_sites.(site_ids, second)
}
end
def last_two_billing_cycles(user, today \\ Timex.today()) do
last_bill_date = user.subscription.last_bill_date
normalized_last_bill_date =
Timex.shift(last_bill_date,
months: Timex.diff(today, last_bill_date, :months)
)
{
Date.range(
Timex.shift(normalized_last_bill_date, months: -2),
Timex.shift(normalized_last_bill_date, days: -1, months: -1)
),
Date.range(
Timex.shift(normalized_last_bill_date, months: -1),
Timex.shift(normalized_last_bill_date, days: -1)
)
}
end
def usage_breakdown(user) do
site_ids = Plausible.Sites.owned_site_ids(user)
Plausible.Stats.Clickhouse.usage_breakdown(site_ids)
end
defp handle_subscription_created(params) do
params =
if present?(params["passthrough"]) do
@ -306,6 +259,7 @@ defmodule Plausible.Billing do
user
|> maybe_remove_grace_period()
|> Plausible.Users.maybe_reset_next_upgrade_override()
|> tap(&Plausible.Billing.SiteLocker.update_sites_for/1)
|> maybe_adjust_api_key_limits()
end

View File

@ -7,7 +7,6 @@ defmodule Plausible.Billing.Quota do
import Ecto.Query
alias Plausible.Auth.User
alias Plausible.Site
alias Plausible.Billing
alias Plausible.Billing.{Plan, Plans, Subscription, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, RevenueGoals, Funnels, Props, StatsAPI}
@ -112,15 +111,93 @@ defmodule Plausible.Billing.Quota do
end
end
@spec monthly_pageview_usage(User.t()) :: non_neg_integer()
@doc """
Returns the amount of pageviews and custom events
sent by the sites the user owns in last 30 days.
"""
@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()
}
@spec monthly_pageview_usage(User.t()) :: monthly_pageview_usage()
def monthly_pageview_usage(user) do
active_subscription? = Plausible.Billing.subscription_is_active?(user.subscription)
if active_subscription? && user.subscription.last_bill_date do
[:current_cycle, :last_cycle, :penultimate_cycle]
|> Task.async_stream(fn cycle ->
%{cycle => usage_cycle(user, cycle)}
end)
|> Enum.map(fn {:ok, cycle_usage} -> cycle_usage end)
|> Enum.reduce(%{}, &Map.merge/2)
else
%{last_30_days: usage_cycle(user, :last_30_days)}
end
end
@spec usage_cycle(User.t(), period(), Date.t()) :: usage_cycle()
def usage_cycle(user, cycle, today \\ Timex.today())
def usage_cycle(user, :last_30_days, today) do
date_range = Date.range(Timex.shift(today, days: -30), today)
{pageviews, custom_events} =
user
|> Billing.usage_breakdown()
|> Tuple.sum()
|> Plausible.Sites.owned_site_ids()
|> Plausible.Stats.Clickhouse.usage_breakdown(date_range)
%{
date_range: date_range,
pageviews: pageviews,
custom_events: custom_events,
total: pageviews + custom_events
}
end
def usage_cycle(user, cycle, today) do
user = Plausible.Users.with_subscription(user)
last_bill_date = user.subscription.last_bill_date
normalized_last_bill_date =
Timex.shift(last_bill_date, months: Timex.diff(today, last_bill_date, :months))
date_range =
case cycle do
:current_cycle ->
Date.range(
normalized_last_bill_date,
Timex.shift(normalized_last_bill_date, months: 1, days: -1)
)
:last_cycle ->
Date.range(
Timex.shift(normalized_last_bill_date, months: -1),
Timex.shift(normalized_last_bill_date, days: -1)
)
:penultimate_cycle ->
Date.range(
Timex.shift(normalized_last_bill_date, months: -2),
Timex.shift(normalized_last_bill_date, days: -1, months: -1)
)
end
{pageviews, custom_events} =
user
|> Plausible.Sites.owned_site_ids()
|> Plausible.Stats.Clickhouse.usage_breakdown(date_range)
%{
date_range: date_range,
pageviews: pageviews,
custom_events: custom_events,
total: pageviews + custom_events
}
end
@team_member_limit_for_trials 3
@ -259,35 +336,52 @@ defmodule Plausible.Billing.Quota do
for {f_mod, used?} <- used_features, used?, f_mod.enabled?(site), do: f_mod
end
def ensure_can_subscribe_to_plan(user, %Plan{} = plan) do
case exceeded_limits(usage(user), plan) do
[] ->
:ok
def ensure_can_subscribe_to_plan(user, plan, usage \\ nil)
[:monthly_pageview_limit] ->
# This is a quick fix. Need to figure out how to handle this case. Only
# checking the last 30 days usage is not accurate enough. Needs to be
# in sync with the actual locking system.
:ok
def ensure_can_subscribe_to_plan(%User{} = user, %Plan{} = plan, usage) do
usage = if usage, do: usage, else: usage(user)
exceeded_limits ->
{:error, %{exceeded_limits: exceeded_limits}}
case exceeded_limits(user, plan, usage) do
[] -> :ok
exceeded_limits -> {:error, %{exceeded_limits: exceeded_limits}}
end
end
def ensure_can_subscribe_to_plan(_user, nil), do: :ok
def ensure_can_subscribe_to_plan(_, _, _), do: :ok
def exceeded_limits(usage, %Plan{} = plan) do
for {usage_field, limit_field} <- [
{:monthly_pageviews, :monthly_pageview_limit},
{:team_members, :team_member_limit},
{:sites, :site_limit}
defp exceeded_limits(%User{} = user, %Plan{} = plan, usage) 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?(user, plan, usage)}
],
!within_limit?(Map.get(usage, usage_field), Map.get(plan, limit_field)) do
limit_field
exceeded? do
limit
end
end
defp exceeds_monthly_pageview_limit?(%User{allow_next_upgrade_override: true}, _, _) do
false
end
defp exceeds_monthly_pageview_limit?(_user, plan, usage) do
case usage.monthly_pageviews do
%{last_30_days: %{total: total}} ->
!within_limit?(total, pageview_limit_with_margin(plan))
billing_cycles_usage ->
Plausible.Workers.CheckUsage.exceeds_last_two_usage_cycles?(
billing_cycles_usage,
plan.monthly_pageview_limit
)
end
end
defp pageview_limit_with_margin(%Plan{monthly_pageview_limit: limit}) do
allowance_margin = if limit == 10_000, do: 0.3, else: 0.15
ceil(limit * (1 + allowance_margin))
end
@doc """
Returns a list of features the user can use. Trial users have the
ability to use all features during their trial.

View File

@ -68,15 +68,16 @@ 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
{_, last_cycle} = Plausible.Billing.last_two_billing_cycles(user)
{_, last_cycle_usage} = Plausible.Billing.last_two_billing_months_usage(user)
suggested_plan = Plausible.Billing.Plans.suggest(user, last_cycle_usage)
last_cycle_usage =
Plausible.Billing.Quota.usage_cycle(user, :last_cycle)
suggested_plan = Plausible.Billing.Plans.suggest(user, last_cycle_usage.total)
template =
PlausibleWeb.Email.dashboard_locked(
user,
last_cycle_usage,
last_cycle,
last_cycle_usage.total,
last_cycle_usage.date_range,
suggested_plan
)

View File

@ -42,16 +42,6 @@ defmodule Plausible.Stats.Clickhouse do
)
end
def usage_breakdown(domains_or_site_ids) do
range =
Date.range(
Timex.shift(Timex.today(), days: -30),
Timex.today()
)
usage_breakdown(domains_or_site_ids, range)
end
def usage_breakdown([d | _] = domains, date_range) when is_binary(d) do
Enum.chunk_every(domains, 300)
|> Enum.reduce({0, 0}, fn domains, {pageviews_total, custom_events_total} ->

View File

@ -31,6 +31,22 @@ defmodule Plausible.Users do
Auth.EmailVerification.any?(user)
end
def allow_next_upgrade_override(%Auth.User{} = user) do
user
|> Auth.User.changeset(%{allow_next_upgrade_override: true})
|> Repo.update!()
end
def maybe_reset_next_upgrade_override(%Auth.User{} = user) do
if user.allow_next_upgrade_override do
user
|> Auth.User.changeset(%{allow_next_upgrade_override: false})
|> Repo.update!()
else
user
end
end
defp last_subscription_query(user_id) do
from(subscription in Plausible.Billing.Subscription,
where: subscription.user_id == ^user_id,

View File

@ -82,6 +82,147 @@ defmodule PlausibleWeb.Components.Billing do
end
end
def render_monthly_pageview_usage(%{usage: usage} = assigns)
when is_map_key(usage, :last_30_days) do
~H"""
<.monthly_pageview_usage_table usage={@usage.last_30_days} limit={@limit} period={:last_30_days} />
"""
end
def render_monthly_pageview_usage(assigns) do
~H"""
<article id="monthly_pageview_usage_container" x-data="{ tab: 'current_cycle' }" class="mt-8">
<h1 class="text-xl mb-6 font-bold dark:text-gray-100">Monthly pageviews usage</h1>
<div class="mb-3">
<ol class="divide-y divide-gray-300 dark:divide-gray-600 rounded-md border dark:border-gray-600 md:flex md:flex-row-reverse md:divide-y-0 md:overflow-hidden">
<.billing_cycle_tab
name="Ongoing cycle"
tab={:current_cycle}
date_range={@usage.current_cycle.date_range}
with_separator={true}
/>
<.billing_cycle_tab
name="Last cycle"
tab={:last_cycle}
date_range={@usage.last_cycle.date_range}
disabled={@usage.last_cycle.total == 0 && @usage.penultimate_cycle.total == 0}
with_separator={true}
/>
<.billing_cycle_tab
name="Penultimate cycle"
tab={:penultimate_cycle}
date_range={@usage.penultimate_cycle.date_range}
disabled={@usage.penultimate_cycle.total == 0}
/>
</ol>
</div>
<div x-show="tab === 'current_cycle'">
<.monthly_pageview_usage_table
usage={@usage.current_cycle}
limit={@limit}
period={:current_cycle}
/>
</div>
<div x-show="tab === 'last_cycle'">
<.monthly_pageview_usage_table usage={@usage.last_cycle} limit={@limit} period={:last_cycle} />
</div>
<div x-show="tab === 'penultimate_cycle'">
<.monthly_pageview_usage_table
usage={@usage.penultimate_cycle}
limit={@limit}
period={:penultimate_cycle}
/>
</div>
</article>
"""
end
attr(:usage, :map, required: true)
attr(:limit, :any, required: true)
attr(:period, :atom, required: true)
defp monthly_pageview_usage_table(assigns) do
~H"""
<.usage_and_limits_table>
<.usage_and_limits_row
id={"total_pageviews_#{@period}"}
title={"Total billable pageviews#{if @period == :last_30_days, do: " (last 30 days)"}"}
usage={@usage.total}
limit={@limit}
/>
<.usage_and_limits_row
id={"pageviews_#{@period}"}
pad
title="Pageviews"
usage={@usage.pageviews}
class="font-normal text-gray-500 dark:text-gray-400"
/>
<.usage_and_limits_row
id={"custom_events_#{@period}"}
pad
title="Custom events"
usage={@usage.custom_events}
class="font-normal text-gray-500 dark:text-gray-400"
/>
</.usage_and_limits_table>
"""
end
attr(:name, :string, required: true)
attr(:date_range, :any, required: true)
attr(:tab, :atom, required: true)
attr(:disabled, :boolean, default: false)
attr(:with_separator, :boolean, default: false)
defp billing_cycle_tab(assigns) do
~H"""
<li id={"billing_cycle_tab_#{@tab}"} class="relative md:w-1/3">
<button
class={["w-full group", @disabled && "pointer-events-none opacity-50 dark:opacity-25"]}
x-on:click={"tab = '#{@tab}'"}
>
<span
class="absolute left-0 top-0 h-full w-1 md:bottom-0 md:top-auto md:h-1 md:w-full"
x-bind:class={"tab === '#{@tab}' ? 'bg-indigo-500' : 'bg-transparent group-hover:bg-gray-200 dark:group-hover:bg-gray-700 '"}
aria-hidden="true"
>
</span>
<div class={"flex items-center justify-between md:flex-col md:items-start py-2 pr-2 #{if @with_separator, do: "pl-2 md:pl-4", else: "pl-2"}"}>
<span
class="text-sm dark:text-gray-100"
x-bind:class={"tab === '#{@tab}' ? 'text-indigo-600 dark:text-indigo-500 font-semibold' : 'font-medium'"}
>
<%= @name %>
</span>
<span class="flex text-xs text-gray-500 dark:text-gray-400">
<%= if @disabled,
do: "Not available",
else: PlausibleWeb.TextHelpers.format_date_range(@date_range) %>
</span>
</div>
</button>
<div
:if={@with_separator}
class="absolute inset-0 left-0 top-0 w-3 hidden md:block"
aria-hidden="true"
>
<svg
class="h-full w-full text-gray-300 dark:text-gray-600"
viewBox="0 0 12 82"
fill="none"
preserveAspectRatio="none"
>
<path
d="M0.5 0V31L10.5 41L0.5 51V82"
stroke="currentcolor"
vector-effect="non-scaling-stroke"
/>
</svg>
</div>
</li>
"""
end
slot(:inner_block, required: true)
attr(:rest, :global)
@ -401,7 +542,7 @@ defmodule PlausibleWeb.Components.Billing do
<.link class="underline inline-block" href={Plausible.Billing.upgrade_route_for(@user)}>
Upgrade your subscription
</.link>
<p>to get access to your stats again.</p>
to get access to your stats again.
"""
else
~H"""

View File

@ -2,6 +2,7 @@ defmodule PlausibleWeb.AuthController do
use PlausibleWeb, :controller
use Plausible.Repo
alias Plausible.Auth
alias Plausible.Billing.Quota
require Logger
plug(
@ -372,7 +373,6 @@ defmodule PlausibleWeb.AuthController do
email_changeset = Keyword.fetch!(opts, :email_changeset)
user = Plausible.Users.with_subscription(conn.assigns[:current_user])
{pageview_usage, custom_event_usage} = Plausible.Billing.usage_breakdown(user)
render(conn, "user_settings.html",
user: user |> Repo.preload(:api_keys),
@ -381,14 +381,12 @@ defmodule PlausibleWeb.AuthController do
subscription: user.subscription,
invoices: Plausible.Billing.paddle_api().get_invoices(user.subscription),
theme: user.theme || "system",
team_member_limit: Plausible.Billing.Quota.team_member_limit(user),
team_member_usage: Plausible.Billing.Quota.team_member_usage(user),
site_limit: Plausible.Billing.Quota.site_limit(user),
site_usage: Plausible.Billing.Quota.site_usage(user),
total_pageview_limit: Plausible.Billing.Quota.monthly_pageview_limit(user.subscription),
total_pageview_usage: pageview_usage + custom_event_usage,
custom_event_usage: custom_event_usage,
pageview_usage: pageview_usage
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.subscription),
pageview_usage: Quota.monthly_pageview_usage(user)
)
end

View File

@ -29,7 +29,7 @@ defmodule PlausibleWeb.BillingController do
true ->
render(conn, "upgrade.html",
skip_plausible_tracking: true,
usage: Plausible.Billing.Quota.monthly_pageview_usage(user),
usage: Plausible.Billing.Quota.usage_cycle(user, :last_30_days).total,
user: user,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)

View File

@ -92,8 +92,8 @@ defmodule PlausibleWeb.Email do
|> render("trial_one_week_reminder.html", user: user)
end
def trial_upgrade_email(user, day, {pageviews, custom_events}) do
suggested_plan = Plausible.Billing.Plans.suggest(user, pageviews + custom_events)
def trial_upgrade_email(user, day, usage) do
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.total)
base_email()
|> to(user)
@ -102,8 +102,8 @@ defmodule PlausibleWeb.Email do
|> render("trial_upgrade_email.html",
user: user,
day: day,
custom_events: custom_events,
usage: pageviews + custom_events,
custom_events: usage.custom_events,
usage: usage.total,
suggested_plan: suggested_plan
)
end

View File

@ -25,6 +25,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|> 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
end)
|> assign_new(:owned_plan, fn %{user: %{subscription: subscription}} ->
Plans.get_regular_plan(subscription, only_non_expired: true)
end)
@ -45,10 +51,10 @@ defmodule PlausibleWeb.Live.ChoosePlan do
end)
|> assign_new(:selected_volume, fn %{
owned_plan: owned_plan,
usage: usage,
last_30_days_usage: last_30_days_usage,
available_volumes: available_volumes
} ->
default_selected_volume(owned_plan, usage.monthly_pageviews, available_volumes)
default_selected_volume(owned_plan, last_30_days_usage, available_volumes)
end)
|> assign_new(:selected_interval, fn %{current_interval: current_interval} ->
current_interval || :monthly
@ -127,7 +133,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<.enterprise_plan_box 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(@usage.monthly_pageviews) %></b>
You have used <b><%= PlausibleWeb.AuthView.delimit_integer(@last_30_days_usage) %></b>
billable pageviews in the last 30 days
</p>
<.pageview_limit_notice :if={!@owned_plan} />
@ -170,8 +176,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
defp default_selected_volume(%Plan{monthly_pageview_limit: limit}, _, _), do: limit
defp default_selected_volume(_, pageview_usage, available_volumes) do
Enum.find(available_volumes, &(pageview_usage < &1)) || :enterprise
defp default_selected_volume(_, last_30_days_usage, available_volumes) do
Enum.find(available_volumes, &(last_30_days_usage < &1)) || :enterprise
end
defp current_user_subscription_interval(subscription) do
@ -324,10 +330,9 @@ defmodule PlausibleWeb.Live.ChoosePlan do
paddle_product_id = get_paddle_product_id(assigns.plan_to_render, assigns.selected_interval)
change_plan_link_text = change_plan_link_text(assigns)
exceeded_limits = Quota.exceeded_limits(assigns.usage, assigns.plan_to_render)
usage_exceeds_plan_limits =
Enum.any?([:team_member_limit, :site_limit], &(&1 in exceeded_limits))
usage_within_limits =
Quota.ensure_can_subscribe_to_plan(assigns.user, assigns.plan_to_render, assigns.usage) ==
:ok
subscription = assigns.user.subscription
@ -345,7 +350,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
change_plan_link_text == "Currently on this plan" && not subscription_cancelled ->
{true, nil}
assigns.available && usage_exceeds_plan_limits ->
assigns.available && !usage_within_limits ->
{true, "Your usage exceeds this plan"}
billing_details_expired ->

View File

@ -45,12 +45,9 @@
<div class="py-2 text-xl font-medium dark:text-gray-100">
<%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %>
</div>
<%= if @subscription.update_url do %>
<%= link("Update billing info",
to: @subscription.update_url,
class: "text-sm text-indigo-500 font-medium"
) %>
<% end %>
<.styled_link :if={@subscription.update_url} href={@subscription.update_url}>
Update billing info
</.styled_link>
<% else %>
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
<% end %>
@ -65,44 +62,31 @@
<div class="py-2 text-xl font-medium dark:text-gray-100">
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
</div>
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">
<span class="text-gray-600 dark:text-gray-400">
(<%= subscription_interval(@subscription) %> billing)
</div>
</span>
<% else %>
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
<% end %>
</div>
</div>
<PlausibleWeb.Components.Billing.render_monthly_pageview_usage
usage={@pageview_usage}
limit={@pageview_limit}
/>
<article class="mt-8">
<h1 class="text-xl font-bold dark:text-gray-100">Usage & Limits</h1>
<h2 class="mt-1 mb-3 text-sm text-gray-500 leading-5 dark:text-gray-200">
Your usage across all of your sites and the limits of your plan
</h2>
<h1 class="text-xl mb-3 font-bold dark:text-gray-100">Sites & team members usage</h1>
<PlausibleWeb.Components.Billing.usage_and_limits_table>
<PlausibleWeb.Components.Billing.usage_and_limits_row
title="Total billable pageviews (last 30 days)"
usage={@total_pageview_usage}
limit={@total_pageview_limit}
/>
<PlausibleWeb.Components.Billing.usage_and_limits_row
pad
title="Pageviews"
usage={@pageview_usage}
class="font-normal text-gray-500 dark:text-gray-400"
/>
<PlausibleWeb.Components.Billing.usage_and_limits_row
pad
title="Custom events"
usage={@custom_event_usage}
class="font-normal text-gray-500 dark:text-gray-400"
/>
<PlausibleWeb.Components.Billing.usage_and_limits_row
id="site-usage-row"
title="Owned sites"
usage={@site_usage}
limit={@site_limit}
/>
<PlausibleWeb.Components.Billing.usage_and_limits_row
id="team-member-usage-row"
title="Team members"
usage={@team_member_usage}
limit={@team_member_limit}

View File

@ -30,4 +30,12 @@ defmodule PlausibleWeb.TextHelpers do
"#{rest_string} and #{last_string}"
end
def format_date_range(date_range) do
"#{format_date(date_range.first)} - #{format_date(date_range.last)}"
end
def format_date(date) do
Timex.format!(date, "{Mshort} {D}, {YYYY}")
end
end

View File

@ -2,7 +2,8 @@ defmodule Plausible.Workers.CheckUsage do
use Plausible.Repo
use Oban.Worker, queue: :check_usage
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
alias Plausible.Billing.{Subscription, Quota}
alias Plausible.Auth.User
defmacro yesterday() do
quote do
@ -32,12 +33,12 @@ defmodule Plausible.Workers.CheckUsage do
end
@impl Oban.Worker
def perform(_job, billing_mod \\ Plausible.Billing, today \\ Timex.today()) do
def perform(_job, quota_mod \\ Quota, today \\ Timex.today()) do
yesterday = today |> Timex.shift(days: -1)
active_subscribers =
Repo.all(
from(u in Plausible.Auth.User,
from(u in User,
join: s in Plausible.Billing.Subscription,
on: s.user_id == u.id,
left_join: ep in Plausible.Billing.EnterprisePlan,
@ -55,20 +56,20 @@ defmodule Plausible.Workers.CheckUsage do
for subscriber <- active_subscribers do
if subscriber.enterprise_plan do
check_enterprise_subscriber(subscriber, billing_mod)
check_enterprise_subscriber(subscriber, quota_mod)
else
check_regular_subscriber(subscriber, billing_mod)
check_regular_subscriber(subscriber, quota_mod)
end
end
:ok
end
def check_enterprise_subscriber(subscriber, billing_mod) do
pageview_limit = check_pageview_limit(subscriber, billing_mod)
site_limit = check_site_limit_for_enterprise(subscriber)
def check_enterprise_subscriber(subscriber, quota_mod) do
pageview_usage = check_pageview_usage(subscriber, quota_mod)
site_usage = check_site_usage_for_enterprise(subscriber)
case {pageview_limit, site_limit} do
case {pageview_usage, site_usage} do
{{:below_limit, _}, {:below_limit, _}} ->
nil
@ -90,8 +91,8 @@ defmodule Plausible.Workers.CheckUsage do
end
end
defp check_regular_subscriber(subscriber, billing_mod) do
case check_pageview_limit(subscriber, billing_mod) do
defp check_regular_subscriber(subscriber, quota_mod) do
case check_pageview_usage(subscriber, quota_mod) do
{:over_limit, {last_cycle, last_cycle_usage}} ->
suggested_plan = Plausible.Billing.Plans.suggest(subscriber, last_cycle_usage)
@ -114,35 +115,33 @@ defmodule Plausible.Workers.CheckUsage do
end
end
defp check_pageview_limit(subscriber, billing_mod) do
limit =
subscriber.subscription
|> Plausible.Billing.Quota.monthly_pageview_limit()
|> Kernel.*(1.1)
|> ceil()
defp check_pageview_usage(subscriber, quota_mod) do
usage = quota_mod.monthly_pageview_usage(subscriber)
limit = Quota.monthly_pageview_limit(subscriber.subscription)
{_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber)
{last_last_cycle_usage, last_cycle_usage} =
billing_mod.last_two_billing_months_usage(subscriber)
exceeded_last_cycle? = not Plausible.Billing.Quota.below_limit?(last_cycle_usage, limit)
exceeded_last_last_cycle? =
not Plausible.Billing.Quota.below_limit?(last_last_cycle_usage, limit)
if exceeded_last_last_cycle? && exceeded_last_cycle? do
{:over_limit, {last_cycle, last_cycle_usage}}
if exceeds_last_two_usage_cycles?(usage, limit) do
{:over_limit, {usage.last_cycle.date_range, usage.last_cycle.total}}
else
{:below_limit, {last_cycle, last_cycle_usage}}
{:below_limit, {usage.last_cycle.date_range, usage.last_cycle.total}}
end
end
defp check_site_limit_for_enterprise(subscriber) do
@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.1)
Enum.all?([usage.last_cycle, usage.penultimate_cycle], fn usage ->
not Quota.below_limit?(usage.total, limit)
end)
end
defp check_site_usage_for_enterprise(subscriber) do
limit = subscriber.enterprise_plan.site_limit
usage = Plausible.Billing.Quota.site_usage(subscriber)
usage = Quota.site_usage(subscriber)
if Plausible.Billing.Quota.below_limit?(usage, limit) do
if Quota.below_limit?(usage, limit) do
{:below_limit, {usage, limit}}
else
{:over_limit, {usage, limit}}

View File

@ -55,14 +55,14 @@ defmodule Plausible.Workers.SendTrialNotifications do
end
defp send_tomorrow_reminder(user) do
usage = Plausible.Billing.usage_breakdown(user)
usage = Plausible.Billing.Quota.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.usage_breakdown(user)
usage = Plausible.Billing.Quota.usage_cycle(user, :last_30_days)
PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|> Plausible.Mailer.send()

View File

@ -0,0 +1,9 @@
defmodule Plausible.Repo.Migrations.AddAllowNextUpgradeOverrideToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :allow_next_upgrade_override, :boolean, null: false, default: false
end
end
end

View File

@ -5,94 +5,6 @@ defmodule Plausible.BillingTest do
alias Plausible.Billing
alias Plausible.Billing.Subscription
describe "last_two_billing_cycles" do
test "billing on the 1st" do
last_bill_date = ~D[2021-01-01]
today = ~D[2021-01-02]
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
expected_cycles = {
Date.range(~D[2020-11-01], ~D[2020-11-30]),
Date.range(~D[2020-12-01], ~D[2020-12-31])
}
assert Billing.last_two_billing_cycles(user, today) == expected_cycles
end
test "in case of yearly billing, cycles are normalized as if they were paying monthly" do
last_bill_date = ~D[2020-09-01]
today = ~D[2021-02-02]
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
expected_cycles = {
Date.range(~D[2020-12-01], ~D[2020-12-31]),
Date.range(~D[2021-01-01], ~D[2021-01-31])
}
assert Billing.last_two_billing_cycles(user, today) == expected_cycles
end
end
describe "last_two_billing_months_usage" do
test "counts events from last two billing cycles" do
last_bill_date = ~D[2021-01-01]
today = ~D[2021-01-02]
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
site = insert(:site, members: [user])
create_pageviews([
%{site: site, timestamp: ~N[2021-01-01 00:00:00]},
%{site: site, timestamp: ~N[2020-12-31 00:00:00]},
%{site: site, timestamp: ~N[2020-11-01 00:00:00]},
%{site: site, timestamp: ~N[2020-10-31 00:00:00]}
])
assert Billing.last_two_billing_months_usage(user, today) == {1, 1}
end
test "only considers sites that the user owns" do
last_bill_date = ~D[2021-01-01]
today = ~D[2021-01-02]
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
owner_site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
admin_site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :admin)
]
)
create_pageviews([
%{site: owner_site, timestamp: ~N[2020-12-31 00:00:00]},
%{site: admin_site, timestamp: ~N[2020-12-31 00:00:00]},
%{site: owner_site, timestamp: ~N[2020-11-01 00:00:00]},
%{site: admin_site, timestamp: ~N[2020-11-01 00:00:00]}
])
assert Billing.last_two_billing_months_usage(user, today) == {1, 1}
end
test "gets event count from last month and this one" do
user =
insert(:user,
subscription:
build(:subscription, last_bill_date: Timex.today() |> Timex.shift(days: -1))
)
assert Billing.last_two_billing_months_usage(user) == {0, 0}
end
end
describe "trial_days_left" do
test "is 30 days for new signup" do
user = insert(:user)
@ -203,22 +115,40 @@ defmodule Plausible.BillingTest do
@plan_id_10k "654177"
@plan_id_100k "654178"
describe "subscription_created" do
test "creates a subscription" do
user = insert(:user)
Billing.subscription_created(%{
@subscription_created_params %{
"alert_name" => "subscription_created",
"passthrough" => "",
"email" => "",
"subscription_id" => @subscription_id,
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"status" => "active",
"next_bill_date" => "2019-06-01",
"unit_price" => "6.00",
"currency" => "EUR"
})
}
@subscription_updated_params %{
"alert_name" => "subscription_updated",
"passthrough" => "",
"subscription_id" => "",
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"old_status" => "active",
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
}
describe "subscription_created" do
test "creates a subscription" do
user = insert(:user)
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert subscription.paddle_subscription_id == @subscription_id
@ -230,19 +160,8 @@ defmodule Plausible.BillingTest do
test "create with email address" do
user = insert(:user)
Billing.subscription_created(%{
"passthrough" => "",
"email" => user.email,
"alert_name" => "subscription_created",
"subscription_id" => @subscription_id,
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"status" => "active",
"next_bill_date" => "2019-06-01",
"unit_price" => "6.00",
"currency" => "EUR"
})
%{@subscription_created_params | "email" => user.email}
|> Billing.subscription_created()
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert subscription.paddle_subscription_id == @subscription_id
@ -254,22 +173,21 @@ defmodule Plausible.BillingTest do
user = insert(:user)
site = insert(:site, locked: true, members: [user])
Billing.subscription_created(%{
"alert_name" => "subscription_created",
"subscription_id" => @subscription_id,
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"status" => "active",
"next_bill_date" => "2019-06-01",
"unit_price" => "6.00",
"currency" => "EUR"
})
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
refute Repo.reload!(site).locked
end
test "sets user.allow_next_upgrade_override field to false" do
user = insert(:user, allow_next_upgrade_override: true)
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
refute Repo.reload!(user).allow_next_upgrade_override
end
test "if user upgraded to an enterprise plan, their API key limits are automatically adjusted" do
user = insert(:user)
@ -282,18 +200,8 @@ defmodule Plausible.BillingTest do
api_key = insert(:api_key, user: user, hourly_request_limit: 1)
Billing.subscription_created(%{
"alert_name" => "subscription_created",
"subscription_id" => @subscription_id,
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"status" => "active",
"next_bill_date" => "2019-06-01",
"unit_price" => "6.00",
"currency" => "EUR"
})
%{@subscription_created_params | "passthrough" => user.id}
|> Billing.subscription_created()
assert Repo.reload!(api_key).hourly_request_limit == plan.hourly_api_request_limit
end
@ -304,21 +212,15 @@ defmodule Plausible.BillingTest do
user = insert(:user)
subscription = insert(:subscription, user: user)
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"subscription_plan_id" => "new-plan-id",
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"passthrough" => user.id
})
|> Billing.subscription_updated()
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert subscription.paddle_plan_id == "new-plan-id"
assert subscription.paddle_plan_id == @plan_id_10k
assert subscription.next_bill_amount == "12.00"
end
@ -327,23 +229,31 @@ defmodule Plausible.BillingTest do
subscription = insert(:subscription, user: user, status: Subscription.Status.past_due())
site = insert(:site, locked: true, members: [user])
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"subscription_plan_id" => "new-plan-id",
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"old_status" => "past_due",
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"old_status" => "past_due"
})
|> Billing.subscription_updated()
refute Repo.reload!(site).locked
end
test "sets user.allow_next_upgrade_override field to false" do
user = insert(:user, allow_next_upgrade_override: true)
subscription = insert(:subscription, user: user)
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"passthrough" => user.id
})
|> Billing.subscription_updated()
refute Repo.reload!(user).allow_next_upgrade_override
end
test "if user upgraded to an enterprise plan, their API key limits are automatically adjusted" do
user = insert(:user)
subscription = insert(:subscription, user: user)
@ -357,19 +267,13 @@ defmodule Plausible.BillingTest do
api_key = insert(:api_key, user: user, hourly_request_limit: 1)
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"subscription_plan_id" => "new-plan-id",
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"old_status" => "past_due",
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"subscription_plan_id" => plan.paddle_plan_id
})
|> Billing.subscription_updated()
assert Repo.reload!(api_key).hourly_request_limit == plan.hourly_api_request_limit
end
@ -386,19 +290,13 @@ defmodule Plausible.BillingTest do
subscription = insert(:subscription, user: user)
site = insert(:site, locked: true, members: [user])
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"subscription_plan_id" => @plan_id_100k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"old_status" => "past_due",
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"subscription_plan_id" => @plan_id_100k
})
|> Billing.subscription_updated()
assert Repo.reload!(site).locked == false
assert Repo.reload!(user).grace_period == nil
@ -416,19 +314,12 @@ defmodule Plausible.BillingTest do
subscription = insert(:subscription, user: user)
site = insert(:site, locked: true, members: [user])
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => subscription.paddle_subscription_id,
"subscription_plan_id" => @plan_id_10k,
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"old_status" => "past_due",
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"passthrough" => user.id
})
|> Billing.subscription_updated()
assert Repo.reload!(site).locked == true
assert Repo.reload!(user).grace_period.allowance_required == 11_000
@ -438,20 +329,14 @@ defmodule Plausible.BillingTest do
user = insert(:user)
res =
Billing.subscription_updated(%{
"alert_name" => "subscription_updated",
@subscription_updated_params
|> Map.merge(%{
"subscription_id" => "666",
"subscription_plan_id" => "new-plan-id",
"update_url" => "update_url.com",
"cancel_url" => "cancel_url.com",
"passthrough" => user.id,
"status" => "active",
"next_bill_date" => "2019-06-01",
"new_unit_price" => "12.00",
"currency" => "EUR"
"passthrough" => user.id
})
|> Billing.subscription_updated()
assert res == {:ok, nil}
assert {:ok, nil} = res
end
end

View File

@ -14,6 +14,7 @@ defmodule Plausible.Billing.QuotaTest do
@v2_plan_id "654177"
@v3_plan_id "749342"
@v3_business_plan_id "857481"
@v4_1m_plan_id "857101"
describe "site_limit/1" do
@describetag :full_build_only
@ -136,29 +137,107 @@ defmodule Plausible.Billing.QuotaTest do
end
end
describe "exceeded_limits/2" do
test "returns limits that are exceeded" do
describe "ensure_can_subscribe_to_plan/2" do
test "returns :ok when site and team member limits are reached but not exceeded" do
user = insert(:user)
usage = %{
monthly_pageviews: 10_001,
team_members: 2,
sites: 51
monthly_pageviews: %{last_30_days: %{total: 1}},
team_members: 3,
sites: 10
}
plan = Plans.find(@v3_plan_id)
plan = Plans.find(@v4_1m_plan_id)
assert Quota.exceeded_limits(usage, plan) == [:monthly_pageview_limit, :site_limit]
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage) == :ok
end
test "if limits are reached, they're not exceeded" do
test "returns all exceeded limits" do
user = insert(:user)
usage = %{
monthly_pageviews: 10_000,
team_members: 2,
sites: 50
monthly_pageviews: %{last_30_days: %{total: 1_150_001}},
team_members: 4,
sites: 11
}
plan = Plans.find(@v4_1m_plan_id)
{:error, %{exceeded_limits: exceeded_limits}} =
Quota.ensure_can_subscribe_to_plan(user, plan, usage)
assert :monthly_pageview_limit in exceeded_limits
assert :team_member_limit in exceeded_limits
assert :site_limit in exceeded_limits
end
test "by the last 30 days usage, pageview limit for 10k plan is only exceeded when 30% over the limit" do
user = insert(:user)
usage_within_pageview_limit = %{
monthly_pageviews: %{last_30_days: %{total: 13_000}},
team_members: 1,
sites: 1
}
usage_over_pageview_limit = %{
monthly_pageviews: %{last_30_days: %{total: 13_001}},
team_members: 1,
sites: 1
}
plan = Plans.find(@v3_plan_id)
assert Quota.exceeded_limits(usage, plan) == []
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_within_pageview_limit) == :ok
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_over_pageview_limit) ==
{:error, %{exceeded_limits: [:monthly_pageview_limit]}}
end
test "by the last 30 days usage, pageview limit for all plans above 10k is exceeded when 15% over the limit" do
user = insert(:user)
usage_within_pageview_limit = %{
monthly_pageviews: %{last_30_days: %{total: 1_150_000}},
team_members: 1,
sites: 1
}
usage_over_pageview_limit = %{
monthly_pageviews: %{last_30_days: %{total: 1_150_001}},
team_members: 1,
sites: 1
}
plan = Plans.find(@v4_1m_plan_id)
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_within_pageview_limit) == :ok
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_over_pageview_limit) ==
{:error, %{exceeded_limits: [:monthly_pageview_limit]}}
end
test "by billing cycles usage, pageview limit is exceeded when last two billing cycles exceed by 10%" do
user = insert(:user)
usage_within_pageview_limit = %{
monthly_pageviews: %{penultimate_cycle: %{total: 11_000}, last_cycle: %{total: 10_999}},
team_members: 1,
sites: 1
}
usage_over_pageview_limit = %{
monthly_pageviews: %{penultimate_cycle: %{total: 11_000}, last_cycle: %{total: 11_000}},
team_members: 1,
sites: 1
}
plan = Plans.find(@v3_plan_id)
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_within_pageview_limit) == :ok
assert Quota.ensure_can_subscribe_to_plan(user, plan, usage_over_pageview_limit) ==
{:error, %{exceeded_limits: [:monthly_pageview_limit]}}
end
end
@ -201,52 +280,6 @@ defmodule Plausible.Billing.QuotaTest do
end
end
describe "monthly_pageview_usage/1" do
test "is 0 with no events" do
user = insert(:user)
assert Quota.monthly_pageview_usage(user) == 0
end
test "counts the total number of events from all sites the user owns" do
user = insert(:user)
site1 = insert(:site, members: [user])
site2 = insert(:site, members: [user])
populate_stats(site1, [
build(:pageview),
build(:pageview)
])
populate_stats(site2, [
build(:pageview),
build(:event, name: "custom events")
])
assert Quota.monthly_pageview_usage(user) == 4
end
test "only counts usage from sites where the user is the owner" do
user = insert(:user)
insert(:site,
domain: "site-with-no-views.com",
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
insert(:site,
domain: "test-site.com",
memberships: [
build(:site_membership, user: user, role: :admin)
]
)
assert Quota.monthly_pageview_usage(user) == 0
end
end
describe "team_member_usage/1" do
test "returns the number of members in all of the sites the user owns" do
me = insert(:user)
@ -587,4 +620,123 @@ defmodule Plausible.Billing.QuotaTest do
assert [Plausible.Billing.Feature.StatsAPI] == Quota.allowed_features_for(user)
end
end
describe "usage_cycle/1" do
setup do
user = insert(:user)
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, timestamp: ~N[2023-04-01 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-04-02 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-04-03 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-04-04 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-04-05 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-05-01 00:00:00], name: "pageview"),
build(:event, timestamp: ~N[2023-05-02 00:00:00], name: "pageview"),
build(:event, timestamp: ~N[2023-05-03 00:00:00], name: "pageview"),
build(:event, timestamp: ~N[2023-05-04 00:00:00], name: "pageview"),
build(:event, timestamp: ~N[2023-05-05 00:00:00], name: "pageview"),
build(:event, timestamp: ~N[2023-06-01 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-06-02 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-06-03 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-06-04 00:00:00], name: "custom"),
build(:event, timestamp: ~N[2023-06-05 00:00:00], name: "custom")
])
{:ok, %{user: user}}
end
test "returns usage and date_range for the given billing month", %{user: user} do
last_bill_date = ~D[2023-06-03]
today = ~D[2023-06-05]
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, today)
assert %{date_range: last_cycle, pageviews: 3, custom_events: 2, total: 5} =
Quota.usage_cycle(user, :last_cycle, today)
assert %{date_range: current_cycle, pageviews: 0, custom_events: 3, total: 3} =
Quota.usage_cycle(user, :current_cycle, 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])
assert current_cycle == Date.range(~D[2023-06-03], ~D[2023-07-02])
end
test "returns usage and date_range for the last 30 days", %{user: user} 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, today)
assert last_30_days == Date.range(~D[2023-05-02], ~D[2023-06-01])
end
test "only considers sites that the user owns", %{user: user} do
different_site =
insert(:site,
memberships: [
build(:site_membership, user: user, role: :admin)
]
)
populate_stats(different_site, [
build(:event, timestamp: ~N[2023-05-05 00:00:00], name: "custom")
])
last_bill_date = ~D[2023-06-03]
today = ~D[2023-06-05]
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, today)
assert last_cycle == Date.range(~D[2023-05-03], ~D[2023-06-02])
end
test "in case of yearly billing, cycles are normalized as if they were paying monthly" do
last_bill_date = ~D[2020-09-01]
today = ~D[2021-02-02]
user = insert(:user, subscription: build(:subscription, last_bill_date: last_bill_date))
assert %{date_range: penultimate_cycle} =
Quota.usage_cycle(user, :penultimate_cycle, today)
assert %{date_range: last_cycle} =
Quota.usage_cycle(user, :last_cycle, today)
assert %{date_range: current_cycle} =
Quota.usage_cycle(user, :current_cycle, 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])
assert current_cycle == Date.range(~D[2021-02-01], ~D[2021-02-28])
end
test "returns correct billing months when last_bill_date is the first day of the year" do
last_bill_date = ~D[2021-01-01]
today = ~D[2021-01-02]
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, today)
assert %{date_range: last_cycle, total: 0} =
Quota.usage_cycle(user, :last_cycle, today)
assert %{date_range: current_cycle, total: 0} =
Quota.usage_cycle(user, :current_cycle, 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])
assert current_cycle == Date.range(~D[2021-01-01], ~D[2021-01-31])
end
end
end

View File

@ -743,6 +743,292 @@ defmodule PlausibleWeb.AuthControllerTest do
conn = get(conn, "/settings")
refute html_response(conn, 200) =~ "Invoices"
end
@tag :full_build_only
test "renders pageview usage for current, last, and penultimate billing cycles", %{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20)),
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -50)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -50))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
last_bill_date: last_bill_date
)
doc = get(conn, "/settings") |> html_response(200)
assert text_of_element(doc, "#billing_cycle_tab_current_cycle") =~
Date.range(
last_bill_date,
Timex.shift(last_bill_date, months: 1, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(doc, "#billing_cycle_tab_last_cycle") =~
Date.range(
Timex.shift(last_bill_date, months: -1),
Timex.shift(last_bill_date, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(doc, "#billing_cycle_tab_penultimate_cycle") =~
Date.range(
Timex.shift(last_bill_date, months: -2),
Timex.shift(last_bill_date, months: -1, days: -1)
)
|> PlausibleWeb.TextHelpers.format_date_range()
assert text_of_element(doc, "#total_pageviews_current_cycle") =~
"Total billable pageviews 1"
assert text_of_element(doc, "#pageviews_current_cycle") =~ "Pageviews 1"
assert text_of_element(doc, "#custom_events_current_cycle") =~ "Custom events 0"
assert text_of_element(doc, "#total_pageviews_last_cycle") =~ "Total billable pageviews 1"
assert text_of_element(doc, "#pageviews_last_cycle") =~ "Pageviews 0"
assert text_of_element(doc, "#custom_events_last_cycle") =~ "Custom events 1"
assert text_of_element(doc, "#total_pageviews_penultimate_cycle") =~
"Total billable pageviews 2"
assert text_of_element(doc, "#pageviews_penultimate_cycle") =~ "Pageviews 1"
assert text_of_element(doc, "#custom_events_penultimate_cycle") =~ "Custom events 1"
end
@tag :full_build_only
test "renders pageview usage per billing cycle for active subscribers", %{
conn: conn,
user: user
} do
assert_cycles_rendered = fn doc ->
refute element_exists?(doc, "#total_pageviews_last_30_days")
assert element_exists?(doc, "#total_pageviews_current_cycle")
assert element_exists?(doc, "#total_pageviews_last_cycle")
assert element_exists?(doc, "#total_pageviews_penultimate_cycle")
end
# for an active subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :active,
last_bill_date: Timex.shift(Timex.now(), months: -6)
)
get(conn, "/settings") |> html_response(200) |> assert_cycles_rendered.()
# for a past_due subscription
subscription =
subscription
|> Plausible.Billing.Subscription.changeset(%{status: :past_due})
|> Repo.update!()
get(conn, "/settings") |> html_response(200) |> assert_cycles_rendered.()
# for a deleted (but not expired) subscription
subscription
|> Plausible.Billing.Subscription.changeset(%{
status: :deleted,
next_bill_date: Timex.shift(Timex.now(), months: 6)
})
|> Repo.update!()
get(conn, "/settings") |> html_response(200) |> assert_cycles_rendered.()
end
@tag :full_build_only
test "penultimate cycle is disabled if there's no usage", %{conn: conn, user: user} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: last_bill_date
)
doc = get(conn, "/settings") |> html_response(200)
assert text_of_attr(find(doc, "#monthly_pageview_usage_container"), "x-data") ==
"{ tab: 'current_cycle' }"
assert class_of_element(doc, "#billing_cycle_tab_penultimate_cycle button") =~
"pointer-events-none"
assert text_of_element(doc, "#billing_cycle_tab_penultimate_cycle") =~ "Not available"
end
@tag :full_build_only
test "penultimate and last cycles are both disabled if there's no usage", %{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: last_bill_date
)
doc = get(conn, "/settings") |> html_response(200)
assert text_of_attr(find(doc, "#monthly_pageview_usage_container"), "x-data") ==
"{ tab: 'current_cycle' }"
assert class_of_element(doc, "#billing_cycle_tab_last_cycle button") =~
"pointer-events-none"
assert text_of_element(doc, "#billing_cycle_tab_last_cycle") =~ "Not available"
assert class_of_element(doc, "#billing_cycle_tab_penultimate_cycle button") =~
"pointer-events-none"
assert text_of_element(doc, "#billing_cycle_tab_penultimate_cycle") =~ "Not available"
end
@tag :full_build_only
test "when last cycle usage is 0, it's still not disabled if penultimate cycle has usage", %{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -50))
])
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: last_bill_date
)
doc = get(conn, "/settings") |> html_response(200)
assert text_of_attr(find(doc, "#monthly_pageview_usage_container"), "x-data") ==
"{ tab: 'current_cycle' }"
refute class_of_element(doc, "#billing_cycle_tab_last_cycle") =~ "pointer-events-none"
refute text_of_element(doc, "#billing_cycle_tab_last_cycle") =~ "Not available"
refute class_of_element(doc, "#billing_cycle_tab_penultimate_cycle") =~
"pointer-events-none"
refute text_of_element(doc, "#billing_cycle_tab_penultimate_cycle") =~ "Not available"
end
@tag :full_build_only
test "renders last 30 days pageview usage for trials and non-active/free_10k subscriptions",
%{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -1)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -10)),
build(:event, name: "customevent", timestamp: Timex.shift(Timex.now(), days: -20))
])
assert_usage = fn doc ->
refute element_exists?(doc, "#total_pageviews_current_cycle")
assert text_of_element(doc, "#total_pageviews_last_30_days") =~
"Total billable pageviews (last 30 days) 3"
assert text_of_element(doc, "#pageviews_last_30_days") =~ "Pageviews 1"
assert text_of_element(doc, "#custom_events_last_30_days") =~ "Custom events 2"
end
# for a trial user
get(conn, "/settings") |> html_response(200) |> assert_usage.()
# for an expired subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
last_bill_date: ~D[2022-01-01],
next_bill_date: ~D[2022-02-01]
)
get(conn, "/settings") |> html_response(200) |> assert_usage.()
# for a paused subscription
subscription =
subscription
|> Plausible.Billing.Subscription.changeset(%{status: :paused})
|> Repo.update!()
get(conn, "/settings") |> html_response(200) |> assert_usage.()
# for a free_10k subscription (without a `last_bill_date`)
Repo.delete!(subscription)
Plausible.Billing.Subscription.free(%{user_id: user.id})
|> Repo.insert!()
get(conn, "/settings") |> html_response(200) |> assert_usage.()
end
@tag :full_build_only
test "renders sites usage and limit", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
insert(:site, members: [user])
site_usage_row_text =
conn
|> get("/settings")
|> html_response(200)
|> text_of_element("#site-usage-row")
assert site_usage_row_text =~ "Owned sites 1 / 50"
end
@tag :full_build_only
test "renders team members usage and limit", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v4_plan_id, user: user)
team_member_usage_row_text =
conn
|> get("/settings")
|> html_response(200)
|> text_of_element("#team-member-usage-row")
assert team_member_usage_row_text =~ "Team members 0 / 3"
end
end
describe "PUT /settings" do

View File

@ -52,7 +52,7 @@ defmodule PlausibleWeb.BillingControllerTest do
describe "POST /change-plan" do
setup [:create_user, :log_in]
test "errors if usage exceeds some limit on the new plan", %{conn: conn, user: user} do
test "errors if usage exceeds team member limit on the new plan", %{conn: conn, user: user} do
insert(:subscription, user: user, paddle_plan_id: "123123")
insert(:site,
@ -71,6 +71,54 @@ defmodule PlausibleWeb.BillingControllerTest do
"Unable to subscribe to this plan because the following limits are exceeded: [:team_member_limit]"
end
test "errors if usage exceeds site limit even when user.next_upgrade_override is true", %{
conn: conn,
user: user
} do
insert(:subscription, user: user, paddle_plan_id: "123123")
for _ <- 1..11, do: insert(:site, members: [user])
Plausible.Users.allow_next_upgrade_override(user)
conn = post(conn, Routes.billing_path(conn, :change_plan, @v4_growth_plan))
subscription = Plausible.Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "are exceeded: [:site_limit]"
assert subscription.paddle_plan_id == "123123"
end
test "can override allowing to upgrade when pageview limit is exceeded", %{
conn: conn,
user: user
} do
insert(:subscription, user: user, paddle_plan_id: "123123")
site = insert(:site, members: [user])
now = NaiveDateTime.utc_now()
generate_usage_for(site, 11_000, Timex.shift(now, days: -5))
generate_usage_for(site, 11_000, Timex.shift(now, days: -35))
conn1 = post(conn, Routes.billing_path(conn, :change_plan, @v4_growth_plan))
subscription = Plausible.Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert Phoenix.Flash.get(conn1.assigns.flash, :error) =~
"are exceeded: [:monthly_pageview_limit]"
assert subscription.paddle_plan_id == "123123"
Plausible.Users.allow_next_upgrade_override(user)
conn2 = post(conn, Routes.billing_path(conn, :change_plan, @v4_growth_plan))
subscription = Plausible.Repo.reload!(subscription)
assert Phoenix.Flash.get(conn2.assigns.flash, :success) =~ "Plan changed successfully"
assert subscription.paddle_plan_id == @v4_growth_plan
end
test "calls Paddle API to update subscription", %{conn: conn, user: user} do
insert(:subscription, user: user)

View File

@ -476,6 +476,25 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
assert class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
end
test "checkout is not disabled when pageview usage exceeded but next upgrade allowed by override",
%{
conn: conn,
user: user
} do
site = insert(:site, members: [user])
now = NaiveDateTime.utc_now()
generate_usage_for(site, 11_000, Timex.shift(now, days: -5))
generate_usage_for(site, 11_000, Timex.shift(now, days: -35))
Plausible.Users.allow_next_upgrade_override(user)
{:ok, _lv, doc} = get_liveview(conn)
refute text_of_element(doc, @growth_plan_box) =~ "Your usage exceeds this plan"
refute class_of_element(doc, @growth_checkout_button) =~ "pointer-events-none"
end
@tag :full_build_only
test "warns about losing access to a feature", %{conn: conn, user: user} do
site = insert(:site, members: [user])

View File

@ -139,8 +139,8 @@ defmodule Plausible.TestUtils do
|> Plug.Conn.fetch_session()
end
def generate_usage_for(site, i) do
events = for _i <- 1..i, do: Factory.build(:pageview)
def generate_usage_for(site, i, timestamp \\ NaiveDateTime.utc_now()) do
events = for _i <- 1..i, do: Factory.build(:pageview, timestamp: timestamp)
populate_stats(site, events)
:ok
end

View File

@ -7,6 +7,7 @@ defmodule Plausible.Workers.CheckUsageTest do
setup [:create_user, :create_site]
@paddle_id_10k "558018"
@date_range Date.range(Timex.today(), Timex.today())
test "ignores user without subscription" do
CheckUsage.perform(nil)
@ -30,12 +31,14 @@ 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
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 9_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
|> stub(:last_two_billing_months_usage, fn _user -> {9_000, 11_000} end)
insert(:subscription,
user: user,
@ -43,7 +46,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period == nil
@ -52,12 +55,14 @@ defmodule Plausible.Workers.CheckUsageTest do
test "does not send an email if account is over the limit by less than 10%", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 10_999},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
|> stub(:last_two_billing_months_usage, fn _user -> {10_999, 11_000} end)
insert(:subscription,
user: user,
@ -65,7 +70,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period == nil
@ -74,11 +79,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "sends an email when an account is over their limit for two consecutive billing months", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -87,7 +94,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_email_delivered_with(
to: [user],
@ -100,11 +107,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "sends an email suggesting enterprise plan when usage is greater than 10M ", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000_000, 11_000_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000_000},
last_cycle: %{date_range: @date_range, total: 11_000_000}
}
end)
insert(:subscription,
@ -113,7 +122,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_delivered_email_matches(%{html_body: html_body})
@ -124,11 +133,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "skips checking users who already have a grace period", %{user: user} do
user |> Plausible.Auth.GracePeriod.start_changeset(12_000) |> Repo.update()
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -137,7 +148,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_no_emails_delivered()
assert Repo.reload(user).grace_period.allowance_required == 12_000
@ -146,11 +157,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "recommends a plan to upgrade to", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -159,7 +172,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_delivered_email_matches(%{
html_body: html_body
@ -174,11 +187,13 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {1_100_000, 1_100_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
last_cycle: %{date_range: @date_range, total: 1_100_000}
}
end)
enterprise_plan = insert(:enterprise_plan, user: user, monthly_pageview_limit: 1_000_000)
@ -189,7 +204,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_email_delivered_with(
to: [{nil, "enterprise@plausible.io"}],
@ -201,11 +216,13 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {1, 1} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1},
last_cycle: %{date_range: @date_range, total: 1}
}
end)
enterprise_plan = insert(:enterprise_plan, user: user, site_limit: 2)
@ -220,7 +237,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_email_delivered_with(
to: [{nil, "enterprise@plausible.io"}],
@ -229,11 +246,13 @@ defmodule Plausible.Workers.CheckUsageTest do
end
test "starts grace period when plan is outgrown", %{user: user} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {1_100_000, 1_100_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
last_cycle: %{date_range: @date_range, total: 1_100_000}
}
end)
enterprise_plan = insert(:enterprise_plan, user: user, monthly_pageview_limit: 1_000_000)
@ -244,7 +263,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
end
end
@ -253,11 +272,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "checks usage one day after the last_bill_date", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -266,7 +287,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
CheckUsage.perform(nil, billing_stub)
CheckUsage.perform(nil, quota_stub)
assert_email_delivered_with(
to: [user],
@ -277,11 +298,13 @@ defmodule Plausible.Workers.CheckUsageTest do
test "does not check exactly one month after last_bill_date", %{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -290,7 +313,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: ~D[2021-03-28]
)
CheckUsage.perform(nil, billing_stub, ~D[2021-03-28])
CheckUsage.perform(nil, quota_stub, ~D[2021-03-28])
assert_no_emails_delivered()
end
@ -299,11 +322,13 @@ defmodule Plausible.Workers.CheckUsageTest do
%{
user: user
} do
billing_stub =
Plausible.Billing
|> stub(:last_two_billing_months_usage, fn _user -> {11_000, 11_000} end)
|> stub(:last_two_billing_cycles, fn _user ->
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
quota_stub =
Plausible.Billing.Quota
|> stub(:monthly_pageview_usage, fn _user ->
%{
penultimate_cycle: %{date_range: @date_range, total: 11_000},
last_cycle: %{date_range: @date_range, total: 11_000}
}
end)
insert(:subscription,
@ -312,7 +337,7 @@ defmodule Plausible.Workers.CheckUsageTest do
last_bill_date: ~D[2021-06-29]
)
CheckUsage.perform(nil, billing_stub, ~D[2021-08-30])
CheckUsage.perform(nil, quota_stub, ~D[2021-08-30])
assert_email_delivered_with(
to: [user],

View File

@ -64,6 +64,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
test "sends an upgrade email the day before the trial ends" do
user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 1))
site = insert(:site, members: [user])
usage = %{total: 3, custom_events: 0}
populate_stats(site, [
build(:pageview),
@ -73,12 +74,13 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
perform_job(SendTrialNotifications, %{})
assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", {3, 0}))
assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", usage))
end
test "sends an upgrade email the day the trial ends" do
user = insert(:user, trial_expiry_date: Timex.today())
site = insert(:site, members: [user])
usage = %{total: 3, custom_events: 0}
populate_stats(site, [
build(:pageview),
@ -88,13 +90,14 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
perform_job(SendTrialNotifications, %{})
assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "today", {3, 0}))
assert_delivered_email(PlausibleWeb.Email.trial_upgrade_email(user, "today", usage))
end
test "does not include custom event note if user has not used custom events" do
user = insert(:user, trial_expiry_date: Timex.today())
usage = %{total: 9_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~
"In the last month, your account has used 9,000 billable pageviews."
@ -102,8 +105,9 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
test "includes custom event note if user has used custom events" do
user = insert(:user, trial_expiry_date: Timex.today())
usage = %{total: 9_100, custom_events: 100}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 100})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~
"In the last month, your account has used 9,100 billable pageviews and custom events in total."
@ -145,72 +149,83 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
describe "Suggested plans" do
test "suggests 10k/mo plan" do
user = insert(:user)
usage = %{total: 9_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 10k/mo plan."
end
test "suggests 100k/mo plan" do
user = insert(:user)
usage = %{total: 90_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {90_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 100k/mo plan."
end
test "suggests 200k/mo plan" do
user = insert(:user)
usage = %{total: 180_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {180_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 200k/mo plan."
end
test "suggests 500k/mo plan" do
user = insert(:user)
usage = %{total: 450_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {450_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 500k/mo plan."
end
test "suggests 1m/mo plan" do
user = insert(:user)
usage = %{total: 900_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {900_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 1M/mo plan."
end
test "suggests 2m/mo plan" do
user = insert(:user)
usage = %{total: 1_800_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {1_800_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 2M/mo plan."
end
test "suggests 5m/mo plan" do
user = insert(:user)
usage = %{total: 4_500_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {4_500_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 5M/mo plan."
end
test "suggests 10m/mo plan" do
user = insert(:user)
usage = %{total: 9_000_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {9_000_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "we recommend you select a 10M/mo plan."
end
test "does not suggest a plan above that" do
user = insert(:user)
usage = %{total: 20_000_000, custom_events: 0}
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {20_000_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "please reply back to this email to get a quote for your volume"
end
test "does not suggest a plan when user is switching to an enterprise plan" do
user = insert(:user)
usage = %{total: 10_000, custom_events: 0}
insert(:enterprise_plan, user: user, paddle_plan_id: "enterprise-plan-id")
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", {10_000, 0})
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
assert email.html_body =~ "please reply back to this email to get a quote for your volume"
end
end