mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
Usage & Limits section (#3291)
* Update user_settings template to HEEX * Move site_limit function to Billing.Quota * Create Billing.Quota.site_usage function * Move monthly_pageview_limit function to Billing.Quota * Create Billing.Quota.monthly_pageview_usage function * Add "Usage & Limits" section to user settings page * Apply suggestions from code review
This commit is contained in:
parent
8c870f7c6b
commit
5d9d05c1f6
@ -108,11 +108,8 @@ defmodule Plausible.Billing do
|
||||
Timex.diff(user.trial_expiry_date, Timex.today(), :days)
|
||||
end
|
||||
|
||||
def usage(user) do
|
||||
{pageviews, custom_events} = usage_breakdown(user)
|
||||
pageviews + custom_events
|
||||
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)
|
||||
|
||||
@ -248,7 +245,7 @@ defmodule Plausible.Billing do
|
||||
case user.grace_period do
|
||||
%GracePeriod{allowance_required: allowance_required} ->
|
||||
new_monthly_pageview_limit =
|
||||
Plausible.Billing.Plans.monthly_pageview_limit(user.subscription)
|
||||
Plausible.Billing.Quota.monthly_pageview_limit(user.subscription)
|
||||
|
||||
if new_monthly_pageview_limit > allowance_required do
|
||||
user
|
||||
|
@ -105,36 +105,7 @@ defmodule Plausible.Billing.Plans do
|
||||
end)
|
||||
end
|
||||
|
||||
@limit_sites_since ~D[2021-05-05]
|
||||
@spec site_limit(Plausible.Auth.User.t()) :: non_neg_integer() | :unlimited
|
||||
@doc """
|
||||
Returns the limit of sites a user can have.
|
||||
|
||||
For enterprise customers, returns :unlimited. The site limit is checked in a
|
||||
background job so as to avoid service disruption.
|
||||
"""
|
||||
def site_limit(user) do
|
||||
cond do
|
||||
Application.get_env(:plausible, :is_selfhost) -> :unlimited
|
||||
Timex.before?(user.inserted_at, @limit_sites_since) -> :unlimited
|
||||
true -> get_site_limit_from_plan(user)
|
||||
end
|
||||
end
|
||||
|
||||
@site_limit_for_trials 50
|
||||
@site_limit_for_free_10k 50
|
||||
defp get_site_limit_from_plan(user) do
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
|
||||
case get_subscription_plan(user.subscription) do
|
||||
%Plausible.Billing.EnterprisePlan{} -> :unlimited
|
||||
%Plausible.Billing.Plan{site_limit: site_limit} -> site_limit
|
||||
:free_10k -> @site_limit_for_free_10k
|
||||
nil -> @site_limit_for_trials
|
||||
end
|
||||
end
|
||||
|
||||
defp get_subscription_plan(subscription) do
|
||||
def get_subscription_plan(subscription) do
|
||||
if subscription && subscription.paddle_plan_id == "free_10k" do
|
||||
:free_10k
|
||||
else
|
||||
@ -159,25 +130,6 @@ defmodule Plausible.Billing.Plans do
|
||||
end
|
||||
end
|
||||
|
||||
@spec monthly_pageview_limit(Plausible.Billing.Subscription.t()) :: non_neg_integer() | nil
|
||||
def monthly_pageview_limit(subscription) do
|
||||
case get_subscription_plan(subscription) do
|
||||
%Plausible.Billing.EnterprisePlan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
%Plausible.Billing.Plan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
:free_10k ->
|
||||
10_000
|
||||
|
||||
_any ->
|
||||
Sentry.capture_message("Unknown monthly pageview limit for plan",
|
||||
extra: %{paddle_plan_id: subscription.paddle_plan_id}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_enterprise_plan(nil), do: nil
|
||||
|
||||
defp get_enterprise_plan(%Plausible.Billing.Subscription{} = subscription) do
|
||||
|
89
lib/plausible/billing/quota.ex
Normal file
89
lib/plausible/billing/quota.ex
Normal file
@ -0,0 +1,89 @@
|
||||
defmodule Plausible.Billing.Quota do
|
||||
@moduledoc """
|
||||
This module provides functions to work with plans usage and limits.
|
||||
"""
|
||||
|
||||
alias Plausible.Billing.Plans
|
||||
|
||||
@limit_sites_since ~D[2021-05-05]
|
||||
@spec site_limit(Plausible.Auth.User.t()) :: non_neg_integer() | :unlimited
|
||||
@doc """
|
||||
Returns the limit of sites a user can have.
|
||||
|
||||
For enterprise customers, returns :unlimited. The site limit is checked in a
|
||||
background job so as to avoid service disruption.
|
||||
"""
|
||||
def site_limit(user) do
|
||||
cond do
|
||||
Application.get_env(:plausible, :is_selfhost) -> :unlimited
|
||||
Timex.before?(user.inserted_at, @limit_sites_since) -> :unlimited
|
||||
true -> get_site_limit_from_plan(user)
|
||||
end
|
||||
end
|
||||
|
||||
@site_limit_for_trials 50
|
||||
@site_limit_for_free_10k 50
|
||||
defp get_site_limit_from_plan(user) do
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
|
||||
case Plans.get_subscription_plan(user.subscription) do
|
||||
%Plausible.Billing.EnterprisePlan{} -> :unlimited
|
||||
%Plausible.Billing.Plan{site_limit: site_limit} -> site_limit
|
||||
:free_10k -> @site_limit_for_free_10k
|
||||
nil -> @site_limit_for_trials
|
||||
end
|
||||
end
|
||||
|
||||
@spec site_usage(Plausible.Auth.User.t()) :: non_neg_integer()
|
||||
@doc """
|
||||
Returns the number of sites the given user owns.
|
||||
"""
|
||||
def site_usage(user) do
|
||||
Plausible.Sites.owned_sites_count(user)
|
||||
end
|
||||
|
||||
@monthly_pageview_limit_for_free_10k 10_000
|
||||
|
||||
@spec monthly_pageview_limit(Plausible.Billing.Subscription.t()) ::
|
||||
non_neg_integer() | :unlimited
|
||||
@doc """
|
||||
Returns the limit of pageviews for a subscription.
|
||||
"""
|
||||
def monthly_pageview_limit(subscription) do
|
||||
case Plans.get_subscription_plan(subscription) do
|
||||
%Plausible.Billing.EnterprisePlan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
%Plausible.Billing.Plan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
:free_10k ->
|
||||
@monthly_pageview_limit_for_free_10k
|
||||
|
||||
_any ->
|
||||
Sentry.capture_message("Unknown monthly pageview limit for plan",
|
||||
extra: %{paddle_plan_id: subscription && subscription.paddle_plan_id}
|
||||
)
|
||||
|
||||
:unlimited
|
||||
end
|
||||
end
|
||||
|
||||
@spec monthly_pageview_usage(Plausible.Auth.User.t()) :: non_neg_integer()
|
||||
@doc """
|
||||
Returns the amount of pageviews sent by the sites the user owns in last 30 days.
|
||||
"""
|
||||
def monthly_pageview_usage(user) do
|
||||
user
|
||||
|> Plausible.Billing.usage_breakdown()
|
||||
|> Tuple.sum()
|
||||
end
|
||||
|
||||
@spec within_limit?(non_neg_integer(), non_neg_integer() | :unlimited) :: boolean()
|
||||
@doc """
|
||||
Returns whether the limit has been exceeded or not.
|
||||
"""
|
||||
def within_limit?(usage, limit) do
|
||||
if limit == :unlimited, do: true, else: usage < limit
|
||||
end
|
||||
end
|
@ -13,11 +13,12 @@ defmodule Plausible.Sites do
|
||||
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.run(:limit, fn _, _ ->
|
||||
case {Plausible.Billing.Plans.site_limit(user), owned_sites_count(user)} do
|
||||
{:unlimited, actual} -> {:ok, actual}
|
||||
{limit, actual} when actual >= limit -> {:error, limit}
|
||||
{_limit, actual} -> {:ok, actual}
|
||||
end
|
||||
limit = Plausible.Billing.Quota.site_limit(user)
|
||||
usage = Plausible.Billing.Quota.site_usage(user)
|
||||
|
||||
if Plausible.Billing.Quota.within_limit?(usage, limit),
|
||||
do: {:ok, usage},
|
||||
else: {:error, limit}
|
||||
end)
|
||||
|> Ecto.Multi.insert(:site, site_changeset)
|
||||
|> Ecto.Multi.run(:site_membership, fn repo, %{site: site} ->
|
||||
@ -111,8 +112,9 @@ defmodule Plausible.Sites do
|
||||
|
||||
def has_goals?(site) do
|
||||
Repo.exists?(
|
||||
from g in Plausible.Goal,
|
||||
from(g in Plausible.Goal,
|
||||
where: g.site_id == ^site.id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@ -130,9 +132,10 @@ defmodule Plausible.Sites do
|
||||
|
||||
def role(user_id, site) do
|
||||
Repo.one(
|
||||
from sm in Site.Membership,
|
||||
from(sm in Site.Membership,
|
||||
where: sm.user_id == ^user_id and sm.site_id == ^site.id,
|
||||
select: sm.role
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@ -167,11 +170,12 @@ defmodule Plausible.Sites do
|
||||
|
||||
def owner_for(site) do
|
||||
Repo.one(
|
||||
from u in Plausible.Auth.User,
|
||||
from(u in Plausible.Auth.User,
|
||||
join: sm in Site.Membership,
|
||||
on: sm.user_id == u.id,
|
||||
where: sm.site_id == ^site.id,
|
||||
where: sm.role == :owner
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
36
lib/plausible_web/components/billing.ex
Normal file
36
lib/plausible_web/components/billing.ex
Normal file
@ -0,0 +1,36 @@
|
||||
defmodule PlausibleWeb.Components.Billing do
|
||||
@moduledoc false
|
||||
|
||||
use Phoenix.Component
|
||||
|
||||
slot(:inner_block, required: true)
|
||||
attr(:rest, :global)
|
||||
|
||||
def usage_and_limits_table(assigns) do
|
||||
~H"""
|
||||
<table class="min-w-full text-gray-900 dark:text-gray-100" {@rest}>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<%= render_slot(@inner_block) %>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:title, :string, required: true)
|
||||
attr(:usage, :any, required: true)
|
||||
attr(:limit, :integer, default: nil)
|
||||
attr(:pad, :boolean, default: false)
|
||||
attr(:rest, :global)
|
||||
|
||||
def usage_and_limits_row(assigns) do
|
||||
~H"""
|
||||
<tr {@rest}>
|
||||
<td class={["py-4 text-sm whitespace-nowrap text-left", @pad && "pl-6"]}><%= @title %></td>
|
||||
<td class="py-4 text-sm whitespace-nowrap text-right">
|
||||
<%= Cldr.Number.to_string!(@usage) %>
|
||||
<%= if is_number(@limit), do: "/ #{Cldr.Number.to_string!(@limit)}" %>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
end
|
||||
end
|
@ -489,8 +489,8 @@ defmodule PlausibleWeb.AuthController do
|
||||
end
|
||||
|
||||
defp render_settings(conn, changeset) do
|
||||
user = conn.assigns[:current_user]
|
||||
{usage_pageviews, usage_custom_events} = Plausible.Billing.usage_breakdown(user)
|
||||
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),
|
||||
@ -498,8 +498,12 @@ defmodule PlausibleWeb.AuthController do
|
||||
subscription: user.subscription,
|
||||
invoices: Plausible.Billing.paddle_api().get_invoices(user.subscription),
|
||||
theme: user.theme || "system",
|
||||
usage_pageviews: usage_pageviews,
|
||||
usage_custom_events: usage_custom_events
|
||||
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
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -28,7 +28,7 @@ defmodule PlausibleWeb.BillingController do
|
||||
true ->
|
||||
render(conn, "upgrade.html",
|
||||
skip_plausible_tracking: true,
|
||||
usage: Plausible.Billing.usage(user),
|
||||
usage: Plausible.Billing.Quota.monthly_pageview_usage(user),
|
||||
user: user,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
|
@ -51,27 +51,24 @@ defmodule PlausibleWeb.SiteController do
|
||||
|
||||
def new(conn, _params) do
|
||||
current_user = conn.assigns[:current_user]
|
||||
owned_site_count = Plausible.Sites.owned_sites_count(current_user)
|
||||
|
||||
{site_limit, is_at_limit} =
|
||||
case Plausible.Billing.Plans.site_limit(current_user) do
|
||||
:unlimited -> {:unlimited, false}
|
||||
limit when is_integer(limit) -> {limit, owned_site_count >= limit}
|
||||
end
|
||||
limit = Plausible.Billing.Quota.site_limit(current_user)
|
||||
usage = Plausible.Billing.Quota.site_usage(current_user)
|
||||
within_limit? = Plausible.Billing.Quota.within_limit?(usage, limit)
|
||||
|
||||
render(conn, "new.html",
|
||||
changeset: Plausible.Site.changeset(%Plausible.Site{}),
|
||||
is_first_site: owned_site_count == 0,
|
||||
is_at_limit: is_at_limit,
|
||||
site_limit: site_limit,
|
||||
is_first_site: usage == 0,
|
||||
is_at_limit: !within_limit?,
|
||||
site_limit: limit,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
|
||||
def create_site(conn, %{"site" => site_params}) do
|
||||
user = conn.assigns[:current_user]
|
||||
site_count = Plausible.Sites.owned_sites_count(user)
|
||||
is_first_site = site_count == 0
|
||||
usage = Plausible.Billing.Quota.site_usage(user)
|
||||
is_first_site = usage == 0
|
||||
|
||||
case Sites.create(user, site_params) do
|
||||
{:ok, %{site: site}} ->
|
||||
|
@ -1,281 +0,0 @@
|
||||
<%= if !Application.get_env(:plausible, :is_selfhost) do %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-24 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-orange-200 ">
|
||||
<div class="flex justify-between">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Subscription Plan</h2>
|
||||
<%= if @subscription do %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-bold leading-5 <%= subscription_colors(@subscription.status) %>">
|
||||
<%= present_subscription_status(@subscription.status) %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="my-4 border-b border-gray-400"></div>
|
||||
|
||||
<%= if @subscription && @subscription.status == "deleted" do %>
|
||||
<div class="p-2 bg-red-100 rounded-lg sm:p-3">
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<div class="flex items-center flex-1 w-0">
|
||||
<svg class="w-6 h-6 text-red-800" viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<p class="ml-3 font-medium text-red-800">
|
||||
<%= if @subscription.next_bill_date && Timex.compare(@subscription.next_bill_date, Timex.today()) >= 0 do %>
|
||||
Your subscription is cancelled but you have access to your stats until <%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>. Upgrade below to make sure you don't lose access.
|
||||
<% else %>
|
||||
Your subscription is cancelled. Upgrade below to get access to your stats again.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
|
||||
<div class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900" style="width: 11.75rem;">
|
||||
<h4 class="font-black dark:text-gray-100">Monthly quota</h4>
|
||||
<%= if @subscription do %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100"><%= subscription_quota(@subscription) %> pageviews</div>
|
||||
<%= case @subscription.status do %>
|
||||
<% "active" -> %>
|
||||
<%= link("Change plan", to: "/billing/change-plan", class: "text-sm text-indigo-500 font-medium") %>
|
||||
<% "past_due" -> %>
|
||||
<span class="text-sm font-medium text-gray-600 dark:text-gray-400" tooltip="Please update your billing details before changing plans">Change plan</span>
|
||||
<% _ -> %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">Free trial</div>
|
||||
<%= link("Upgrade", to: "/billing/upgrade", class: "text-sm text-indigo-500 font-medium") %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900" style="width: 11.75rem;">
|
||||
<h4 class="font-black dark:text-gray-100">Next bill amount</h4>
|
||||
<%= if @subscription && @subscription.status in ["active", "past_due"] do %>
|
||||
<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 %>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900" style="width: 11.75rem;">
|
||||
<h4 class="font-black dark:text-gray-100">Next bill date</h4>
|
||||
|
||||
<%= if @subscription && @subscription.next_bill_date && @subscription.status in ["active", "past_due"] do %>
|
||||
<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">(<%= subscription_interval(@subscription) %> billing)</div>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-8 text-xl font-bold dark:text-gray-100">Your usage</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 leading-5 dark:text-gray-200">Last 30 days total usage across all of your sites</p>
|
||||
<div class="py-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="-my-2 sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<div>
|
||||
<table class="min-w-full text-gray-900 divide-y divide-gray-200 dark:text-gray-100">
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td class="py-4 text-sm whitespace-nowrap">
|
||||
Pageviews
|
||||
</td>
|
||||
<td class="py-4 text-sm whitespace-nowrap">
|
||||
<%= delimit_integer(@usage_pageviews) %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-4 text-sm whitespace-nowrap">
|
||||
Custom events
|
||||
</td>
|
||||
<td class="py-4 text-sm whitespace-nowrap">
|
||||
<%= delimit_integer(@usage_custom_events) %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="py-4 text-sm font-medium whitespace-nowrap">
|
||||
Total billable pageviews
|
||||
</td>
|
||||
<td class="py-4 text-sm font-medium whitespace-nowrap">
|
||||
<%= delimit_integer(@usage_pageviews + @usage_custom_events) %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= cond do %>
|
||||
<% @subscription && @subscription.status in ["active", "past_due", "paused"] && @subscription.cancel_url -> %>
|
||||
<div class="mt-8">
|
||||
<%= link("Cancel my subscription", to: @subscription.cancel_url, class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150") %>
|
||||
</div>
|
||||
<% true -> %>
|
||||
<div class="mt-8">
|
||||
<%= link("Upgrade", to: "/billing/upgrade", class: "inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= case @invoices do %>
|
||||
<% {:error, :no_invoices} -> %>
|
||||
|
||||
<% {:error, :request_failed} -> %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Invoices</h2>
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
<p class="text-center text-black dark:text-gray-100 m-2">
|
||||
Something went wrong
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<% {:ok, invoice_list} when is_list(invoice_list) -> %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Invoices</h2>
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Amount
|
||||
</th>
|
||||
<th scope="col" class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Invoice
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<%= for invoice <- format_invoices(invoice_list) do %>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td class="py-4 text-sm text-gray-800 dark:text-gray-200 font-medium">
|
||||
<%= invoice.date %>
|
||||
</td>
|
||||
<td class="py-4 text-sm text-gray-800 dark:text-gray-200">
|
||||
<%= invoice.currency <> invoice.amount %>
|
||||
</td>
|
||||
<td class="py-4 text-sm text-indigo-500">
|
||||
<%= link("Link", to: invoice.url, target: "_blank" ) %>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-green-500 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Dashboard Appearance</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %>
|
||||
<div class="col-span-4 sm:col-span-2">
|
||||
<%= label f, :theme, "Theme Selection", class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300" %>
|
||||
<%= select f, :theme, Plausible.Themes.options(), class: "dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer" %>
|
||||
</div>
|
||||
<%= submit "Save", class: "button mt-4" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-indigo-100 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-indigo-500">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Account settings</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %>
|
||||
<div class="my-4">
|
||||
<%= label f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<div class="mt-1">
|
||||
<%= text_input f, :name, class: "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800" %>
|
||||
<%= error_tag f, :name %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<%= label f, :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<div class="mt-1">
|
||||
<%= email_input f, :email, class: "shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md" %>
|
||||
<%= error_tag f, :email %>
|
||||
</div>
|
||||
</div>
|
||||
<%= submit "Save changes", class: "button mt-4" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="api-keys" class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-indigo-100 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-indigo-500">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">API keys</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<div class="flex flex-col mt-6">
|
||||
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
|
||||
<%= if Enum.any?(@user.api_keys) do %>
|
||||
<div class="overflow-hidden border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-100">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-100">
|
||||
Key
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Revoke</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for api_key <- @user.api_keys do %>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-nowrap">
|
||||
<%= api_key.name %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-100 whitespace-nowrap">
|
||||
<%= api_key.key_prefix %><%= String.duplicate("*", 32 - 6) %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
|
||||
<%= button("Revoke", to: "/settings/api-keys/#{api_key.id}", class: "text-red-600 hover:text-red-900", method: :delete, "data-confirm": "Are you sure you want to revoke this key? This action cannot be reversed.") %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link "+ New API key", to: "/settings/api-keys/new", class: "button mt-4" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 mb-24 bg-white border-t-2 border-red-600 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Delete account</h2>
|
||||
</div>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<p class="dark:text-gray-100">Deleting your account removes all sites and stats you've collected</p>
|
||||
|
||||
<%= if @subscription && @subscription.status == "active" do %>
|
||||
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">Delete my account</span>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.</p>
|
||||
<% else %>
|
||||
<%= link("Delete my account", to: "/me", class: "inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150", method: "delete", data: [confirm: "Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?"]) %>
|
||||
<% end %>
|
||||
</div>
|
377
lib/plausible_web/templates/auth/user_settings.html.heex
Normal file
377
lib/plausible_web/templates/auth/user_settings.html.heex
Normal file
@ -0,0 +1,377 @@
|
||||
<%= if !Application.get_env(:plausible, :is_selfhost) do %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-24 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-orange-200 ">
|
||||
<div class="flex justify-between">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Subscription Plan</h2>
|
||||
<span
|
||||
:if={@subscription}
|
||||
class={[
|
||||
"inline-flex items-center px-2.5 py-0.5 rounded-md text-sm font-bold leading-5",
|
||||
subscription_colors(@subscription.status)
|
||||
]}
|
||||
>
|
||||
<%= present_subscription_status(@subscription.status) %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="my-4 border-b border-gray-400"></div>
|
||||
|
||||
<div
|
||||
:if={@subscription && @subscription.status == "deleted"}
|
||||
class="p-2 bg-red-100 rounded-lg sm:p-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between">
|
||||
<div class="flex items-center flex-1 w-0">
|
||||
<svg
|
||||
class="w-6 h-6 text-red-800"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<p class="ml-3 font-medium text-red-800">
|
||||
<%= if @subscription.next_bill_date && Timex.compare(@subscription.next_bill_date, Timex.today()) >= 0 do %>
|
||||
Your subscription is cancelled but you have access to your stats until <%= Timex.format!(
|
||||
@subscription.next_bill_date,
|
||||
"{Mshort} {D}, {YYYY}"
|
||||
) %>. Upgrade below to make sure you don't lose access.
|
||||
<% else %>
|
||||
Your subscription is cancelled. Upgrade below to get access to your stats again.
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
|
||||
<div
|
||||
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
||||
style="width: 11.75rem;"
|
||||
>
|
||||
<h4 class="font-black dark:text-gray-100">Monthly quota</h4>
|
||||
<%= if @subscription do %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">
|
||||
<%= subscription_quota(@subscription) %> pageviews
|
||||
</div>
|
||||
|
||||
<.link
|
||||
:if={@subscription.status == "active"}
|
||||
href={Routes.billing_path(@conn, :change_plan_form)}
|
||||
class="text-sm text-indigo-500 font-medium"
|
||||
>
|
||||
Change plan
|
||||
</.link>
|
||||
|
||||
<span
|
||||
:if={@subscription.status == "past_due"}
|
||||
class="text-sm text-gray-600 dark:text-gray-400 font-medium"
|
||||
tooltip="Please update your billing details before changing plans"
|
||||
>
|
||||
Change plan
|
||||
</span>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">Free trial</div>
|
||||
<.link
|
||||
href={Routes.billing_path(@conn, :upgrade)}
|
||||
class="text-sm text-indigo-500 font-medium"
|
||||
>
|
||||
Upgrade
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
<div
|
||||
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
||||
style="width: 11.75rem;"
|
||||
>
|
||||
<h4 class="font-black dark:text-gray-100">Next bill amount</h4>
|
||||
<%= if @subscription && @subscription.status in ["active", "past_due"] do %>
|
||||
<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 %>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div
|
||||
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
||||
style="width: 11.75rem;"
|
||||
>
|
||||
<h4 class="font-black dark:text-gray-100">Next bill date</h4>
|
||||
|
||||
<%= if @subscription && @subscription.next_bill_date && @subscription.status in ["active", "past_due"] do %>
|
||||
<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">
|
||||
(<%= subscription_interval(@subscription) %> billing)
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="py-2 text-xl font-medium dark:text-gray-100">---</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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
|
||||
title="Owned sites"
|
||||
usage={@site_usage}
|
||||
limit={@site_limit}
|
||||
/>
|
||||
</PlausibleWeb.Components.Billing.usage_and_limits_table>
|
||||
</article>
|
||||
|
||||
<%= cond do %>
|
||||
<% @subscription && @subscription.status in ["active", "past_due", "paused"] && @subscription.cancel_url -> %>
|
||||
<div class="mt-8">
|
||||
<%= link("Cancel my subscription",
|
||||
to: @subscription.cancel_url,
|
||||
class:
|
||||
"inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150"
|
||||
) %>
|
||||
</div>
|
||||
<% true -> %>
|
||||
<div class="mt-8">
|
||||
<%= link("Upgrade",
|
||||
to: "/billing/upgrade",
|
||||
class:
|
||||
"inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
|
||||
) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= case @invoices do %>
|
||||
<% {:error, :no_invoices} -> %>
|
||||
<% {:error, :request_failed} -> %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Invoices</h2>
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
<p class="text-center text-black dark:text-gray-100 m-2">
|
||||
Something went wrong
|
||||
</p>
|
||||
</div>
|
||||
<% {:ok, invoice_list} when is_list(invoice_list) -> %>
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-orange-200 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Invoices</h2>
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Date
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Amount
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
|
||||
>
|
||||
Invoice
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<%= for invoice <- format_invoices(invoice_list) do %>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tr>
|
||||
<td class="py-4 text-sm text-gray-800 dark:text-gray-200 font-medium">
|
||||
<%= invoice.date %>
|
||||
</td>
|
||||
<td class="py-4 text-sm text-gray-800 dark:text-gray-200">
|
||||
<%= invoice.currency <> invoice.amount %>
|
||||
</td>
|
||||
<td class="py-4 text-sm text-indigo-500">
|
||||
<%= link("Link", to: invoice.url, target: "_blank") %>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-green-500 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Dashboard Appearance</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %>
|
||||
<div class="col-span-4 sm:col-span-2">
|
||||
<%= label(f, :theme, "Theme Selection",
|
||||
class: "block text-sm font-medium leading-5 text-gray-700 dark:text-gray-300"
|
||||
) %>
|
||||
<%= select(f, :theme, Plausible.Themes.options(),
|
||||
class:
|
||||
"dark:bg-gray-900 mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-500 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:text-gray-100 cursor-pointer"
|
||||
) %>
|
||||
</div>
|
||||
<%= submit("Save", class: "button mt-4") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-indigo-100 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-indigo-500">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Account settings</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<%= form_for @changeset, "/settings", [class: "max-w-sm"], fn f -> %>
|
||||
<div class="my-4">
|
||||
<%= label(f, :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %>
|
||||
<div class="mt-1">
|
||||
<%= text_input(f, :name,
|
||||
class:
|
||||
"shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md dark:bg-gray-800"
|
||||
) %>
|
||||
<%= error_tag(f, :name) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-4">
|
||||
<%= label(f, :email, class: "block text-sm font-medium text-gray-700 dark:text-gray-300") %>
|
||||
<div class="mt-1">
|
||||
<%= email_input(f, :email,
|
||||
class:
|
||||
"shadow-sm dark:bg-gray-900 dark:text-gray-300 focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 dark:border-gray-500 rounded-md"
|
||||
) %>
|
||||
<%= error_tag(f, :email) %>
|
||||
</div>
|
||||
</div>
|
||||
<%= submit("Save changes", class: "button mt-4") %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="api-keys"
|
||||
class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 bg-white border-t-2 border-indigo-100 rounded rounded-t-none shadow-md dark:bg-gray-800 dark:border-indigo-500"
|
||||
>
|
||||
<h2 class="text-xl font-black dark:text-gray-100">API keys</h2>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<div class="flex flex-col mt-6">
|
||||
<div class="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<%= if Enum.any?(@user.api_keys) do %>
|
||||
<div class="overflow-hidden border-b border-gray-200 shadow dark:border-gray-900 sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-900">
|
||||
<thead class="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
class="px-6 py-3 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-gray-100"
|
||||
>
|
||||
Key
|
||||
</th>
|
||||
<th scope="col" class="relative px-6 py-3">
|
||||
<span class="sr-only">Revoke</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for api_key <- @user.api_keys do %>
|
||||
<tr class="bg-white dark:bg-gray-800">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-nowrap">
|
||||
<%= api_key.name %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-100 whitespace-nowrap">
|
||||
<%= api_key.key_prefix %><%= String.duplicate("*", 32 - 6) %>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm font-medium text-right whitespace-nowrap">
|
||||
<%= button("Revoke",
|
||||
to: "/settings/api-keys/#{api_key.id}",
|
||||
class: "text-red-600 hover:text-red-900",
|
||||
method: :delete,
|
||||
"data-confirm":
|
||||
"Are you sure you want to revoke this key? This action cannot be reversed."
|
||||
) %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= link("+ New API key", to: "/settings/api-keys/new", class: "button mt-4") %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-2xl px-8 pt-6 pb-8 mx-auto mt-16 mb-24 bg-white border-t-2 border-red-600 rounded rounded-t-none shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-black dark:text-gray-100">Delete account</h2>
|
||||
</div>
|
||||
|
||||
<div class="my-4 border-b border-gray-300 dark:border-gray-500"></div>
|
||||
|
||||
<p class="dark:text-gray-100">
|
||||
Deleting your account removes all sites and stats you've collected
|
||||
</p>
|
||||
|
||||
<%= if @subscription && @subscription.status == "active" do %>
|
||||
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">
|
||||
Delete my account
|
||||
</span>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Your account cannot be deleted because you have an active subscription. If you want to delete your account, please cancel your subscription first.
|
||||
</p>
|
||||
<% else %>
|
||||
<%= link("Delete my account",
|
||||
to: "/me",
|
||||
class:
|
||||
"inline-block mt-4 px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-red-700 bg-white dark:bg-gray-800 hover:text-red-500 dark:hover:text-red-400 focus:outline-none focus:border-blue-300 focus:ring active:text-red-800 active:bg-gray-50 transition ease-in-out duration-150",
|
||||
method: "delete",
|
||||
data: [
|
||||
confirm:
|
||||
"Deleting your account will also delete all the sites and data that you own. This action cannot be reversed. Are you sure?"
|
||||
]
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
@ -12,7 +12,7 @@ defmodule PlausibleWeb.AuthView do
|
||||
|
||||
def subscription_quota(subscription) do
|
||||
subscription
|
||||
|> Plans.monthly_pageview_limit()
|
||||
|> Plausible.Billing.Quota.monthly_pageview_limit()
|
||||
|> PlausibleWeb.StatsView.large_number_format()
|
||||
end
|
||||
|
||||
|
@ -35,7 +35,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
|
||||
active_subscribers =
|
||||
Repo.all(
|
||||
from u in Plausible.Auth.User,
|
||||
from(u in Plausible.Auth.User,
|
||||
join: s in Plausible.Billing.Subscription,
|
||||
on: s.user_id == u.id,
|
||||
left_join: ep in Plausible.Billing.EnterprisePlan,
|
||||
@ -48,6 +48,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) ==
|
||||
day_of_month(^yesterday),
|
||||
preload: [subscription: s, enterprise_plan: ep]
|
||||
)
|
||||
)
|
||||
|
||||
for subscriber <- active_subscribers do
|
||||
@ -63,7 +64,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
|
||||
def check_enterprise_subscriber(subscriber, billing_mod) do
|
||||
pageview_limit = check_pageview_limit(subscriber, billing_mod)
|
||||
site_limit = check_site_limit(subscriber)
|
||||
site_limit = check_site_limit_for_enterprise(subscriber)
|
||||
|
||||
case {pageview_limit, site_limit} do
|
||||
{{:within_limit, _}, {:within_limit, _}} ->
|
||||
@ -112,25 +113,21 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
end
|
||||
|
||||
defp check_pageview_limit(subscriber, billing_mod) do
|
||||
monthly_pageview_limit =
|
||||
case Plausible.Billing.Plans.monthly_pageview_limit(subscriber.subscription) do
|
||||
monthly_pageview_limit when is_number(monthly_pageview_limit) ->
|
||||
monthly_pageview_limit * 1.1
|
||||
|
||||
nil ->
|
||||
Sentry.capture_message("Unable to calculate monthly pageview limit",
|
||||
user: subscriber,
|
||||
subscription: subscriber.subscription
|
||||
)
|
||||
end
|
||||
limit =
|
||||
subscriber.subscription
|
||||
|> Plausible.Billing.Quota.monthly_pageview_limit()
|
||||
|> Kernel.*(1.1)
|
||||
|> ceil()
|
||||
|
||||
{_, 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? = last_cycle_usage >= monthly_pageview_limit
|
||||
exceeded_last_last_cycle? = last_last_cycle_usage >= monthly_pageview_limit
|
||||
exceeded_last_cycle? = not Plausible.Billing.Quota.within_limit?(last_cycle_usage, limit)
|
||||
|
||||
exceeded_last_last_cycle? =
|
||||
not Plausible.Billing.Quota.within_limit?(last_last_cycle_usage, limit)
|
||||
|
||||
if exceeded_last_last_cycle? && exceeded_last_cycle? do
|
||||
{:over_limit, {last_cycle, last_cycle_usage}}
|
||||
@ -139,14 +136,14 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
end
|
||||
end
|
||||
|
||||
defp check_site_limit(subscriber) do
|
||||
site_limit = subscriber.enterprise_plan.site_limit
|
||||
total_sites = Plausible.Sites.owned_sites_count(subscriber)
|
||||
defp check_site_limit_for_enterprise(subscriber) do
|
||||
limit = subscriber.enterprise_plan.site_limit
|
||||
usage = Plausible.Billing.Quota.site_usage(subscriber)
|
||||
|
||||
if total_sites >= site_limit do
|
||||
{:over_limit, {total_sites, site_limit}}
|
||||
if Plausible.Billing.Quota.within_limit?(usage, limit) do
|
||||
{:within_limit, {usage, limit}}
|
||||
else
|
||||
{:within_limit, {total_sites, site_limit}}
|
||||
{:over_limit, {usage, limit}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,52 +3,6 @@ defmodule Plausible.BillingTest do
|
||||
use Bamboo.Test, shared: true
|
||||
alias Plausible.Billing
|
||||
|
||||
describe "usage" do
|
||||
test "is 0 with no events" do
|
||||
user = insert(:user)
|
||||
|
||||
assert Billing.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 Billing.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 Billing.usage(user) == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "last_two_billing_cycles" do
|
||||
test "billing on the 1st" do
|
||||
last_bill_date = ~D[2021-01-01]
|
||||
|
@ -32,32 +32,6 @@ defmodule Plausible.Billing.PlansTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "monthly_pageview_limit" do
|
||||
test "is based on the plan if user is on a standard plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
|
||||
assert Plans.monthly_pageview_limit(user.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "free_10k has 10k monthly_pageview_limit" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
|
||||
assert Plans.monthly_pageview_limit(user.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "is based on the enterprise plan if user is on an enterprise plan" do
|
||||
user = insert(:user)
|
||||
|
||||
enterprise_plan =
|
||||
insert(:enterprise_plan, user_id: user.id, monthly_pageview_limit: 100_000)
|
||||
|
||||
subscription =
|
||||
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
|
||||
|
||||
assert Plans.monthly_pageview_limit(subscription) == 100_000
|
||||
end
|
||||
end
|
||||
|
||||
describe "subscription_interval" do
|
||||
test "is based on the plan if user is on a standard plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
@ -153,86 +127,4 @@ defmodule Plausible.Billing.PlansTest do
|
||||
] == Plans.yearly_product_ids()
|
||||
end
|
||||
end
|
||||
|
||||
describe "site_limit/1" do
|
||||
test "returns 50 when user is on an old plan" do
|
||||
user_on_v1 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|
||||
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
|
||||
|
||||
assert 50 == Plans.site_limit(user_on_v1)
|
||||
assert 50 == Plans.site_limit(user_on_v2)
|
||||
assert 50 == Plans.site_limit(user_on_v3)
|
||||
end
|
||||
|
||||
test "returns 50 when user is on free_10k plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
assert 50 == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns unlimited when user is on an enterprise plan" do
|
||||
user = insert(:user)
|
||||
|
||||
enterprise_plan =
|
||||
insert(:enterprise_plan,
|
||||
user_id: user.id,
|
||||
monthly_pageview_limit: 100_000,
|
||||
site_limit: 500
|
||||
)
|
||||
|
||||
_subscription =
|
||||
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
|
||||
|
||||
assert :unlimited == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns 50 when user in on trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 7))
|
||||
assert 50 == Plans.site_limit(user)
|
||||
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: -7))
|
||||
assert 50 == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns the subscription limit for enterprise users who have not paid yet" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
|
||||
)
|
||||
|
||||
assert 50 == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns 50 for enterprise users who have not upgraded yet and are on trial" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: nil
|
||||
)
|
||||
|
||||
assert 50 == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "is unlimited for enterprise customers" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: build(:subscription, paddle_plan_id: "123321")
|
||||
)
|
||||
|
||||
assert :unlimited == Plans.site_limit(user)
|
||||
end
|
||||
|
||||
test "is unlimited for enterprise customers who are due to change a plan" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "old-paddle-plan-id"),
|
||||
subscription: build(:subscription, paddle_plan_id: "old-paddle-plan-id")
|
||||
)
|
||||
|
||||
insert(:enterprise_plan, user_id: user.id, paddle_plan_id: "new-paddle-plan-id")
|
||||
assert :unlimited == Plans.site_limit(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
196
test/plausible/billing/quota_test.exs
Normal file
196
test/plausible/billing/quota_test.exs
Normal file
@ -0,0 +1,196 @@
|
||||
defmodule Plausible.Billing.QuotaTest do
|
||||
use Plausible.DataCase, async: true
|
||||
alias Plausible.Billing.Quota
|
||||
|
||||
@v1_plan_id "558018"
|
||||
@v2_plan_id "654177"
|
||||
@v3_plan_id "749342"
|
||||
|
||||
describe "site_limit/1" do
|
||||
test "returns 50 when user is on an old plan" do
|
||||
user_on_v1 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|
||||
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
|
||||
|
||||
assert 50 == Quota.site_limit(user_on_v1)
|
||||
assert 50 == Quota.site_limit(user_on_v2)
|
||||
assert 50 == Quota.site_limit(user_on_v3)
|
||||
end
|
||||
|
||||
test "returns 50 when user is on free_10k plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
assert 50 == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns unlimited when user is on an enterprise plan" do
|
||||
user = insert(:user)
|
||||
|
||||
enterprise_plan =
|
||||
insert(:enterprise_plan,
|
||||
user_id: user.id,
|
||||
monthly_pageview_limit: 100_000,
|
||||
site_limit: 500
|
||||
)
|
||||
|
||||
_subscription =
|
||||
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
|
||||
|
||||
assert :unlimited == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns 50 when user in on trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 7))
|
||||
assert 50 == Quota.site_limit(user)
|
||||
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: -7))
|
||||
assert 50 == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns the subscription limit for enterprise users who have not paid yet" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
|
||||
)
|
||||
|
||||
assert 50 == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "returns 50 for enterprise users who have not upgraded yet and are on trial" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: nil
|
||||
)
|
||||
|
||||
assert 50 == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "is unlimited for enterprise customers" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
|
||||
subscription: build(:subscription, paddle_plan_id: "123321")
|
||||
)
|
||||
|
||||
assert :unlimited == Quota.site_limit(user)
|
||||
end
|
||||
|
||||
test "is unlimited for enterprise customers who are due to change a plan" do
|
||||
user =
|
||||
insert(:user,
|
||||
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "old-paddle-plan-id"),
|
||||
subscription: build(:subscription, paddle_plan_id: "old-paddle-plan-id")
|
||||
)
|
||||
|
||||
insert(:enterprise_plan, user_id: user.id, paddle_plan_id: "new-paddle-plan-id")
|
||||
assert :unlimited == Quota.site_limit(user)
|
||||
end
|
||||
end
|
||||
|
||||
test "site_usage/1 returns the amount of sites the user owns" do
|
||||
user = insert(:user)
|
||||
insert_list(3, :site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
insert(:site, memberships: [build(:site_membership, user: user, role: :admin)])
|
||||
insert(:site, memberships: [build(:site_membership, user: user, role: :viewer)])
|
||||
|
||||
assert Quota.site_usage(user) == 3
|
||||
end
|
||||
|
||||
describe "within_limit?/2" do
|
||||
test "returns true when quota is not exceeded" do
|
||||
assert Quota.within_limit?(3, 5)
|
||||
end
|
||||
|
||||
test "returns true when limit is :unlimited" do
|
||||
assert Quota.within_limit?(10_000, :unlimited)
|
||||
end
|
||||
|
||||
test "returns false when usage is at limit" do
|
||||
refute Quota.within_limit?(3, 3)
|
||||
end
|
||||
|
||||
test "returns false when usage exceeds the limit" do
|
||||
refute Quota.within_limit?(10, 3)
|
||||
end
|
||||
end
|
||||
|
||||
describe "monthly_pageview_limit/1" do
|
||||
test "is based on the plan if user is on a standard plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
|
||||
assert Quota.monthly_pageview_limit(user.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "free_10k has 10k monthly_pageview_limit" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
|
||||
assert Quota.monthly_pageview_limit(user.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "is based on the enterprise plan if user is on an enterprise plan" do
|
||||
user = insert(:user)
|
||||
|
||||
enterprise_plan =
|
||||
insert(:enterprise_plan, user_id: user.id, monthly_pageview_limit: 100_000)
|
||||
|
||||
subscription =
|
||||
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
|
||||
|
||||
assert Quota.monthly_pageview_limit(subscription) == 100_000
|
||||
end
|
||||
|
||||
test "does not limit pageviews when user has a pending enterprise plan" do
|
||||
user = insert(:user)
|
||||
subscription = insert(:subscription, user_id: user.id, paddle_plan_id: "pending-enterprise")
|
||||
|
||||
assert Quota.monthly_pageview_limit(subscription) == :unlimited
|
||||
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
|
||||
end
|
Loading…
Reference in New Issue
Block a user