mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
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:
parent
bd7deb5631
commit
57188a402a
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
user
|
||||
|> Billing.usage_breakdown()
|
||||
|> Tuple.sum()
|
||||
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
|
||||
|> 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.
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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} ->
|
||||
|
@ -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,
|
||||
|
@ -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"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 ->
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
limit = subscriber.enterprise_plan.site_limit
|
||||
usage = Plausible.Billing.Quota.site_usage(subscriber)
|
||||
@spec exceeds_last_two_usage_cycles?(Quota.monthly_pageview_usage(), non_neg_integer()) ::
|
||||
boolean()
|
||||
|
||||
if Plausible.Billing.Quota.below_limit?(usage, limit) do
|
||||
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 = Quota.site_usage(subscriber)
|
||||
|
||||
if Quota.below_limit?(usage, limit) do
|
||||
{:below_limit, {usage, limit}}
|
||||
else
|
||||
{:over_limit, {usage, limit}}
|
||||
|
@ -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()
|
||||
|
@ -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
|
@ -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"
|
||||
|
||||
@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",
|
||||
"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)
|
||||
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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],
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user