mirror of
https://github.com/plausible/analytics.git
synced 2024-08-17 05:40:24 +03:00
Count pending ownership sites towards account usage on the upgrade page (#4277)
* add notice about pending ownerships counting towards usage * Refactor feature usage to take site_ids too * Add option to query usage from pending ownerships * count pending ownerships towards usage on the choose-plan page * turn 'eligible_for_upgrade?' into a predicate * fix variable names on ce * fix unused alias on ce * fix ce test
This commit is contained in:
parent
5c146ad30e
commit
07a54ef65c
@ -56,6 +56,8 @@ defmodule Plausible.Billing.Quota do
|
||||
|
||||
def ensure_within_plan_limits(_, _, _), do: :ok
|
||||
|
||||
def eligible_for_upgrade?(usage), do: usage.sites > 0
|
||||
|
||||
defp exceeded_limits(usage, plan, opts) do
|
||||
for {limit, exceeded?} <- [
|
||||
{:team_member_limit, not within_limit?(usage.team_members, plan.team_member_limit)},
|
||||
|
@ -6,8 +6,7 @@ defmodule Plausible.Billing.Quota.Usage do
|
||||
alias Plausible.Users
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Site
|
||||
alias Plausible.Billing.{Subscriptions}
|
||||
alias Plausible.Billing.Feature.{RevenueGoals, Funnels, Props, StatsAPI}
|
||||
alias Plausible.Billing.{Subscriptions, Feature}
|
||||
|
||||
@type cycles_usage() :: %{cycle() => usage_cycle()}
|
||||
|
||||
@ -22,16 +21,36 @@ defmodule Plausible.Billing.Quota.Usage do
|
||||
total: non_neg_integer()
|
||||
}
|
||||
|
||||
@doc """
|
||||
Returns a full usage report for the user.
|
||||
|
||||
### Options
|
||||
|
||||
* `pending_ownership_site_ids` - a list of site IDs from which to count
|
||||
additional usage. This allows us to look at the total usage from pending
|
||||
ownerships and owned sites at the same time, which is useful, for example,
|
||||
when deciding whether to let the user upgrade to a plan, or accept a site
|
||||
ownership.
|
||||
|
||||
* `with_features` - when `true`, the returned map will contain features
|
||||
usage. Also counts usage from `pending_ownership_site_ids` if that option
|
||||
is given.
|
||||
"""
|
||||
def usage(user, opts \\ []) do
|
||||
owned_site_ids = Plausible.Sites.owned_site_ids(user)
|
||||
pending_ownership_site_ids = Keyword.get(opts, :pending_ownership_site_ids, [])
|
||||
all_site_ids = Enum.uniq(owned_site_ids ++ pending_ownership_site_ids)
|
||||
|
||||
basic_usage = %{
|
||||
monthly_pageviews: monthly_pageview_usage(user),
|
||||
team_members: team_member_usage(user),
|
||||
sites: site_usage(user)
|
||||
monthly_pageviews: monthly_pageview_usage(user, all_site_ids),
|
||||
team_members:
|
||||
team_member_usage(user, pending_ownership_site_ids: pending_ownership_site_ids),
|
||||
sites: length(all_site_ids)
|
||||
}
|
||||
|
||||
if Keyword.get(opts, :with_features) == true do
|
||||
basic_usage
|
||||
|> Map.put(:features, features_usage(user))
|
||||
|> Map.put(:features, features_usage(user, all_site_ids))
|
||||
else
|
||||
basic_usage
|
||||
end
|
||||
@ -151,116 +170,120 @@ defmodule Plausible.Billing.Quota.Usage do
|
||||
}
|
||||
end
|
||||
|
||||
@spec team_member_usage(User.t()) :: integer()
|
||||
@spec team_member_usage(User.t(), Keyword.t()) :: non_neg_integer()
|
||||
@doc """
|
||||
Returns the total count of team members associated with the user's sites.
|
||||
|
||||
* The given user (i.e. the owner) is not counted as a team member.
|
||||
|
||||
* Pending invitations are counted as team members even before accepted.
|
||||
* Pending invitations (but not ownership transfers) are counted as team
|
||||
members even before accepted.
|
||||
|
||||
* Users are counted uniquely - i.e. even if an account is associated with
|
||||
many sites owned by the given user, they still count as one team member.
|
||||
|
||||
* Specific e-mails can be excluded from the count, so that where necessary,
|
||||
we can ensure inviting the same person(s) to more than 1 sites is allowed
|
||||
"""
|
||||
def team_member_usage(user, opts \\ []) do
|
||||
{:ok, opts} = Keyword.validate(opts, site: nil, exclude_emails: [])
|
||||
### Options
|
||||
|
||||
user
|
||||
|> team_member_usage_query(opts)
|
||||
* `exclude_emails` - a list of emails to not count towards the usage. This
|
||||
allows us to exclude a user from being counted as a team member when
|
||||
checking whether a site invitation can be created for that same user.
|
||||
|
||||
* `pending_ownership_site_ids` - a list of site IDs from which to count
|
||||
additional team member usage. Without this option, usage is queried only
|
||||
across sites owned by the given user.
|
||||
"""
|
||||
def team_member_usage(user, opts \\ [])
|
||||
|
||||
def team_member_usage(%User{} = user, opts) do
|
||||
exclude_emails = Keyword.get(opts, :exclude_emails, []) ++ [user.email]
|
||||
|
||||
q =
|
||||
user
|
||||
|> Plausible.Sites.owned_site_ids()
|
||||
|> query_team_member_emails()
|
||||
|
||||
q =
|
||||
case Keyword.get(opts, :pending_ownership_site_ids) do
|
||||
[_ | _] = site_ids -> union(q, ^query_team_member_emails(site_ids))
|
||||
_ -> q
|
||||
end
|
||||
|
||||
from(u in subquery(q),
|
||||
where: u.email not in ^exclude_emails,
|
||||
distinct: u.email
|
||||
)
|
||||
|> Plausible.Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
defp team_member_usage_query(user, opts) do
|
||||
owned_sites_query = owned_sites_query(user)
|
||||
|
||||
excluded_emails =
|
||||
opts
|
||||
|> Keyword.get(:exclude_emails, [])
|
||||
|> List.wrap()
|
||||
|
||||
site = opts[:site]
|
||||
|
||||
owned_sites_query =
|
||||
if site do
|
||||
where(owned_sites_query, [os], os.site_id == ^site.id)
|
||||
else
|
||||
owned_sites_query
|
||||
end
|
||||
|
||||
team_members_query =
|
||||
from os in subquery(owned_sites_query),
|
||||
inner_join: sm in Site.Membership,
|
||||
on: sm.site_id == os.site_id,
|
||||
def query_team_member_emails(site_ids) do
|
||||
memberships_q =
|
||||
from sm in Site.Membership,
|
||||
where: sm.site_id in ^site_ids,
|
||||
inner_join: u in assoc(sm, :user),
|
||||
where: sm.role != :owner,
|
||||
select: u.email
|
||||
select: %{email: u.email}
|
||||
|
||||
team_members_query =
|
||||
if excluded_emails != [] do
|
||||
team_members_query |> where([..., u], u.email not in ^excluded_emails)
|
||||
else
|
||||
team_members_query
|
||||
end
|
||||
|
||||
query =
|
||||
invitations_q =
|
||||
from i in Plausible.Auth.Invitation,
|
||||
inner_join: os in subquery(owned_sites_query),
|
||||
on: i.site_id == os.site_id,
|
||||
where: i.role != :owner,
|
||||
select: i.email,
|
||||
union: ^team_members_query
|
||||
where: i.site_id in ^site_ids and i.role != :owner,
|
||||
select: %{email: i.email}
|
||||
|
||||
if excluded_emails != [] do
|
||||
query
|
||||
|> where([i], i.email not in ^excluded_emails)
|
||||
union(memberships_q, ^invitations_q)
|
||||
end
|
||||
|
||||
@spec features_usage(User.t() | nil, list() | nil) :: [atom()]
|
||||
@doc """
|
||||
Given only a user, this function returns the features used across all the
|
||||
sites this user owns + StatsAPI if the user has a configured Stats API key.
|
||||
|
||||
Given a user, and a list of site_ids, returns the features used by those
|
||||
sites instead + StatsAPI if the user has a configured Stats API key.
|
||||
|
||||
The user can also be passed as `nil`, in which case we will never return
|
||||
Stats API as a used feature.
|
||||
"""
|
||||
def features_usage(user, site_ids \\ nil)
|
||||
|
||||
def features_usage(%User{} = user, nil) do
|
||||
site_ids = Plausible.Sites.owned_site_ids(user)
|
||||
features_usage(user, site_ids)
|
||||
end
|
||||
|
||||
def features_usage(%User{} = user, site_ids) when is_list(site_ids) do
|
||||
site_scoped_feature_usage = features_usage(nil, site_ids)
|
||||
|
||||
stats_api_used? =
|
||||
from(a in Plausible.Auth.ApiKey, where: a.user_id == ^user.id)
|
||||
|> Plausible.Repo.exists?()
|
||||
|
||||
if stats_api_used? do
|
||||
site_scoped_feature_usage ++ [Feature.StatsAPI]
|
||||
else
|
||||
query
|
||||
site_scoped_feature_usage
|
||||
end
|
||||
end
|
||||
|
||||
@spec features_usage(User.t() | Site.t()) :: [atom()]
|
||||
@doc """
|
||||
Given a user, this function returns the features used across all the sites
|
||||
this user owns + StatsAPI if the user has a configured Stats API key.
|
||||
|
||||
Given a site, returns the features used by the site.
|
||||
"""
|
||||
def features_usage(%User{} = user) do
|
||||
props_usage_query =
|
||||
def features_usage(nil, site_ids) when is_list(site_ids) do
|
||||
props_usage_q =
|
||||
from s in Site,
|
||||
inner_join: os in subquery(owned_sites_query(user)),
|
||||
on: s.id == os.site_id,
|
||||
where: fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||
|
||||
revenue_goals_usage =
|
||||
revenue_goals_usage_q =
|
||||
from g in Plausible.Goal,
|
||||
inner_join: os in subquery(owned_sites_query(user)),
|
||||
on: g.site_id == os.site_id,
|
||||
where: not is_nil(g.currency)
|
||||
|
||||
stats_api_usage = from a in Plausible.Auth.ApiKey, where: a.user_id == ^user.id
|
||||
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
||||
|
||||
queries =
|
||||
on_ee do
|
||||
funnels_usage_query =
|
||||
from f in "funnels",
|
||||
inner_join: os in subquery(owned_sites_query(user)),
|
||||
on: f.site_id == os.site_id
|
||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||
|
||||
[
|
||||
{Props, props_usage_query},
|
||||
{Funnels, funnels_usage_query},
|
||||
{RevenueGoals, revenue_goals_usage},
|
||||
{StatsAPI, stats_api_usage}
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.Funnels, funnels_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
else
|
||||
[
|
||||
{Props, props_usage_query},
|
||||
{RevenueGoals, revenue_goals_usage},
|
||||
{StatsAPI, stats_api_usage}
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
end
|
||||
|
||||
@ -268,34 +291,4 @@ defmodule Plausible.Billing.Quota.Usage do
|
||||
if Plausible.Repo.exists?(query), do: acc ++ [feature], else: acc
|
||||
end)
|
||||
end
|
||||
|
||||
def features_usage(%Site{} = site) do
|
||||
props_exist = is_list(site.allowed_event_props) && site.allowed_event_props != []
|
||||
|
||||
funnels_exist =
|
||||
on_ee do
|
||||
Plausible.Repo.exists?(from f in Plausible.Funnel, where: f.site_id == ^site.id)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
revenue_goals_exist =
|
||||
Plausible.Repo.exists?(
|
||||
from g in Plausible.Goal, where: g.site_id == ^site.id and not is_nil(g.currency)
|
||||
)
|
||||
|
||||
used_features = [
|
||||
{Props, props_exist},
|
||||
{Funnels, funnels_exist},
|
||||
{RevenueGoals, revenue_goals_exist}
|
||||
]
|
||||
|
||||
for {f_mod, used?} <- used_features, used?, f_mod.enabled?(site), do: f_mod
|
||||
end
|
||||
|
||||
defp owned_sites_query(user) do
|
||||
from sm in Site.Membership,
|
||||
where: sm.role == :owner and sm.user_id == ^user.id,
|
||||
select: %{site_id: sm.site_id}
|
||||
end
|
||||
end
|
||||
|
@ -38,13 +38,16 @@ defmodule Plausible.Site.Memberships do
|
||||
)
|
||||
end
|
||||
|
||||
@spec all_pending_ownerships(String.t()) :: list()
|
||||
def all_pending_ownerships(email) do
|
||||
pending_ownership_invitation_q(email)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec pending_ownerships?(String.t()) :: boolean()
|
||||
def pending_ownerships?(email) do
|
||||
Repo.exists?(
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where: i.email == ^email and i.role == ^:owner
|
||||
)
|
||||
)
|
||||
pending_ownership_invitation_q(email)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec any_or_pending?(Plausible.Auth.User.t()) :: boolean()
|
||||
@ -61,4 +64,10 @@ defmodule Plausible.Site.Memberships do
|
||||
)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
defp pending_ownership_invitation_q(email) do
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where: i.email == ^email and i.role == ^:owner
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -135,7 +135,7 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
defp check_team_member_limit(site, _role, invitee_email) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
limit = Quota.Limits.team_member_limit(site.owner)
|
||||
usage = Quota.Usage.team_member_usage(site.owner, exclude_emails: invitee_email)
|
||||
usage = Quota.Usage.team_member_usage(site.owner, exclude_emails: [invitee_email])
|
||||
|
||||
if Quota.below_limit?(usage, limit),
|
||||
do: :ok,
|
||||
|
@ -75,32 +75,13 @@ defmodule Plausible.Site.Memberships.Invitations do
|
||||
active_subscription? = Plausible.Billing.Subscriptions.active?(new_owner.subscription)
|
||||
|
||||
if active_subscription? && plan != :free_10k do
|
||||
usage_after_transfer = %{
|
||||
monthly_pageviews: monthly_pageview_usage_after_transfer(site, new_owner),
|
||||
team_members: team_member_usage_after_transfer(site, new_owner),
|
||||
sites: Quota.Usage.site_usage(new_owner) + 1
|
||||
}
|
||||
|
||||
Quota.ensure_within_plan_limits(usage_after_transfer, plan)
|
||||
new_owner
|
||||
|> Quota.Usage.usage(pending_ownership_site_ids: [site.id])
|
||||
|> Quota.ensure_within_plan_limits(plan)
|
||||
else
|
||||
{:error, :no_plan}
|
||||
end
|
||||
end
|
||||
|
||||
defp team_member_usage_after_transfer(site, new_owner) do
|
||||
current_usage = Quota.Usage.team_member_usage(new_owner)
|
||||
site_usage = Quota.Usage.team_member_usage(site.owner, site: site)
|
||||
|
||||
extra_usage =
|
||||
if Plausible.Sites.is_member?(new_owner.id, site), do: 0, else: 1
|
||||
|
||||
current_usage + site_usage + extra_usage
|
||||
end
|
||||
|
||||
defp monthly_pageview_usage_after_transfer(site, new_owner) do
|
||||
site_ids = Plausible.Sites.owned_site_ids(new_owner) ++ [site.id]
|
||||
Quota.Usage.monthly_pageview_usage(new_owner, site_ids)
|
||||
end
|
||||
else
|
||||
@spec ensure_can_take_ownership(Site.t(), Auth.User.t()) :: :ok
|
||||
def ensure_can_take_ownership(_site, _new_owner) do
|
||||
@ -116,8 +97,7 @@ defmodule Plausible.Site.Memberships.Invitations do
|
||||
|
||||
def check_feature_access(site, new_owner, false = _selfhost?) do
|
||||
missing_features =
|
||||
site
|
||||
|> Quota.Usage.features_usage()
|
||||
Quota.Usage.features_usage(nil, [site.id])
|
||||
|> Enum.filter(&(&1.check_availability(new_owner) != :ok))
|
||||
|
||||
if missing_features == [] do
|
||||
|
@ -217,6 +217,31 @@ defmodule PlausibleWeb.Components.Billing.Notice do
|
||||
"""
|
||||
end
|
||||
|
||||
def pending_site_ownerships_notice(%{pending_ownership_count: count} = assigns) do
|
||||
if count > 0 do
|
||||
message =
|
||||
"Your account has been invited to become the owner of " <>
|
||||
if(count == 1, do: "a site, which is", else: "#{count} sites, which are") <>
|
||||
" being counted towards the usage of your account."
|
||||
|
||||
assigns = assign(assigns, message: message)
|
||||
|
||||
~H"""
|
||||
<aside class={@class}>
|
||||
<.notice title="Pending ownership transfers" class="shadow-md dark:shadow-none mt-4">
|
||||
<%= @message %> To exclude pending sites from your usage, please go to
|
||||
<.link href="https://plausible.io/sites" class="whitespace-nowrap font-semibold">
|
||||
plausible.io/sites
|
||||
</.link>
|
||||
and reject the invitations.
|
||||
</.notice>
|
||||
</aside>
|
||||
"""
|
||||
else
|
||||
~H""
|
||||
end
|
||||
end
|
||||
|
||||
def growth_grandfathered(assigns) do
|
||||
~H"""
|
||||
<div class="mt-8 space-y-3 text-sm leading-6 text-gray-600 text-justify dark:text-gray-100 xl:mt-10">
|
||||
|
@ -168,7 +168,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
|
||||
|
||||
{checkout_disabled, disabled_message} =
|
||||
cond do
|
||||
not assigns.eligible_for_upgrade? ->
|
||||
not Quota.eligible_for_upgrade?(assigns.usage) ->
|
||||
{true, nil}
|
||||
|
||||
change_plan_link_text == "Currently on this plan" && not subscription_deleted ->
|
||||
|
@ -21,8 +21,19 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
||||
|> assign_new(:user, fn ->
|
||||
Users.with_subscription(user_id)
|
||||
end)
|
||||
|> assign_new(:usage, fn %{user: user} ->
|
||||
Quota.Usage.usage(user, with_features: true)
|
||||
|> assign_new(:pending_ownership_site_ids, fn %{user: user} ->
|
||||
user.email
|
||||
|> Site.Memberships.all_pending_ownerships()
|
||||
|> Enum.map(& &1.site_id)
|
||||
end)
|
||||
|> assign_new(:usage, fn %{
|
||||
user: user,
|
||||
pending_ownership_site_ids: pending_ownership_site_ids
|
||||
} ->
|
||||
Quota.Usage.usage(user,
|
||||
with_features: true,
|
||||
pending_ownership_site_ids: pending_ownership_site_ids
|
||||
)
|
||||
end)
|
||||
|> assign_new(:owned_plan, fn %{user: %{subscription: subscription}} ->
|
||||
Plans.get_regular_plan(subscription, only_non_expired: true)
|
||||
@ -30,18 +41,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
||||
|> assign_new(:owned_tier, fn %{owned_plan: owned_plan} ->
|
||||
if owned_plan, do: Map.get(owned_plan, :kind), else: nil
|
||||
end)
|
||||
|> assign_new(:eligible_for_upgrade?, fn %{user: user, usage: usage} ->
|
||||
has_sites? = usage.sites > 0
|
||||
has_pending_ownerships? = Site.Memberships.pending_ownerships?(user.email)
|
||||
|
||||
has_sites? or has_pending_ownerships?
|
||||
end)
|
||||
|> assign_new(:recommended_tier, fn %{
|
||||
owned_plan: owned_plan,
|
||||
eligible_for_upgrade?: eligible_for_upgrade?,
|
||||
usage: usage,
|
||||
user: user
|
||||
} ->
|
||||
if owned_plan != nil or not eligible_for_upgrade? do
|
||||
if owned_plan != nil or not Quota.eligible_for_upgrade?(usage) do
|
||||
nil
|
||||
else
|
||||
Plans.suggest_tier(user)
|
||||
@ -103,9 +108,13 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
||||
~H"""
|
||||
<div class="bg-gray-100 dark:bg-gray-900 pt-1 pb-12 sm:pb-16 text-gray-900 dark:text-gray-100">
|
||||
<div class="mx-auto max-w-7xl px-6 lg:px-20">
|
||||
<Notice.pending_site_ownerships_notice
|
||||
class="pb-6"
|
||||
pending_ownership_count={length(@pending_ownership_site_ids)}
|
||||
/>
|
||||
<Notice.subscription_past_due class="pb-6" subscription={@user.subscription} />
|
||||
<Notice.subscription_paused class="pb-6" subscription={@user.subscription} />
|
||||
<Notice.upgrade_ineligible :if={not @eligible_for_upgrade?} />
|
||||
<Notice.upgrade_ineligible :if={not Quota.eligible_for_upgrade?(@usage)} />
|
||||
<div class="mx-auto max-w-4xl text-center">
|
||||
<p class="text-4xl font-bold tracking-tight lg:text-5xl">
|
||||
<%= if @owned_plan,
|
||||
|
@ -244,7 +244,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "team_member_usage/1" do
|
||||
describe "team_member_usage/2" do
|
||||
test "returns the number of members in all of the sites the user owns" do
|
||||
me = insert(:user)
|
||||
|
||||
@ -335,7 +335,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
assert Quota.Usage.team_member_usage(me) == 3
|
||||
end
|
||||
|
||||
test "does not count ownership transfer as a team member" do
|
||||
test "does not count ownership transfer as a team member by default" do
|
||||
me = insert(:user)
|
||||
site_i_own = insert(:site, memberships: [build(:site_membership, user: me, role: :owner)])
|
||||
|
||||
@ -344,6 +344,62 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
assert Quota.Usage.team_member_usage(me) == 0
|
||||
end
|
||||
|
||||
test "counts team members from pending ownerships when specified" do
|
||||
me = insert(:user)
|
||||
|
||||
user_1 = insert(:user)
|
||||
user_2 = insert(:user)
|
||||
|
||||
pending_ownership_site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user_1, role: :owner),
|
||||
build(:site_membership, user: user_2, role: :admin)
|
||||
]
|
||||
)
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_ownership_site,
|
||||
inviter: user_1,
|
||||
email: me.email,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me,
|
||||
pending_ownership_site_ids: [pending_ownership_site.id]
|
||||
) == 2
|
||||
end
|
||||
|
||||
test "counts invitations towards team members from pending ownership sites" do
|
||||
me = insert(:user)
|
||||
|
||||
user_1 = insert(:user)
|
||||
user_2 = insert(:user)
|
||||
|
||||
pending_ownership_site =
|
||||
insert(:site,
|
||||
memberships: [build(:site_membership, user: user_1, role: :owner)]
|
||||
)
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_ownership_site,
|
||||
inviter: user_1,
|
||||
email: me.email,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_ownership_site,
|
||||
inviter: user_1,
|
||||
email: user_2.email,
|
||||
role: :admin
|
||||
)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me,
|
||||
pending_ownership_site_ids: [pending_ownership_site.id]
|
||||
) == 2
|
||||
end
|
||||
|
||||
test "returns zero when user does not have any site" do
|
||||
me = insert(:user)
|
||||
assert Quota.Usage.team_member_usage(me) == 0
|
||||
@ -378,9 +434,9 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
invitation = insert(:invitation, site: site_i_own, inviter: me, email: "foo@example.com")
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 4
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: "arbitrary@example.com") == 4
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: member.email) == 3
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: invitation.email) == 3
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: ["arbitrary@example.com"]) == 4
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: [member.email]) == 3
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: [invitation.email]) == 3
|
||||
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: [member.email, invitation.email]) ==
|
||||
2
|
||||
@ -441,10 +497,10 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "features_usage/1" do
|
||||
describe "features_usage/2" do
|
||||
test "returns an empty list for a user/site who does not use any feature" do
|
||||
assert [] == Quota.Usage.features_usage(insert(:user))
|
||||
assert [] == Quota.Usage.features_usage(insert(:site))
|
||||
assert [] == Quota.Usage.features_usage(nil, [insert(:site).id])
|
||||
end
|
||||
|
||||
test "returns [Props] when user/site uses custom props" do
|
||||
@ -456,7 +512,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
memberships: [build(:site_membership, user: user, role: :owner)]
|
||||
)
|
||||
|
||||
assert [Props] == Quota.Usage.features_usage(site)
|
||||
assert [Props] == Quota.Usage.features_usage(nil, [site.id])
|
||||
assert [Props] == Quota.Usage.features_usage(user)
|
||||
end
|
||||
|
||||
@ -469,7 +525,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
steps = Enum.map(goals, &%{"goal_id" => &1.id})
|
||||
Plausible.Funnels.create(site, "dummy", steps)
|
||||
|
||||
assert [Funnels] == Quota.Usage.features_usage(site)
|
||||
assert [Funnels] == Quota.Usage.features_usage(nil, [site.id])
|
||||
assert [Funnels] == Quota.Usage.features_usage(user)
|
||||
end
|
||||
|
||||
@ -478,7 +534,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
insert(:goal, currency: :USD, site: site, event_name: "Purchase")
|
||||
|
||||
assert [RevenueGoals] == Quota.Usage.features_usage(site)
|
||||
assert [RevenueGoals] == Quota.Usage.features_usage(nil, [site.id])
|
||||
assert [RevenueGoals] == Quota.Usage.features_usage(user)
|
||||
end
|
||||
end
|
||||
@ -490,9 +546,20 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
assert [StatsAPI] == Quota.Usage.features_usage(user)
|
||||
end
|
||||
|
||||
test "returns feature usage based on a user and a custom list of site_ids" do
|
||||
user = insert(:user)
|
||||
insert(:api_key, user: user)
|
||||
|
||||
site_using_props = insert(:site, allowed_event_props: ["dummy"])
|
||||
|
||||
site_ids = [site_using_props.id]
|
||||
assert [Props, StatsAPI] == Quota.Usage.features_usage(user, site_ids)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
test "returns multiple features" do
|
||||
test "returns multiple features used by the user" do
|
||||
user = insert(:user)
|
||||
insert(:api_key, user: user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
@ -506,8 +573,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
steps = Enum.map(goals, &%{"goal_id" => &1.id})
|
||||
Plausible.Funnels.create(site, "dummy", steps)
|
||||
|
||||
assert [Props, Funnels, RevenueGoals] == Quota.Usage.features_usage(site)
|
||||
assert [Props, Funnels, RevenueGoals] == Quota.Usage.features_usage(user)
|
||||
assert [Props, Funnels, RevenueGoals, StatsAPI] == Quota.Usage.features_usage(user)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -375,6 +375,75 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
||||
assert doc =~ "billable pageviews in the last billing cycle"
|
||||
end
|
||||
|
||||
test "renders notice about pending ownerships and counts their usage", %{
|
||||
conn: conn,
|
||||
user: user,
|
||||
site: site
|
||||
} do
|
||||
yesterday = NaiveDateTime.utc_now() |> NaiveDateTime.add(-1, :day)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview, timestamp: yesterday)
|
||||
])
|
||||
|
||||
another_user = insert(:user)
|
||||
|
||||
pending_site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, role: :owner, user: another_user),
|
||||
build(:site_membership, role: :admin, user: build(:user)),
|
||||
build(:site_membership, role: :viewer, user: build(:user)),
|
||||
build(:site_membership, role: :viewer, user: build(:user))
|
||||
]
|
||||
)
|
||||
|
||||
populate_stats(pending_site, [
|
||||
build(:pageview, timestamp: yesterday)
|
||||
])
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_site,
|
||||
inviter: another_user,
|
||||
email: user.email,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
{:ok, _lv, doc} = get_liveview(conn)
|
||||
|
||||
assert doc =~ "Your account has been invited to become the owner of a site"
|
||||
|
||||
assert text_of_element(doc, @growth_plan_tooltip) ==
|
||||
"Your usage exceeds the following limit(s): Team member limit"
|
||||
|
||||
assert doc =~ "<b>2</b>"
|
||||
assert doc =~ "billable pageviews in the last billing cycle"
|
||||
end
|
||||
|
||||
test "warns about losing access to a feature used by a pending ownership site", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
another_user = insert(:user)
|
||||
pending_site = insert(:site, members: [another_user])
|
||||
|
||||
Plausible.Props.allow(pending_site, ["author"])
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_site,
|
||||
inviter: another_user,
|
||||
email: user.email,
|
||||
role: :owner
|
||||
)
|
||||
|
||||
{:ok, _lv, doc} = get_liveview(conn)
|
||||
|
||||
assert doc =~ "Your account has been invited to become the owner of a site"
|
||||
|
||||
assert text_of_attr(find(doc, @growth_checkout_button), "onclick") =~
|
||||
"if (confirm(\"This plan does not support Custom Properties, which you are currently using. Please note that by subscribing to this plan you will lose access to this feature.\")) {window.location = "
|
||||
end
|
||||
|
||||
test "gets default selected interval from current subscription plan", %{conn: conn} do
|
||||
{:ok, _lv, doc} = get_liveview(conn)
|
||||
assert class_of_element(doc, @yearly_interval_button) =~ @interval_button_active_class
|
||||
|
Loading…
Reference in New Issue
Block a user