mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
Remove team adapters (#4877)
* wip
* wip
* 🍌
* WIP
* Draw the rest of the owl (well, almost)
* Remove obsolete unlimited trial logic
* Remove `allow_next_upgrade_override?` adapter
* Remove `Teams` adapter
* Remove /sites adapters
* Remove `Sites` adapter
* Remove `change_plan` adapter
* Fix up CE test
* Remove adapter for Billing.latest_enterprise_plan_with_price(s)
* Remove adapter for `Billing.has_active_subscription?`
* Remove adapter for `Billing.active_subscription_for`
* Remove remaining billing adapter
* Remove all_pending_transfers
* Remove `get_owner` adapter
* Remove `has_sites?` and `owns_sites?` adapters
* Remove `Ownership` adapter
* Remove `check_invitation_permissions` adapter
* Remove `check_team_member_limit` adapter
* Remove `ensure_transfer_valid` adapter
* Remove Invitations adapter
* Remove sole teams adapter
* Make dialyzer happy
* Consolidate `Billing.features_usage` definition
* Remove unused `Quota.Usage`
* Remove remains of `read_team_schema` FF and reduce number of CI passes
* Bang up the condition
* Include pending invitations when querying `has_sites?`
* Fix and improve conditional expression in `plan_box` component
* Update comments
---------
Co-authored-by: Adam Rutkowski <hq@mtod.org>
This commit is contained in:
parent
90e541be38
commit
3afec60d98
13
.github/workflows/elixir.yml
vendored
13
.github/workflows/elixir.yml
vendored
@ -16,22 +16,19 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build and test (${{ matrix.mix_env }}, ${{ matrix.postgres_image }}${{ matrix.test_read_team_schemas == '1' && ', read_team_schemas' || '' }})"
|
||||
name: "Build and test (${{ matrix.mix_env }}, ${{ matrix.postgres_image }})"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
mix_env: ["test", "ce_test"]
|
||||
postgres_image: ["postgres:16"]
|
||||
test_read_team_schemas: ["0"]
|
||||
mix_env: ["test"]
|
||||
postgres_image: ["postgres:15"]
|
||||
|
||||
include:
|
||||
- mix_env: "test"
|
||||
postgres_image: "postgres:15"
|
||||
test_read_team_schemas: "1"
|
||||
- mix_env: "ce_test"
|
||||
postgres_image: "postgres:16"
|
||||
|
||||
env:
|
||||
MIX_ENV: ${{ matrix.mix_env }}
|
||||
TEST_READ_TEAM_SCHEMAS: ${{ matrix.test_read_team_schemas }}
|
||||
services:
|
||||
postgres:
|
||||
image: ${{ matrix.postgres_image }}
|
||||
|
@ -65,7 +65,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
{:ok, %{site: site}} ->
|
||||
json(conn, site)
|
||||
|
||||
{:error, {:over_limit, limit}} ->
|
||||
{:error, _, {:over_limit, limit}, _} ->
|
||||
conn
|
||||
|> put_status(402)
|
||||
|> json(%{
|
||||
@ -206,7 +206,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
|
||||
end
|
||||
|
||||
defp get_site(user, site_id, roles) do
|
||||
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, site_id, roles) do
|
||||
case Plausible.Sites.get_for_user(user, site_id, roles) do
|
||||
nil -> {:error, :site_not_found}
|
||||
site -> {:ok, site}
|
||||
end
|
||||
|
@ -16,7 +16,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
@ -106,7 +106,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
|
||||
|
||||
def handle_event("delete-funnel", %{"funnel-id" => id}, socket) do
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(
|
||||
Plausible.Sites.get_for_user!(
|
||||
socket.assigns.current_user,
|
||||
socket.assigns.domain,
|
||||
[:owner, :admin]
|
||||
|
@ -13,7 +13,7 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
|
||||
|
||||
def mount(_params, %{"domain" => domain} = session, socket) do
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -117,17 +117,6 @@ defmodule Plausible.Auth do
|
||||
end)
|
||||
end
|
||||
|
||||
def user_owns_sites?(user) do
|
||||
Repo.exists?(
|
||||
from(s in Plausible.Site,
|
||||
join: sm in Plausible.Site.Membership,
|
||||
on: sm.site_id == s.id,
|
||||
where: sm.user_id == ^user.id,
|
||||
where: sm.role == :owner
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
def is_super_admin?(nil), do: false
|
||||
def is_super_admin?(%Plausible.Auth.User{id: id}), do: is_super_admin?(id)
|
||||
@ -139,21 +128,19 @@ defmodule Plausible.Auth do
|
||||
def is_super_admin?(_), do: false
|
||||
end
|
||||
|
||||
def enterprise_configured?(nil), do: false
|
||||
|
||||
def enterprise_configured?(%Plausible.Auth.User{} = user) do
|
||||
user
|
||||
|> Ecto.assoc(:enterprise_plan)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec create_api_key(Auth.User.t(), String.t(), String.t()) ::
|
||||
{:ok, Auth.ApiKey.t()} | {:error, Ecto.Changeset.t() | :upgrade_required}
|
||||
def create_api_key(user, name, key) do
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
params = %{name: name, user_id: user.id, key: key}
|
||||
changeset = Auth.ApiKey.changeset(%Auth.ApiKey{}, params)
|
||||
|
||||
with :ok <- Plausible.Billing.Feature.StatsAPI.check_availability(user),
|
||||
with :ok <- Plausible.Billing.Feature.StatsAPI.check_availability(team),
|
||||
do: Repo.insert(changeset)
|
||||
end
|
||||
|
||||
|
@ -2,20 +2,9 @@ defmodule Plausible.Billing do
|
||||
use Plausible
|
||||
use Plausible.Repo
|
||||
require Plausible.Billing.Subscription.Status
|
||||
alias Plausible.Billing.Subscriptions
|
||||
alias Plausible.Billing.{Subscription, Plans, Quota}
|
||||
alias Plausible.Billing.Subscription
|
||||
alias Plausible.Auth.User
|
||||
|
||||
@spec active_subscription_for(User.t()) :: Subscription.t() | nil
|
||||
def active_subscription_for(user) do
|
||||
user |> active_subscription_query() |> Repo.one()
|
||||
end
|
||||
|
||||
@spec has_active_subscription?(User.t()) :: boolean()
|
||||
def has_active_subscription?(user) do
|
||||
user |> active_subscription_query() |> Repo.exists?()
|
||||
end
|
||||
|
||||
def subscription_created(params) do
|
||||
Repo.transaction(fn ->
|
||||
handle_subscription_created(params)
|
||||
@ -40,44 +29,6 @@ defmodule Plausible.Billing do
|
||||
end)
|
||||
end
|
||||
|
||||
def change_plan(user, new_plan_id) do
|
||||
subscription = active_subscription_for(user)
|
||||
plan = Plans.find(new_plan_id)
|
||||
|
||||
limit_checking_opts =
|
||||
if user.allow_next_upgrade_override do
|
||||
[ignore_pageview_limit: true]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
with :ok <- Quota.ensure_within_plan_limits(user, plan, limit_checking_opts),
|
||||
do: do_change_plan(subscription, new_plan_id)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_change_plan(subscription, new_plan_id) do
|
||||
res =
|
||||
paddle_api().update_subscription(subscription.paddle_subscription_id, %{
|
||||
plan_id: new_plan_id
|
||||
})
|
||||
|
||||
case res do
|
||||
{:ok, response} ->
|
||||
amount = :erlang.float_to_binary(response["next_payment"]["amount"] / 1, decimals: 2)
|
||||
|
||||
Subscription.changeset(subscription, %{
|
||||
paddle_plan_id: Integer.to_string(response["plan_id"]),
|
||||
next_bill_amount: amount,
|
||||
next_bill_date: response["next_payment"]["date"]
|
||||
})
|
||||
|> Repo.update()
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def change_plan_preview(subscription, new_plan_id) do
|
||||
case paddle_api().update_subscription_preview(
|
||||
subscription.paddle_subscription_id,
|
||||
@ -91,33 +42,6 @@ defmodule Plausible.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_needs_to_upgrade(User.t()) ::
|
||||
{:needs_to_upgrade, :no_trial | :no_active_subscription | :grace_period_ended}
|
||||
| :no_upgrade_needed
|
||||
def check_needs_to_upgrade(user) do
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
|
||||
trial_over? =
|
||||
not is_nil(user.trial_expiry_date) and
|
||||
Date.before?(user.trial_expiry_date, Date.utc_today())
|
||||
|
||||
subscription_active? = Subscriptions.active?(user.subscription)
|
||||
|
||||
cond do
|
||||
is_nil(user.trial_expiry_date) and not subscription_active? ->
|
||||
{:needs_to_upgrade, :no_trial}
|
||||
|
||||
trial_over? and not subscription_active? ->
|
||||
{:needs_to_upgrade, :no_active_subscription}
|
||||
|
||||
Plausible.Auth.GracePeriod.expired?(user) ->
|
||||
{:needs_to_upgrade, :grace_period_ended}
|
||||
|
||||
true ->
|
||||
:no_upgrade_needed
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_subscription_created(params) do
|
||||
params =
|
||||
if present?(params["passthrough"]) do
|
||||
@ -284,14 +208,6 @@ defmodule Plausible.Billing do
|
||||
"subscription_cancelled__#{id}"
|
||||
end
|
||||
|
||||
defp active_subscription_query(user) do
|
||||
from(s in Subscription,
|
||||
where: s.user_id == ^user.id and s.status == ^Subscription.Status.active(),
|
||||
order_by: [desc: s.inserted_at],
|
||||
limit: 1
|
||||
)
|
||||
end
|
||||
|
||||
defp after_subscription_update(subscription) do
|
||||
user =
|
||||
User
|
||||
|
@ -65,9 +65,9 @@ defmodule Plausible.Billing.Feature do
|
||||
@callback opted_out?(Plausible.Site.t()) :: boolean()
|
||||
|
||||
@doc """
|
||||
Checks whether the site owner or the user plan includes the given feature.
|
||||
Checks whether the team or the team plan includes the given feature.
|
||||
"""
|
||||
@callback check_availability(Plausible.Auth.User.t() | Plausible.Teams.Team.t() | nil) ::
|
||||
@callback check_availability(Plausible.Teams.Team.t() | nil) ::
|
||||
:ok | {:error, :upgrade_required} | {:error, :not_implemented}
|
||||
|
||||
@features [
|
||||
@ -120,8 +120,8 @@ defmodule Plausible.Billing.Feature do
|
||||
|
||||
@impl true
|
||||
def enabled?(%Plausible.Site{} = site) do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
check_availability(site.owner) == :ok && !opted_out?(site)
|
||||
site = Plausible.Repo.preload(site, :team)
|
||||
check_availability(site.team) == :ok && !opted_out?(site)
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -130,8 +130,12 @@ defmodule Plausible.Billing.Feature do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def check_availability(%Plausible.Auth.User{} = user) do
|
||||
Plausible.Teams.Adapter.Read.Billing.check_feature_availability(__MODULE__, user)
|
||||
def check_availability(team_or_nil) do
|
||||
cond do
|
||||
free?() -> :ok
|
||||
__MODULE__ in Plausible.Teams.Billing.allowed_features_for(team_or_nil) -> :ok
|
||||
true -> {:error, :upgrade_required}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
@ -140,11 +144,9 @@ defmodule Plausible.Billing.Feature do
|
||||
end
|
||||
|
||||
defp do_toggle(%Plausible.Site{} = site, user, opts) do
|
||||
owner = Plausible.Teams.Adapter.Read.Ownership.get_owner(site, user)
|
||||
|
||||
override = Keyword.get(opts, :override)
|
||||
toggle = if is_boolean(override), do: override, else: !Map.fetch!(site, toggle_field())
|
||||
availability = if toggle, do: check_availability(owner), else: :ok
|
||||
availability = if toggle, do: check_availability(site.team), else: :ok
|
||||
|
||||
case availability do
|
||||
:ok ->
|
||||
@ -195,22 +197,10 @@ defmodule Plausible.Billing.Feature.Props do
|
||||
end
|
||||
|
||||
defmodule Plausible.Billing.Feature.StatsAPI do
|
||||
use Plausible
|
||||
|
||||
@moduledoc false
|
||||
use Plausible.Billing.Feature,
|
||||
name: :stats_api,
|
||||
display_name: "Stats API"
|
||||
|
||||
@impl true
|
||||
@doc """
|
||||
Checks whether the user has access to Stats API or not.
|
||||
|
||||
Before the business tier, users who had not yet started their trial had
|
||||
access to Stats API. With the business tier work, access is blocked and they
|
||||
must either start their trial or subscribe to a plan. This is common when a
|
||||
site owner invites a new user. In such cases, using the owner's API key is
|
||||
recommended.
|
||||
"""
|
||||
def check_availability(%Plausible.Auth.User{} = user) do
|
||||
Plausible.Teams.Adapter.Read.Billing.check_feature_availability_for_stats_api(user)
|
||||
end
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ defmodule Plausible.Billing.Plans do
|
||||
alias Plausible.Billing.Subscriptions
|
||||
use Plausible.Repo
|
||||
alias Plausible.Billing.{Subscription, Plan, EnterprisePlan}
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Teams
|
||||
|
||||
for f <- [
|
||||
:legacy_plans,
|
||||
@ -25,9 +25,6 @@ defmodule Plausible.Billing.Plans do
|
||||
Module.put_attribute(__MODULE__, :external_resource, path)
|
||||
end
|
||||
|
||||
@business_tier_launch ~N[2023-11-08 12:00:00]
|
||||
def business_tier_launch, do: @business_tier_launch
|
||||
|
||||
@spec growth_plans_for(Subscription.t()) :: [Plan.t()]
|
||||
@doc """
|
||||
Returns a list of growth plans available for the subscription to choose.
|
||||
@ -115,19 +112,6 @@ defmodule Plausible.Billing.Plans do
|
||||
end
|
||||
end
|
||||
|
||||
def latest_enterprise_plan_with_price(user, customer_ip) do
|
||||
enterprise_plan =
|
||||
Repo.one!(
|
||||
from(e in EnterprisePlan,
|
||||
where: e.user_id == ^user.id,
|
||||
order_by: [desc: e.inserted_at],
|
||||
limit: 1
|
||||
)
|
||||
)
|
||||
|
||||
{enterprise_plan, get_price_for(enterprise_plan, customer_ip)}
|
||||
end
|
||||
|
||||
def subscription_interval(subscription) do
|
||||
case get_subscription_plan(subscription) do
|
||||
%EnterprisePlan{billing_interval: interval} ->
|
||||
@ -202,24 +186,30 @@ defmodule Plausible.Billing.Plans do
|
||||
end
|
||||
end
|
||||
|
||||
@enterprise_level_usage 10_000_000
|
||||
@spec suggest(User.t(), non_neg_integer()) :: Plan.t()
|
||||
@doc """
|
||||
Returns the most appropriate plan for a user based on their usage during a
|
||||
Returns the most appropriate plan for a team based on its usage during a
|
||||
given cycle.
|
||||
|
||||
If the usage during the cycle exceeds the enterprise-level threshold, or if
|
||||
the user already belongs to an enterprise plan, it suggests the :enterprise
|
||||
the team already has an enterprise plan, it suggests the :enterprise
|
||||
plan.
|
||||
|
||||
Otherwise, it recommends the plan where the cycle usage falls just under the
|
||||
plan's limit from the available options for the user.
|
||||
plan's limit from the available options for the team.
|
||||
"""
|
||||
def suggest(user, usage_during_cycle) do
|
||||
@enterprise_level_usage 10_000_000
|
||||
@spec suggest(Teams.Team.t(), non_neg_integer()) :: Plan.t()
|
||||
def suggest(team, usage_during_cycle) do
|
||||
cond do
|
||||
usage_during_cycle > @enterprise_level_usage -> :enterprise
|
||||
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user) -> :enterprise
|
||||
true -> Plausible.Teams.Adapter.Read.Billing.suggest_by_usage(user, usage_during_cycle)
|
||||
usage_during_cycle > @enterprise_level_usage ->
|
||||
:enterprise
|
||||
|
||||
Teams.Billing.enterprise_configured?(team) ->
|
||||
:enterprise
|
||||
|
||||
true ->
|
||||
subscription = Teams.Billing.get_subscription(team)
|
||||
suggest_by_usage(subscription, usage_during_cycle)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,120 +1,13 @@
|
||||
defmodule Plausible.Billing.Quota.Limits do
|
||||
@moduledoc false
|
||||
|
||||
use Plausible
|
||||
alias Plausible.Users
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Billing.{Plan, Plans, Subscription, EnterprisePlan, Feature}
|
||||
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI}
|
||||
|
||||
@type over_limits_error() :: {:over_plan_limits, [limit()]}
|
||||
@typep limit() :: :site_limit | :pageview_limit | :team_member_limit
|
||||
|
||||
@pageview_allowance_margin 0.1
|
||||
|
||||
on_ee do
|
||||
@limit_sites_since ~D[2021-05-05]
|
||||
@site_limit_for_trials 10
|
||||
@team_member_limit_for_trials 3
|
||||
|
||||
@spec site_limit(User.t()) :: non_neg_integer() | :unlimited
|
||||
def site_limit(user) do
|
||||
if Date.before?(user.inserted_at, @limit_sites_since) do
|
||||
:unlimited
|
||||
else
|
||||
get_site_limit_from_plan(user)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_site_limit_from_plan(user) do
|
||||
user = Users.with_subscription(user)
|
||||
|
||||
case Plans.get_subscription_plan(user.subscription) do
|
||||
%{site_limit: site_limit} -> site_limit
|
||||
:free_10k -> 50
|
||||
nil -> @site_limit_for_trials
|
||||
end
|
||||
end
|
||||
|
||||
@spec team_member_limit(User.t()) :: non_neg_integer()
|
||||
def team_member_limit(user) do
|
||||
user = Users.with_subscription(user)
|
||||
|
||||
case Plans.get_subscription_plan(user.subscription) do
|
||||
%{team_member_limit: limit} -> limit
|
||||
:free_10k -> :unlimited
|
||||
nil -> @team_member_limit_for_trials
|
||||
end
|
||||
end
|
||||
else
|
||||
def site_limit(_) do
|
||||
:unlimited
|
||||
end
|
||||
|
||||
def team_member_limit(_) do
|
||||
:unlimited
|
||||
end
|
||||
end
|
||||
|
||||
@monthly_pageview_limit_for_free_10k 10_000
|
||||
@monthly_pageview_limit_for_trials :unlimited
|
||||
|
||||
@spec monthly_pageview_limit(User.t() | Subscription.t() | nil) ::
|
||||
non_neg_integer() | :unlimited
|
||||
def monthly_pageview_limit(%User{} = user) do
|
||||
user = Users.with_subscription(user)
|
||||
monthly_pageview_limit(user.subscription)
|
||||
end
|
||||
|
||||
def monthly_pageview_limit(subscription) do
|
||||
case Plans.get_subscription_plan(subscription) do
|
||||
%EnterprisePlan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
%Plan{monthly_pageview_limit: limit} ->
|
||||
limit
|
||||
|
||||
:free_10k ->
|
||||
@monthly_pageview_limit_for_free_10k
|
||||
|
||||
_any ->
|
||||
if subscription do
|
||||
Sentry.capture_message("Unknown monthly pageview limit for plan",
|
||||
extra: %{paddle_plan_id: subscription.paddle_plan_id}
|
||||
)
|
||||
end
|
||||
|
||||
@monthly_pageview_limit_for_trials
|
||||
end
|
||||
end
|
||||
|
||||
def pageview_limit_with_margin(limit, margin \\ nil) do
|
||||
margin = if margin, do: margin, else: @pageview_allowance_margin
|
||||
ceil(limit * (1 + margin))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list of features the user can use. Trial users have the
|
||||
ability to use all features during their trial.
|
||||
"""
|
||||
def allowed_features_for(user) do
|
||||
user = Users.with_subscription(user)
|
||||
|
||||
case Plans.get_subscription_plan(user.subscription) do
|
||||
%EnterprisePlan{features: features} ->
|
||||
features
|
||||
|
||||
%Plan{features: features} ->
|
||||
features
|
||||
|
||||
:free_10k ->
|
||||
[Goals, Props, StatsAPI]
|
||||
|
||||
nil ->
|
||||
if Users.on_trial?(user) do
|
||||
Feature.list()
|
||||
else
|
||||
[Goals]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -4,47 +4,21 @@ defmodule Plausible.Billing.Quota do
|
||||
"""
|
||||
|
||||
use Plausible
|
||||
alias Plausible.Users
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Billing.{Plan, Plans, EnterprisePlan}
|
||||
alias Plausible.Billing.Quota.{Usage, Limits}
|
||||
alias Plausible.Billing.{Plan, EnterprisePlan}
|
||||
alias Plausible.Billing.Quota.Limits
|
||||
|
||||
@doc """
|
||||
Enterprise plans are always allowed to add more sites (even when
|
||||
over limit) to avoid service disruption. Their usage is checked
|
||||
in a background job instead (see `check_usage.ex`).
|
||||
"""
|
||||
def ensure_can_add_new_site(user) do
|
||||
user = Users.with_subscription(user)
|
||||
|
||||
case Plans.get_subscription_plan(user.subscription) do
|
||||
%EnterprisePlan{} ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
usage = Usage.site_usage(user)
|
||||
limit = Limits.site_limit(user)
|
||||
|
||||
if below_limit?(usage, limit), do: :ok, else: {:error, {:over_limit, limit}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures that the given user (or the usage map) is within the limits
|
||||
Ensures that the given usage map is within the limits
|
||||
of the given plan.
|
||||
|
||||
An `opts` argument can be passed with `ignore_pageview_limit: true`
|
||||
which bypasses the pageview limit check and returns `:ok` as long as
|
||||
the other limits are not exceeded.
|
||||
"""
|
||||
@spec ensure_within_plan_limits(User.t() | map(), struct() | atom() | nil, Keyword.t()) ::
|
||||
@spec ensure_within_plan_limits(map(), struct() | atom() | nil, Keyword.t()) ::
|
||||
:ok | {:error, Limits.over_limits_error()}
|
||||
def ensure_within_plan_limits(user_or_usage, plan, opts \\ [])
|
||||
|
||||
def ensure_within_plan_limits(%User{} = user, %plan_mod{} = plan, opts)
|
||||
when plan_mod in [Plan, EnterprisePlan] do
|
||||
ensure_within_plan_limits(Usage.usage(user), plan, opts)
|
||||
end
|
||||
def ensure_within_plan_limits(usage, plan_mod, opts \\ [])
|
||||
|
||||
def ensure_within_plan_limits(usage, %plan_mod{} = plan, opts)
|
||||
when plan_mod in [Plan, EnterprisePlan] do
|
||||
@ -121,13 +95,14 @@ defmodule Plausible.Billing.Quota do
|
||||
end
|
||||
end
|
||||
|
||||
@spec exceeds_last_two_usage_cycles?(Usage.cycles_usage(), non_neg_integer()) :: boolean()
|
||||
@spec exceeds_last_two_usage_cycles?(Plausible.Teams.Billing.cycles_usage(), non_neg_integer()) ::
|
||||
boolean()
|
||||
def exceeds_last_two_usage_cycles?(cycles_usage, allowed_volume) do
|
||||
exceeded = exceeded_cycles(cycles_usage, allowed_volume)
|
||||
:penultimate_cycle in exceeded && :last_cycle in exceeded
|
||||
end
|
||||
|
||||
@spec exceeded_cycles(Usage.cycles_usage(), non_neg_integer()) :: list()
|
||||
@spec exceeded_cycles(Plausible.Teams.Billing.cycles_usage(), non_neg_integer()) :: list()
|
||||
def exceeded_cycles(cycles_usage, allowed_volume) do
|
||||
limit = Limits.pageview_limit_with_margin(allowed_volume)
|
||||
|
||||
|
@ -1,294 +0,0 @@
|
||||
defmodule Plausible.Billing.Quota.Usage do
|
||||
@moduledoc false
|
||||
|
||||
use Plausible
|
||||
import Ecto.Query
|
||||
alias Plausible.Users
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Site
|
||||
alias Plausible.Billing.{Subscriptions, Feature}
|
||||
|
||||
@type cycles_usage() :: %{cycle() => usage_cycle()}
|
||||
|
||||
@typep cycle :: :current_cycle | :last_cycle | :penultimate_cycle
|
||||
@typep last_30_days_usage() :: %{:last_30_days => usage_cycle()}
|
||||
@typep monthly_pageview_usage() :: cycles_usage() | last_30_days_usage()
|
||||
|
||||
@typep usage_cycle :: %{
|
||||
date_range: Date.Range.t(),
|
||||
pageviews: non_neg_integer(),
|
||||
custom_events: non_neg_integer(),
|
||||
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, 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, all_site_ids))
|
||||
else
|
||||
basic_usage
|
||||
end
|
||||
end
|
||||
|
||||
@spec site_usage(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
|
||||
|
||||
@doc """
|
||||
Queries the ClickHouse database for the monthly pageview usage. If the given user's
|
||||
subscription is `active`, `past_due`, or a `deleted` (but not yet expired), a map
|
||||
with the following structure is returned:
|
||||
|
||||
```elixir
|
||||
%{
|
||||
current_cycle: usage_cycle(),
|
||||
last_cycle: usage_cycle(),
|
||||
penultimate_cycle: usage_cycle()
|
||||
}
|
||||
```
|
||||
|
||||
In all other cases of the subscription status (or a `free_10k` subscription which
|
||||
does not have a `last_bill_date` defined) - the following structure is returned:
|
||||
|
||||
```elixir
|
||||
%{last_30_days: usage_cycle()}
|
||||
```
|
||||
|
||||
Given only a user as input, the usage is queried from across all the sites that the
|
||||
user owns. Alternatively, given an optional argument of `site_ids`, the usage from
|
||||
across all those sites is queried instead.
|
||||
"""
|
||||
@spec monthly_pageview_usage(User.t(), list() | nil) :: monthly_pageview_usage()
|
||||
def monthly_pageview_usage(user, site_ids \\ nil)
|
||||
|
||||
def monthly_pageview_usage(user, nil) do
|
||||
monthly_pageview_usage(user, Plausible.Sites.owned_site_ids(user))
|
||||
end
|
||||
|
||||
def monthly_pageview_usage(user, site_ids) do
|
||||
active_subscription? = Subscriptions.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, site_ids)}
|
||||
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, site_ids)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec usage_cycle(User.t(), :last_30_days | cycle(), list() | nil, Date.t()) :: usage_cycle()
|
||||
def usage_cycle(user, cycle, owned_site_ids \\ nil, today \\ Date.utc_today())
|
||||
|
||||
def usage_cycle(user, cycle, nil, today) do
|
||||
usage_cycle(user, cycle, Plausible.Sites.owned_site_ids(user), today)
|
||||
end
|
||||
|
||||
def usage_cycle(_user, :last_30_days, owned_site_ids, today) do
|
||||
date_range = Date.range(Date.shift(today, day: -30), today)
|
||||
|
||||
{pageviews, custom_events} =
|
||||
Plausible.Stats.Clickhouse.usage_breakdown(owned_site_ids, date_range)
|
||||
|
||||
%{
|
||||
date_range: date_range,
|
||||
pageviews: pageviews,
|
||||
custom_events: custom_events,
|
||||
total: pageviews + custom_events
|
||||
}
|
||||
end
|
||||
|
||||
def usage_cycle(user, cycle, owned_site_ids, today) do
|
||||
user = Users.with_subscription(user)
|
||||
last_bill_date = user.subscription.last_bill_date
|
||||
|
||||
normalized_last_bill_date =
|
||||
Date.shift(last_bill_date, month: Timex.diff(today, last_bill_date, :months))
|
||||
|
||||
date_range =
|
||||
case cycle do
|
||||
:current_cycle ->
|
||||
Date.range(
|
||||
normalized_last_bill_date,
|
||||
Date.shift(normalized_last_bill_date, month: 1, day: -1)
|
||||
)
|
||||
|
||||
:last_cycle ->
|
||||
Date.range(
|
||||
Date.shift(normalized_last_bill_date, month: -1),
|
||||
Date.shift(normalized_last_bill_date, day: -1)
|
||||
)
|
||||
|
||||
:penultimate_cycle ->
|
||||
Date.range(
|
||||
Date.shift(normalized_last_bill_date, month: -2),
|
||||
Date.shift(normalized_last_bill_date, day: -1, month: -1)
|
||||
)
|
||||
end
|
||||
|
||||
{pageviews, custom_events} =
|
||||
Plausible.Stats.Clickhouse.usage_breakdown(owned_site_ids, date_range)
|
||||
|
||||
%{
|
||||
date_range: date_range,
|
||||
pageviews: pageviews,
|
||||
custom_events: custom_events,
|
||||
total: pageviews + custom_events
|
||||
}
|
||||
end
|
||||
|
||||
@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 (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.
|
||||
|
||||
### Options
|
||||
|
||||
* `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
|
||||
|
||||
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),
|
||||
select: %{email: u.email}
|
||||
|
||||
invitations_q =
|
||||
from i in Plausible.Auth.Invitation,
|
||||
where: i.site_id in ^site_ids and i.role != :owner,
|
||||
select: %{email: i.email}
|
||||
|
||||
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
|
||||
site_scoped_feature_usage
|
||||
end
|
||||
end
|
||||
|
||||
def features_usage(nil, site_ids) when is_list(site_ids) do
|
||||
props_usage_q =
|
||||
from s in Site,
|
||||
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||
|
||||
revenue_goals_usage_q =
|
||||
from g in Plausible.Goal,
|
||||
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
||||
|
||||
queries =
|
||||
on_ee do
|
||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||
|
||||
[
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.Funnels, funnels_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
else
|
||||
[
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
end
|
||||
|
||||
Enum.reduce(queries, [], fn {feature, query}, acc ->
|
||||
if Plausible.Repo.exists?(query), do: acc ++ [feature], else: acc
|
||||
end)
|
||||
end
|
||||
end
|
@ -16,8 +16,13 @@ defmodule Plausible.Billing.SiteLocker do
|
||||
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
|
||||
# TODO: Use team version once we start switching writes.
|
||||
case Plausible.Billing.check_needs_to_upgrade(user) do
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
case Plausible.Teams.Billing.check_needs_to_upgrade(team) do
|
||||
{:needs_to_upgrade, :grace_period_ended} ->
|
||||
set_lock_status_for(user, true)
|
||||
|
||||
@ -67,8 +72,14 @@ defmodule Plausible.Billing.SiteLocker do
|
||||
|
||||
@spec send_grace_period_end_email(Plausible.Auth.User.t()) :: Plausible.Mailer.result()
|
||||
def send_grace_period_end_email(user) do
|
||||
usage = Plausible.Teams.Adapter.Read.Billing.monthly_pageview_usage(user)
|
||||
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.last_cycle.total)
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
usage = Plausible.Teams.Billing.monthly_pageview_usage(team)
|
||||
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.last_cycle.total)
|
||||
|
||||
user
|
||||
|> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan)
|
||||
|
@ -13,11 +13,11 @@ defmodule Plausible.Plugins.API.Capabilities do
|
||||
|
||||
features =
|
||||
if site do
|
||||
site = Plausible.Repo.preload(site, :owner)
|
||||
site = Plausible.Repo.preload(site, :team)
|
||||
|
||||
Feature.list()
|
||||
|> Enum.map(fn mod ->
|
||||
result = mod.check_availability(site.owner)
|
||||
result = mod.check_availability(site.team)
|
||||
feature = mod |> Module.split() |> List.last()
|
||||
{feature, result == :ok}
|
||||
end)
|
||||
|
@ -131,13 +131,10 @@ defmodule Plausible.SiteAdmin do
|
||||
{:error, "Please select at least one site from the list"}
|
||||
end
|
||||
|
||||
defp transfer_ownership_direct(conn, sites, %{"email" => email}) do
|
||||
current_user = conn.assigns.current_user
|
||||
|
||||
defp transfer_ownership_direct(_conn, sites, %{"email" => email}) do
|
||||
with {:ok, new_owner} <- Plausible.Auth.get_user_by(email: email),
|
||||
{:ok, _} <-
|
||||
Plausible.Site.Memberships.bulk_transfer_ownership_direct(
|
||||
current_user,
|
||||
sites,
|
||||
new_owner
|
||||
) do
|
||||
|
@ -19,7 +19,7 @@ defmodule Plausible.Site.Memberships do
|
||||
defdelegate bulk_create_invitation(sites, inviter, invitee_email, role, opts),
|
||||
to: Memberships.CreateInvitation
|
||||
|
||||
defdelegate bulk_transfer_ownership_direct(current_user, sites, new_owner),
|
||||
defdelegate bulk_transfer_ownership_direct(sites, new_owner),
|
||||
to: Memberships.AcceptInvitation
|
||||
|
||||
@spec any?(Auth.User.t()) :: boolean()
|
||||
@ -37,37 +37,4 @@ 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
|
||||
pending_ownership_invitation_q(email)
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
@spec any_or_pending?(Plausible.Auth.User.t()) :: boolean()
|
||||
def any_or_pending?(user) do
|
||||
invitation_query =
|
||||
from(i in Plausible.Auth.Invitation,
|
||||
where: i.email == ^user.email,
|
||||
select: 1
|
||||
)
|
||||
|
||||
from(sm in Plausible.Site.Membership,
|
||||
where: sm.user_id == ^user.id or exists(invitation_query),
|
||||
select: 1
|
||||
)
|
||||
|> 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
|
||||
|
@ -35,12 +35,12 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
|
||||
| Ecto.Changeset.t()
|
||||
| :no_plan
|
||||
|
||||
@spec bulk_transfer_ownership_direct(Auth.User.t(), [Site.t()], Auth.User.t()) ::
|
||||
@spec bulk_transfer_ownership_direct([Site.t()], Auth.User.t()) ::
|
||||
{:ok, [Site.Membership.t()]} | {:error, transfer_error()}
|
||||
def bulk_transfer_ownership_direct(current_user, sites, new_owner) do
|
||||
def bulk_transfer_ownership_direct(sites, new_owner) do
|
||||
Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
case transfer_ownership(current_user, site, new_owner) do
|
||||
case transfer_ownership(site, new_owner) do
|
||||
{:ok, membership} ->
|
||||
membership
|
||||
|
||||
@ -63,15 +63,17 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
|
||||
end
|
||||
end
|
||||
|
||||
defp transfer_ownership(current_user, site, new_owner) do
|
||||
defp transfer_ownership(site, new_owner) do
|
||||
site = Repo.preload(site, :team)
|
||||
|
||||
with :ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.ensure_transfer_valid(
|
||||
current_user,
|
||||
site,
|
||||
Plausible.Teams.Invitations.ensure_transfer_valid(
|
||||
site.team,
|
||||
new_owner,
|
||||
:owner
|
||||
),
|
||||
:ok <- Plausible.Teams.Adapter.Read.Ownership.ensure_can_take_ownership(site, new_owner) do
|
||||
{:ok, new_team} = Plausible.Teams.get_or_create(new_owner),
|
||||
:ok <- Plausible.Teams.Invitations.ensure_can_take_ownership(site, new_team) do
|
||||
membership = get_or_create_owner_membership(site, new_owner)
|
||||
|
||||
multi = add_and_transfer_ownership(site, membership, new_owner)
|
||||
@ -92,16 +94,16 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do
|
||||
|
||||
defp do_accept_ownership_transfer(invitation, user) do
|
||||
membership = get_or_create_membership(invitation, user)
|
||||
site = invitation.site
|
||||
site = Repo.preload(invitation.site, :team)
|
||||
|
||||
with :ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.ensure_transfer_valid(
|
||||
user,
|
||||
site,
|
||||
Plausible.Teams.Invitations.ensure_transfer_valid(
|
||||
site.team,
|
||||
user,
|
||||
:owner
|
||||
),
|
||||
:ok <- Plausible.Teams.Adapter.Read.Ownership.ensure_can_take_ownership(site, user) do
|
||||
{:ok, team} = Plausible.Teams.get_or_create(user),
|
||||
:ok <- Plausible.Teams.Invitations.ensure_can_take_ownership(site, team) do
|
||||
site
|
||||
|> add_and_transfer_ownership(membership, user)
|
||||
|> Multi.delete(:invitation, invitation)
|
||||
|
@ -5,9 +5,9 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
"""
|
||||
|
||||
alias Plausible.Auth.{User, Invitation}
|
||||
alias Plausible.{Site, Sites, Site.Membership}
|
||||
alias Plausible.Billing.Quota
|
||||
import Ecto.Query
|
||||
alias Plausible.Site
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Teams
|
||||
use Plausible
|
||||
|
||||
@type invite_error() ::
|
||||
@ -32,7 +32,7 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
as an ownership transfer and requires the inviter to be the owner of the site.
|
||||
"""
|
||||
def create_invitation(site, inviter, invitee_email, role) do
|
||||
Plausible.Repo.transaction(fn ->
|
||||
Repo.transaction(fn ->
|
||||
do_invite(site, inviter, invitee_email, role)
|
||||
end)
|
||||
end
|
||||
@ -40,7 +40,7 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
@spec bulk_create_invitation([Site.t()], User.t(), String.t(), atom(), Keyword.t()) ::
|
||||
{:ok, [Invitation.t()]} | {:error, invite_error()}
|
||||
def bulk_create_invitation(sites, inviter, invitee_email, role, opts \\ []) do
|
||||
Plausible.Repo.transaction(fn ->
|
||||
Repo.transaction(fn ->
|
||||
for site <- sites do
|
||||
do_invite(site, inviter, invitee_email, role, opts)
|
||||
end
|
||||
@ -50,125 +50,56 @@ defmodule Plausible.Site.Memberships.CreateInvitation do
|
||||
defp do_invite(site, inviter, invitee_email, role, opts \\ []) do
|
||||
attrs = %{email: invitee_email, role: role, site_id: site.id, inviter_id: inviter.id}
|
||||
|
||||
with site <- Plausible.Repo.preload(site, :owner),
|
||||
with site <- Repo.preload(site, [:owner, :team]),
|
||||
:ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.check_invitation_permissions(
|
||||
Teams.Invitations.check_invitation_permissions(
|
||||
site,
|
||||
inviter,
|
||||
role,
|
||||
opts
|
||||
),
|
||||
:ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.check_team_member_limit(
|
||||
inviter,
|
||||
site,
|
||||
Teams.Invitations.check_team_member_limit(
|
||||
site.team,
|
||||
role,
|
||||
invitee_email
|
||||
),
|
||||
invitee = Plausible.Auth.find_user_by(email: invitee_email),
|
||||
:ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.ensure_transfer_valid(
|
||||
inviter,
|
||||
site,
|
||||
Teams.Invitations.ensure_transfer_valid(
|
||||
site.team,
|
||||
invitee,
|
||||
role
|
||||
),
|
||||
:ok <-
|
||||
Plausible.Teams.Adapter.Read.Invitations.ensure_new_membership(
|
||||
inviter,
|
||||
Teams.Invitations.ensure_new_membership(
|
||||
site,
|
||||
invitee,
|
||||
role
|
||||
),
|
||||
%Ecto.Changeset{} = changeset <- Invitation.new(attrs),
|
||||
{:ok, invitation} <- Plausible.Repo.insert(changeset) do
|
||||
Plausible.Teams.Invitations.invite_sync(site, invitation)
|
||||
{:ok, invitation} <- Repo.insert(changeset) do
|
||||
Teams.Invitations.invite_sync(site, invitation)
|
||||
|
||||
Plausible.Teams.Adapter.Read.Invitations.send_invitation_email(inviter, invitation, invitee)
|
||||
send_invitation_email(inviter, invitation, invitee)
|
||||
|
||||
invitation
|
||||
else
|
||||
{:error, cause} -> Plausible.Repo.rollback(cause)
|
||||
{:error, cause} -> Repo.rollback(cause)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def check_invitation_permissions(site, inviter, requested_role, opts) do
|
||||
check_permissions? = Keyword.get(opts, :check_permissions, true)
|
||||
|
||||
if check_permissions? do
|
||||
required_roles = if requested_role == :owner, do: [:owner], else: [:admin, :owner]
|
||||
|
||||
membership_query =
|
||||
from(m in Membership,
|
||||
where: m.user_id == ^inviter.id and m.site_id == ^site.id and m.role in ^required_roles
|
||||
)
|
||||
|
||||
if Plausible.Repo.exists?(membership_query), do: :ok, else: {:error, :forbidden}
|
||||
defp send_invitation_email(inviter, invitation, invitee) do
|
||||
if invitation.role == :owner do
|
||||
Teams.SiteTransfer
|
||||
|> Repo.get_by!(transfer_id: invitation.invitation_id, initiator_id: inviter.id)
|
||||
|> Repo.preload([:site, :initiator])
|
||||
|> Teams.Invitations.send_invitation_email(invitee)
|
||||
else
|
||||
:ok
|
||||
Teams.GuestInvitation
|
||||
|> Repo.get_by!(invitation_id: invitation.invitation_id)
|
||||
|> Repo.preload([:site, team_invitation: :inviter])
|
||||
|> Teams.Invitations.send_invitation_email(invitee)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def send_invitation_email(invitation, invitee) do
|
||||
invitation = Plausible.Repo.preload(invitation, [:site, :inviter])
|
||||
|
||||
email =
|
||||
case {invitee, invitation.role} do
|
||||
{invitee, :owner} ->
|
||||
PlausibleWeb.Email.ownership_transfer_request(
|
||||
invitation.email,
|
||||
invitation.invitation_id,
|
||||
invitation.site,
|
||||
invitation.inviter,
|
||||
invitee
|
||||
)
|
||||
|
||||
{nil, _role} ->
|
||||
PlausibleWeb.Email.new_user_invitation(
|
||||
invitation.email,
|
||||
invitation.invitation_id,
|
||||
invitation.site,
|
||||
invitation.inviter
|
||||
)
|
||||
|
||||
{%User{}, _role} ->
|
||||
PlausibleWeb.Email.existing_user_invitation(
|
||||
invitation.email,
|
||||
invitation.site,
|
||||
invitation.inviter
|
||||
)
|
||||
end
|
||||
|
||||
Plausible.Mailer.send(email)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def ensure_new_membership(_site, _invitee, :owner) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def ensure_new_membership(site, invitee, _role) do
|
||||
if invitee && Sites.is_member?(invitee.id, site) do
|
||||
{:error, :already_a_member}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def check_team_member_limit(_site, :owner, _invitee_email) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def 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])
|
||||
|
||||
if Quota.below_limit?(usage, limit),
|
||||
do: :ok,
|
||||
else: {:error, {:over_limit, limit}}
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,6 @@ defmodule Plausible.Site.Memberships.Invitations do
|
||||
|
||||
import Ecto.Query, only: [from: 2]
|
||||
|
||||
alias Plausible.Site
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Billing.Feature
|
||||
@ -48,45 +47,4 @@ defmodule Plausible.Site.Memberships.Invitations do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec ensure_transfer_valid(Site.t(), Auth.User.t() | nil, Site.Membership.role()) ::
|
||||
:ok | {:error, :transfer_to_self}
|
||||
def ensure_transfer_valid(%Site{} = site, %Auth.User{} = new_owner, :owner) do
|
||||
if Plausible.Sites.role(new_owner.id, site) == :owner do
|
||||
{:error, :transfer_to_self}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_transfer_valid(_site, _invitee, _role) do
|
||||
:ok
|
||||
end
|
||||
|
||||
on_ee do
|
||||
alias Plausible.Billing.Quota
|
||||
|
||||
@spec ensure_can_take_ownership(Site.t(), Auth.User.t()) ::
|
||||
:ok | {:error, Quota.Limits.over_limits_error() | :no_plan}
|
||||
def ensure_can_take_ownership(site, new_owner) do
|
||||
site = Repo.preload(site, :owner)
|
||||
new_owner = Plausible.Users.with_subscription(new_owner)
|
||||
plan = Plausible.Billing.Plans.get_subscription_plan(new_owner.subscription)
|
||||
|
||||
active_subscription? = Plausible.Billing.Subscriptions.active?(new_owner.subscription)
|
||||
|
||||
if active_subscription? && plan != :free_10k do
|
||||
new_owner
|
||||
|> Quota.Usage.usage(pending_ownership_site_ids: [site.id])
|
||||
|> Quota.ensure_within_plan_limits(plan)
|
||||
else
|
||||
{:error, :no_plan}
|
||||
end
|
||||
end
|
||||
else
|
||||
@spec ensure_can_take_ownership(Site.t(), Auth.User.t()) :: :ok
|
||||
def ensure_can_take_ownership(_site, _new_owner) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ defmodule Plausible.Sites do
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Site
|
||||
alias Plausible.Teams
|
||||
alias Plausible.Site.SharedLink
|
||||
|
||||
require Plausible.Site.UserPreference
|
||||
@ -55,7 +56,7 @@ defmodule Plausible.Sites do
|
||||
|
||||
@spec set_option(Auth.User.t(), Site.t(), atom(), any()) :: Site.UserPreference.t()
|
||||
def set_option(user, site, option, value) when option in Site.UserPreference.options() do
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(user, site.domain)
|
||||
Plausible.Sites.get_for_user!(user, site.domain)
|
||||
|
||||
user
|
||||
|> Site.UserPreference.changeset(site, %{option => value})
|
||||
@ -68,10 +69,85 @@ defmodule Plausible.Sites do
|
||||
)
|
||||
end
|
||||
|
||||
defdelegate list(user, pagination_params, opts \\ []), to: Plausible.Teams.Adapter.Read.Sites
|
||||
defdelegate list(user, pagination_params, opts \\ []), to: Plausible.Teams.Sites
|
||||
|
||||
defdelegate list_with_invitations(user, pagination_params, opts \\ []),
|
||||
to: Plausible.Teams.Adapter.Read.Sites
|
||||
to: Plausible.Teams.Sites
|
||||
|
||||
def list_people(site) do
|
||||
owner_membership =
|
||||
from(
|
||||
tm in Teams.Membership,
|
||||
where: tm.team_id == ^site.team_id,
|
||||
where: tm.role == :owner,
|
||||
select: %Plausible.Site.Membership{
|
||||
user_id: tm.user_id,
|
||||
role: tm.role
|
||||
}
|
||||
)
|
||||
|> Repo.one!()
|
||||
|
||||
memberships =
|
||||
from(
|
||||
gm in Teams.GuestMembership,
|
||||
inner_join: tm in assoc(gm, :team_membership),
|
||||
where: gm.site_id == ^site.id,
|
||||
select: %Plausible.Site.Membership{
|
||||
user_id: tm.user_id,
|
||||
role:
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? = 'editor' THEN 'admin'
|
||||
ELSE ?
|
||||
END
|
||||
""",
|
||||
gm.role,
|
||||
gm.role
|
||||
)
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
memberships = Repo.preload([owner_membership | memberships], :user)
|
||||
|
||||
invitations =
|
||||
from(
|
||||
gi in Teams.GuestInvitation,
|
||||
inner_join: ti in assoc(gi, :team_invitation),
|
||||
where: gi.site_id == ^site.id,
|
||||
select: %Plausible.Auth.Invitation{
|
||||
invitation_id: gi.invitation_id,
|
||||
email: ti.email,
|
||||
role:
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? = 'editor' THEN 'admin'
|
||||
ELSE ?
|
||||
END
|
||||
""",
|
||||
gi.role,
|
||||
gi.role
|
||||
)
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
site_transfers =
|
||||
from(
|
||||
st in Teams.SiteTransfer,
|
||||
where: st.site_id == ^site.id,
|
||||
select: %Plausible.Auth.Invitation{
|
||||
invitation_id: st.transfer_id,
|
||||
email: st.email,
|
||||
role: :owner
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
%{memberships: memberships, invitations: site_transfers ++ invitations}
|
||||
end
|
||||
|
||||
@spec for_user_query(Auth.User.t()) :: Ecto.Query.t()
|
||||
def for_user_query(user) do
|
||||
@ -83,42 +159,46 @@ defmodule Plausible.Sites do
|
||||
end
|
||||
|
||||
def create(user, params) do
|
||||
with :ok <- Plausible.Teams.Adapter.Read.Billing.ensure_can_add_new_site(user) do
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.put(:site_changeset, Site.new(params))
|
||||
|> Ecto.Multi.run(:create_team, fn _repo, _context ->
|
||||
Plausible.Teams.get_or_create(user)
|
||||
end)
|
||||
|> Ecto.Multi.run(:clear_changed_from, fn
|
||||
_repo, %{site_changeset: %{changes: %{domain: domain}}} ->
|
||||
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, domain, [:owner]) do
|
||||
%Site{domain_changed_from: ^domain} = site ->
|
||||
site
|
||||
|> Ecto.Changeset.change()
|
||||
|> Ecto.Changeset.put_change(:domain_changed_from, nil)
|
||||
|> Ecto.Changeset.put_change(:domain_changed_at, nil)
|
||||
|> Repo.update()
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.put(:site_changeset, Site.new(params))
|
||||
|> Ecto.Multi.run(:create_team, fn _repo, _context ->
|
||||
Plausible.Teams.get_or_create(user)
|
||||
end)
|
||||
|> Ecto.Multi.run(:ensure_can_add_new_site, fn _repo, %{create_team: team} ->
|
||||
case Plausible.Teams.Billing.ensure_can_add_new_site(team) do
|
||||
:ok -> {:ok, :proceed}
|
||||
error -> error
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run(:clear_changed_from, fn
|
||||
_repo, %{site_changeset: %{changes: %{domain: domain}}} ->
|
||||
case Plausible.Sites.get_for_user(user, domain, [:owner]) do
|
||||
%Site{domain_changed_from: ^domain} = site ->
|
||||
site
|
||||
|> Ecto.Changeset.change()
|
||||
|> Ecto.Changeset.put_change(:domain_changed_from, nil)
|
||||
|> Ecto.Changeset.put_change(:domain_changed_at, nil)
|
||||
|> Repo.update()
|
||||
|
||||
_ ->
|
||||
{:ok, :ignore}
|
||||
end
|
||||
_ ->
|
||||
{:ok, :ignore}
|
||||
end
|
||||
|
||||
_repo, _context ->
|
||||
{:ok, :ignore}
|
||||
end)
|
||||
|> Ecto.Multi.insert(:site, fn %{site_changeset: site, create_team: team} ->
|
||||
Ecto.Changeset.put_assoc(site, :team, team)
|
||||
end)
|
||||
|> Ecto.Multi.insert(:site_membership, fn %{site: site} ->
|
||||
Site.Membership.new(site, user)
|
||||
end)
|
||||
|> maybe_start_trial(user)
|
||||
|> Ecto.Multi.run(:sync_team, fn _repo, %{user: user} ->
|
||||
Plausible.Teams.sync_team(user)
|
||||
{:ok, nil}
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
_repo, _context ->
|
||||
{:ok, :ignore}
|
||||
end)
|
||||
|> Ecto.Multi.insert(:site, fn %{site_changeset: site, create_team: team} ->
|
||||
Ecto.Changeset.put_assoc(site, :team, team)
|
||||
end)
|
||||
|> Ecto.Multi.insert(:site_membership, fn %{site: site} ->
|
||||
Site.Membership.new(site, user)
|
||||
end)
|
||||
|> maybe_start_trial(user)
|
||||
|> Ecto.Multi.run(:sync_team, fn _repo, %{user: user} ->
|
||||
Plausible.Teams.sync_team(user)
|
||||
{:ok, nil}
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
defp maybe_start_trial(multi, user) do
|
||||
@ -231,6 +311,55 @@ defmodule Plausible.Sites do
|
||||
locked
|
||||
end
|
||||
|
||||
def get_for_user!(user, domain, roles \\ [:owner, :admin, :viewer]) do
|
||||
roles = translate_roles(roles)
|
||||
|
||||
site =
|
||||
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
|
||||
get_by_domain!(domain)
|
||||
else
|
||||
user.id
|
||||
|> get_for_user_query(domain, List.delete(roles, :super_admin))
|
||||
|> Repo.one!()
|
||||
end
|
||||
|
||||
Repo.preload(site, :team)
|
||||
end
|
||||
|
||||
def get_for_user(user, domain, roles \\ [:owner, :admin, :viewer]) do
|
||||
roles = translate_roles(roles)
|
||||
|
||||
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
|
||||
get_by_domain(domain)
|
||||
else
|
||||
user.id
|
||||
|> get_for_user_query(domain, List.delete(roles, :super_admin))
|
||||
|> Repo.one()
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_roles(roles) do
|
||||
Enum.map(roles, fn
|
||||
:admin -> :editor
|
||||
role -> role
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_for_user_query(user_id, domain, roles) do
|
||||
roles = Enum.map(roles, &to_string/1)
|
||||
|
||||
from(s in Plausible.Site,
|
||||
join: t in assoc(s, :team),
|
||||
join: tm in assoc(t, :team_memberships),
|
||||
left_join: gm in assoc(tm, :guest_memberships),
|
||||
where: tm.user_id == ^user_id,
|
||||
where: coalesce(gm.role, tm.role) in ^roles,
|
||||
where: s.domain == ^domain or s.domain_changed_from == ^domain,
|
||||
where: is_nil(gm.id) or gm.site_id == s.id,
|
||||
select: s
|
||||
)
|
||||
end
|
||||
|
||||
def role(user_id, site) do
|
||||
Repo.one(
|
||||
from(sm in Site.Membership,
|
||||
|
@ -9,6 +9,15 @@ defmodule Plausible.Teams do
|
||||
alias Plausible.Repo
|
||||
use Plausible
|
||||
|
||||
@spec get_owner(Teams.Team.t()) ::
|
||||
{:ok, Plausible.Auth.User.t()} | {:error, :no_owner | :multiple_owners}
|
||||
def get_owner(team) do
|
||||
case Repo.preload(team, :owner).owner do
|
||||
nil -> {:error, :no_owner}
|
||||
owner_user -> {:ok, owner_user}
|
||||
end
|
||||
end
|
||||
|
||||
@spec on_trial?(Teams.Team.t() | nil) :: boolean()
|
||||
on_ee do
|
||||
def on_trial?(nil), do: false
|
||||
@ -29,10 +38,6 @@ defmodule Plausible.Teams do
|
||||
Date.diff(team.trial_expiry_date, Date.utc_today())
|
||||
end
|
||||
|
||||
def read_team_schemas?(user) do
|
||||
FunWithFlags.enabled?(:read_team_schemas, for: user)
|
||||
end
|
||||
|
||||
def with_subscription(team) do
|
||||
Repo.preload(team, subscription: last_subscription_query())
|
||||
end
|
||||
@ -77,6 +82,12 @@ defmodule Plausible.Teams do
|
||||
)
|
||||
end
|
||||
|
||||
def has_active_sites?(team) do
|
||||
team
|
||||
|> owned_sites()
|
||||
|> Enum.any?(&Plausible.Sites.has_stats?/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create (when necessary) and load team relation for provided site.
|
||||
|
||||
|
@ -1,48 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter do
|
||||
@moduledoc """
|
||||
Commonly used teams-transition functions
|
||||
"""
|
||||
alias Plausible.Teams
|
||||
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
alias Plausible.Teams
|
||||
import Teams.Adapter
|
||||
end
|
||||
end
|
||||
|
||||
def user_or_team(user) do
|
||||
switch(user,
|
||||
team_fn: &Function.identity/1,
|
||||
user_fn: &Function.identity/1
|
||||
)
|
||||
end
|
||||
|
||||
def switch(switch_on, opts \\ [])
|
||||
|
||||
def switch(%Plausible.Auth.User{} = user, opts) do
|
||||
team_fn = Keyword.fetch!(opts, :team_fn)
|
||||
user_fn = Keyword.fetch!(opts, :user_fn)
|
||||
|
||||
if Teams.read_team_schemas?(user) do
|
||||
team =
|
||||
case Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
{:error, _} -> nil
|
||||
end
|
||||
|
||||
team = Plausible.Teams.with_subscription(team)
|
||||
|
||||
team_fn.(team)
|
||||
else
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
user_fn.(user)
|
||||
end
|
||||
end
|
||||
|
||||
def switch(team_or_nil, opts) do
|
||||
team_fn = Keyword.fetch!(opts, :team_fn)
|
||||
team = Plausible.Teams.with_subscription(team_or_nil)
|
||||
team_fn.(team)
|
||||
end
|
||||
end
|
@ -1,190 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter.Read.Billing do
|
||||
@moduledoc """
|
||||
Transition adapter for new schema reads
|
||||
"""
|
||||
use Plausible.Teams.Adapter
|
||||
|
||||
def quota_usage(user, opts \\ []) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.quota_usage(&1, opts),
|
||||
user_fn: &Plausible.Billing.Quota.Usage.usage(&1, opts)
|
||||
)
|
||||
end
|
||||
|
||||
def allow_next_upgrade_override?(user) do
|
||||
switch(user,
|
||||
team_fn: &(&1 && &1.allow_next_upgrade_override),
|
||||
user_fn: & &1.allow_next_upgrade_override
|
||||
)
|
||||
end
|
||||
|
||||
def change_plan(user, new_plan_id) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.change_plan(&1, new_plan_id),
|
||||
user_fn: &Plausible.Billing.change_plan(&1, new_plan_id)
|
||||
)
|
||||
end
|
||||
|
||||
def enterprise_configured?(nil), do: false
|
||||
|
||||
def enterprise_configured?(user) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.enterprise_configured?/1,
|
||||
user_fn: &Plausible.Auth.enterprise_configured?/1
|
||||
)
|
||||
end
|
||||
|
||||
def latest_enterprise_plan_with_prices(user, customer_ip) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.latest_enterprise_plan_with_price(&1, customer_ip),
|
||||
user_fn: &Plausible.Billing.Plans.latest_enterprise_plan_with_price(&1, customer_ip)
|
||||
)
|
||||
end
|
||||
|
||||
def has_active_subscription?(user) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.has_active_subscription?/1,
|
||||
user_fn: &Plausible.Billing.has_active_subscription?/1
|
||||
)
|
||||
end
|
||||
|
||||
def active_subscription_for(user) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.Billing.active_subscription_for/1,
|
||||
user_fn: &Plausible.Billing.active_subscription_for/1
|
||||
)
|
||||
end
|
||||
|
||||
def get_subscription(user) do
|
||||
case user_or_team(user) do
|
||||
%{subscription: subscription} -> subscription
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def team_member_limit(user) do
|
||||
switch(user,
|
||||
team_fn: &Teams.Billing.team_member_limit/1,
|
||||
user_fn: &Plausible.Billing.Quota.Limits.team_member_limit/1
|
||||
)
|
||||
end
|
||||
|
||||
def team_member_usage(user, opts \\ []) do
|
||||
switch(user,
|
||||
team_fn: &Teams.Billing.team_member_usage(&1, opts),
|
||||
user_fn: &Plausible.Billing.Quota.Usage.team_member_usage(&1, opts)
|
||||
)
|
||||
end
|
||||
|
||||
def monthly_pageview_limit(user) do
|
||||
switch(user,
|
||||
team_fn: &Teams.Billing.monthly_pageview_limit/1,
|
||||
user_fn: &Plausible.Billing.Quota.Limits.monthly_pageview_limit/1
|
||||
)
|
||||
end
|
||||
|
||||
def monthly_pageview_usage(user, site_ids \\ nil) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: &Teams.Billing.monthly_pageview_usage(&1, site_ids),
|
||||
user_fn: &Plausible.Billing.Quota.Usage.monthly_pageview_usage(&1, site_ids)
|
||||
)
|
||||
end
|
||||
|
||||
def check_needs_to_upgrade(user) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: &Teams.Billing.check_needs_to_upgrade/1,
|
||||
user_fn: &Plausible.Billing.check_needs_to_upgrade/1
|
||||
)
|
||||
end
|
||||
|
||||
def site_limit(user) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: &Teams.Billing.site_limit/1,
|
||||
user_fn: &Plausible.Billing.Quota.Limits.site_limit/1
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_can_add_new_site(user) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: &Teams.Billing.ensure_can_add_new_site/1,
|
||||
user_fn: &Plausible.Billing.Quota.ensure_can_add_new_site/1
|
||||
)
|
||||
end
|
||||
|
||||
def site_usage(user) do
|
||||
switch(user,
|
||||
team_fn: &Teams.Billing.site_usage/1,
|
||||
user_fn: &Plausible.Billing.Quota.Usage.site_usage/1
|
||||
)
|
||||
end
|
||||
|
||||
use Plausible
|
||||
|
||||
on_ee do
|
||||
def check_feature_availability_for_stats_api(user) do
|
||||
{unlimited_trial?, subscription?} =
|
||||
switch(user,
|
||||
team_fn: fn team ->
|
||||
team = Plausible.Teams.with_subscription(team)
|
||||
unlimited_trial? = is_nil(team) or is_nil(team.trial_expiry_date)
|
||||
|
||||
subscription? =
|
||||
not is_nil(team) and Plausible.Billing.Subscriptions.active?(team.subscription)
|
||||
|
||||
{unlimited_trial?, subscription?}
|
||||
end,
|
||||
user_fn: fn user ->
|
||||
user = Plausible.Users.with_subscription(user)
|
||||
unlimited_trial? = is_nil(user.trial_expiry_date)
|
||||
subscription? = Plausible.Billing.Subscriptions.active?(user.subscription)
|
||||
|
||||
{unlimited_trial?, subscription?}
|
||||
end
|
||||
)
|
||||
|
||||
pre_business_tier_account? =
|
||||
NaiveDateTime.before?(user.inserted_at, Plausible.Billing.Plans.business_tier_launch())
|
||||
|
||||
cond do
|
||||
!subscription? && unlimited_trial? && pre_business_tier_account? ->
|
||||
:ok
|
||||
|
||||
!subscription? && unlimited_trial? && !pre_business_tier_account? ->
|
||||
{:error, :upgrade_required}
|
||||
|
||||
true ->
|
||||
check_feature_availability(Plausible.Billing.Feature.StatsAPI, user)
|
||||
end
|
||||
end
|
||||
else
|
||||
def check_feature_availability_for_stats_api(_user), do: :ok
|
||||
end
|
||||
|
||||
def check_feature_availability(feature, user) do
|
||||
switch(user,
|
||||
team_fn: fn team_or_nil ->
|
||||
cond do
|
||||
feature.free?() -> :ok
|
||||
feature in Teams.Billing.allowed_features_for(team_or_nil) -> :ok
|
||||
true -> {:error, :upgrade_required}
|
||||
end
|
||||
end,
|
||||
user_fn: fn user ->
|
||||
cond do
|
||||
feature.free?() -> :ok
|
||||
feature in Plausible.Billing.Quota.Limits.allowed_features_for(user) -> :ok
|
||||
true -> {:error, :upgrade_required}
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def suggest_by_usage(user, usage_during_cycle) do
|
||||
subscription = get_subscription(user)
|
||||
Plausible.Billing.Plans.suggest_by_usage(subscription, usage_during_cycle)
|
||||
end
|
||||
end
|
@ -1,120 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter.Read.Invitations do
|
||||
@moduledoc """
|
||||
Transition adapter for new schema reads
|
||||
"""
|
||||
use Plausible
|
||||
use Plausible.Teams.Adapter
|
||||
|
||||
alias Plausible.Repo
|
||||
|
||||
def check_invitation_permissions(site, inviter, role, opts) do
|
||||
switch(
|
||||
inviter,
|
||||
team_fn: fn _ ->
|
||||
Plausible.Teams.Invitations.check_invitation_permissions(
|
||||
site,
|
||||
inviter,
|
||||
role,
|
||||
opts
|
||||
)
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Site.Memberships.CreateInvitation.check_invitation_permissions(
|
||||
site,
|
||||
inviter,
|
||||
role,
|
||||
opts
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def check_team_member_limit(inviter, site, role, invitee_email) do
|
||||
switch(
|
||||
inviter,
|
||||
team_fn: fn _ ->
|
||||
site_team = Repo.preload(site, :team).team
|
||||
|
||||
Plausible.Teams.Invitations.check_team_member_limit(
|
||||
site_team,
|
||||
role,
|
||||
invitee_email
|
||||
)
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Site.Memberships.CreateInvitation.check_team_member_limit(
|
||||
site,
|
||||
role,
|
||||
invitee_email
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_transfer_valid(current_user, site, invitee, role) do
|
||||
switch(
|
||||
current_user,
|
||||
team_fn: fn _ ->
|
||||
site_team = Repo.preload(site, :team).team
|
||||
|
||||
Plausible.Teams.Invitations.ensure_transfer_valid(
|
||||
site_team,
|
||||
invitee,
|
||||
role
|
||||
)
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Site.Memberships.Invitations.ensure_transfer_valid(
|
||||
site,
|
||||
invitee,
|
||||
role
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_new_membership(inviter, site, invitee, role) do
|
||||
switch(
|
||||
inviter,
|
||||
team_fn: fn _ ->
|
||||
Plausible.Teams.Invitations.ensure_new_membership(
|
||||
site,
|
||||
invitee,
|
||||
role
|
||||
)
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Site.Memberships.CreateInvitation.ensure_new_membership(
|
||||
site,
|
||||
invitee,
|
||||
role
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def send_invitation_email(inviter, invitation, invitee) do
|
||||
switch(
|
||||
inviter,
|
||||
team_fn: fn _ ->
|
||||
if invitation.role == :owner do
|
||||
Teams.SiteTransfer
|
||||
|> Repo.get_by!(transfer_id: invitation.invitation_id, initiator_id: inviter.id)
|
||||
|> Repo.preload([:site, :initiator])
|
||||
|> Plausible.Teams.Invitations.send_invitation_email(invitee)
|
||||
else
|
||||
Teams.GuestInvitation
|
||||
|> Repo.get_by!(invitation_id: invitation.invitation_id)
|
||||
|> Repo.preload([:site, team_invitation: :inviter])
|
||||
|> Plausible.Teams.Invitations.send_invitation_email(invitee)
|
||||
end
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Site.Memberships.CreateInvitation.send_invitation_email(
|
||||
invitation,
|
||||
invitee
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
@ -1,78 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter.Read.Ownership do
|
||||
@moduledoc """
|
||||
Transition adapter for new schema reads
|
||||
"""
|
||||
use Plausible
|
||||
use Plausible.Teams.Adapter
|
||||
alias Plausible.Site
|
||||
alias Plausible.Auth
|
||||
alias Plausible.Site.Memberships.Invitations
|
||||
|
||||
def all_pending_site_transfers(email, user) do
|
||||
switch(user,
|
||||
team_fn: fn _ -> Plausible.Teams.Memberships.all_pending_site_transfers(email) end,
|
||||
user_fn: fn _ -> Plausible.Site.Memberships.all_pending_ownerships(email) end
|
||||
)
|
||||
end
|
||||
|
||||
def get_owner(site, user) do
|
||||
switch(user,
|
||||
team_fn: fn team ->
|
||||
case Teams.Sites.get_owner(team) do
|
||||
{:ok, user} -> user
|
||||
_ -> nil
|
||||
end
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
Plausible.Repo.preload(site, :owner).owner
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_can_take_ownership(site, user) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: &Teams.Invitations.ensure_can_take_ownership(site, &1),
|
||||
user_fn: &Invitations.ensure_can_take_ownership(site, &1)
|
||||
)
|
||||
end
|
||||
|
||||
def has_sites?(user) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: fn _ -> Teams.Users.has_sites?(user, include_pending?: true) end,
|
||||
user_fn: &Site.Memberships.any_or_pending?/1
|
||||
)
|
||||
end
|
||||
|
||||
def owns_sites?(user, sites) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: fn _ -> Teams.Users.owns_sites?(user, include_pending?: true) end,
|
||||
user_fn: fn user ->
|
||||
Enum.any?(sites.entries, fn site ->
|
||||
length(site.invitations) > 0 && List.first(site.invitations).role == :owner
|
||||
end) ||
|
||||
Auth.user_owns_sites?(user)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
def check_feature_access(site, new_owner) do
|
||||
missing_features =
|
||||
Plausible.Billing.Quota.Usage.features_usage(nil, [site.id])
|
||||
|> Enum.filter(&(&1.check_availability(new_owner) != :ok))
|
||||
|
||||
if missing_features == [] do
|
||||
:ok
|
||||
else
|
||||
{:error, {:missing_features, missing_features}}
|
||||
end
|
||||
end
|
||||
else
|
||||
def check_feature_access(_site, _new_owner) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
@ -1,276 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter.Read.Sites do
|
||||
@moduledoc """
|
||||
Transition adapter for new schema reads
|
||||
"""
|
||||
|
||||
use Plausible.Teams.Adapter
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Site
|
||||
alias Plausible.Teams
|
||||
|
||||
def list(user, pagination_params, opts \\ []) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: fn _ -> Plausible.Teams.Sites.list(user, pagination_params, opts) end,
|
||||
user_fn: fn _ -> old_list(user, pagination_params, opts) end
|
||||
)
|
||||
end
|
||||
|
||||
def list_with_invitations(user, pagination_params, opts \\ []) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: fn _ ->
|
||||
Plausible.Teams.Sites.list_with_invitations(user, pagination_params, opts)
|
||||
end,
|
||||
user_fn: fn _ -> old_list_with_invitations(user, pagination_params, opts) end
|
||||
)
|
||||
end
|
||||
|
||||
defp old_list(user, pagination_params, opts) do
|
||||
domain_filter = Keyword.get(opts, :filter_by_domain)
|
||||
|
||||
from(s in Site,
|
||||
left_join: up in Site.UserPreference,
|
||||
on: up.site_id == s.id and up.user_id == ^user.id,
|
||||
inner_join: sm in assoc(s, :memberships),
|
||||
on: sm.user_id == ^user.id,
|
||||
select: %{
|
||||
s
|
||||
| pinned_at: selected_as(up.pinned_at, :pinned_at),
|
||||
entry_type:
|
||||
selected_as(
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? IS NOT NULL THEN 'pinned_site'
|
||||
ELSE 'site'
|
||||
END
|
||||
""",
|
||||
up.pinned_at
|
||||
),
|
||||
:entry_type
|
||||
)
|
||||
},
|
||||
order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain],
|
||||
preload: [memberships: sm]
|
||||
)
|
||||
|> maybe_filter_by_domain(domain_filter)
|
||||
|> Repo.paginate(pagination_params)
|
||||
end
|
||||
|
||||
defp old_list_with_invitations(user, pagination_params, opts) do
|
||||
domain_filter = Keyword.get(opts, :filter_by_domain)
|
||||
|
||||
result =
|
||||
from(s in Site,
|
||||
left_join: up in Site.UserPreference,
|
||||
on: up.site_id == s.id and up.user_id == ^user.id,
|
||||
left_join: i in assoc(s, :invitations),
|
||||
on: i.email == ^user.email,
|
||||
left_join: sm in assoc(s, :memberships),
|
||||
on: sm.user_id == ^user.id,
|
||||
where: not is_nil(sm.id) or not is_nil(i.id),
|
||||
select: %{
|
||||
s
|
||||
| pinned_at: selected_as(up.pinned_at, :pinned_at),
|
||||
entry_type:
|
||||
selected_as(
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? IS NOT NULL THEN 'invitation'
|
||||
WHEN ? IS NOT NULL THEN 'pinned_site'
|
||||
ELSE 'site'
|
||||
END
|
||||
""",
|
||||
i.id,
|
||||
up.pinned_at
|
||||
),
|
||||
:entry_type
|
||||
)
|
||||
},
|
||||
order_by: [asc: selected_as(:entry_type), desc: selected_as(:pinned_at), asc: s.domain],
|
||||
preload: [memberships: sm, invitations: i]
|
||||
)
|
||||
|> maybe_filter_by_domain(domain_filter)
|
||||
|> Repo.paginate(pagination_params)
|
||||
|
||||
# Populating `site` preload on `invitation`
|
||||
# without requesting it from database.
|
||||
# Necessary for invitation modals logic.
|
||||
entries =
|
||||
Enum.map(result.entries, fn
|
||||
%{invitations: [invitation]} = site ->
|
||||
site = %{site | invitations: [], memberships: []}
|
||||
invitation = %{invitation | site: site}
|
||||
%{site | invitations: [invitation]}
|
||||
|
||||
site ->
|
||||
site
|
||||
end)
|
||||
|
||||
%{result | entries: entries}
|
||||
end
|
||||
|
||||
def list_people(site, user) do
|
||||
if Plausible.Teams.read_team_schemas?(user) do
|
||||
owner_membership =
|
||||
from(
|
||||
tm in Teams.Membership,
|
||||
where: tm.team_id == ^site.team_id,
|
||||
where: tm.role == :owner,
|
||||
select: %Plausible.Site.Membership{
|
||||
user_id: tm.user_id,
|
||||
role: tm.role
|
||||
}
|
||||
)
|
||||
|> Repo.one!()
|
||||
|
||||
memberships =
|
||||
from(
|
||||
gm in Teams.GuestMembership,
|
||||
inner_join: tm in assoc(gm, :team_membership),
|
||||
where: gm.site_id == ^site.id,
|
||||
select: %Plausible.Site.Membership{
|
||||
user_id: tm.user_id,
|
||||
role:
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? = 'editor' THEN 'admin'
|
||||
ELSE ?
|
||||
END
|
||||
""",
|
||||
gm.role,
|
||||
gm.role
|
||||
)
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
memberships = Repo.preload([owner_membership | memberships], :user)
|
||||
|
||||
invitations =
|
||||
from(
|
||||
gi in Teams.GuestInvitation,
|
||||
inner_join: ti in assoc(gi, :team_invitation),
|
||||
where: gi.site_id == ^site.id,
|
||||
select: %Plausible.Auth.Invitation{
|
||||
invitation_id: gi.invitation_id,
|
||||
email: ti.email,
|
||||
role:
|
||||
fragment(
|
||||
"""
|
||||
CASE
|
||||
WHEN ? = 'editor' THEN 'admin'
|
||||
ELSE ?
|
||||
END
|
||||
""",
|
||||
gi.role,
|
||||
gi.role
|
||||
)
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
site_transfers =
|
||||
from(
|
||||
st in Teams.SiteTransfer,
|
||||
where: st.site_id == ^site.id,
|
||||
select: %Plausible.Auth.Invitation{
|
||||
invitation_id: st.transfer_id,
|
||||
email: st.email,
|
||||
role: :owner
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
||||
%{memberships: memberships, invitations: site_transfers ++ invitations}
|
||||
else
|
||||
site
|
||||
|> Repo.preload([:invitations, memberships: :user])
|
||||
|> Map.take([:memberships, :invitations])
|
||||
end
|
||||
end
|
||||
|
||||
def get_for_user!(user, domain, roles \\ [:owner, :admin, :viewer]) do
|
||||
{query_fn, roles} = for_user_query_and_roles(user, roles)
|
||||
|
||||
site =
|
||||
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
|
||||
Plausible.Sites.get_by_domain!(domain)
|
||||
else
|
||||
user.id
|
||||
|> query_fn.(domain, List.delete(roles, :super_admin))
|
||||
|> Repo.one!()
|
||||
end
|
||||
|
||||
Repo.preload(site, :team)
|
||||
end
|
||||
|
||||
def get_for_user(user, domain, roles \\ [:owner, :admin, :viewer]) do
|
||||
{query_fn, roles} = for_user_query_and_roles(user, roles)
|
||||
|
||||
if :super_admin in roles and Plausible.Auth.is_super_admin?(user.id) do
|
||||
Plausible.Sites.get_by_domain(domain)
|
||||
else
|
||||
user.id
|
||||
|> query_fn.(domain, List.delete(roles, :super_admin))
|
||||
|> Repo.one()
|
||||
end
|
||||
end
|
||||
|
||||
defp for_user_query_and_roles(user, roles) do
|
||||
switch(
|
||||
user,
|
||||
team_fn: fn _ ->
|
||||
translated_roles =
|
||||
Enum.map(roles, fn
|
||||
:admin -> :editor
|
||||
other -> other
|
||||
end)
|
||||
|
||||
{&new_get_for_user_query/3, translated_roles}
|
||||
end,
|
||||
user_fn: fn _ ->
|
||||
{&old_get_for_user_query/3, roles}
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
defp old_get_for_user_query(user_id, domain, roles) do
|
||||
from(s in Plausible.Site,
|
||||
join: sm in Plausible.Site.Membership,
|
||||
on: sm.site_id == s.id,
|
||||
where: sm.user_id == ^user_id,
|
||||
where: sm.role in ^roles,
|
||||
where: s.domain == ^domain or s.domain_changed_from == ^domain,
|
||||
select: s
|
||||
)
|
||||
end
|
||||
|
||||
defp new_get_for_user_query(user_id, domain, roles) do
|
||||
roles = Enum.map(roles, &to_string/1)
|
||||
|
||||
from(s in Plausible.Site,
|
||||
join: t in assoc(s, :team),
|
||||
join: tm in assoc(t, :team_memberships),
|
||||
left_join: gm in assoc(tm, :guest_memberships),
|
||||
where: tm.user_id == ^user_id,
|
||||
where: coalesce(gm.role, tm.role) in ^roles,
|
||||
where: s.domain == ^domain or s.domain_changed_from == ^domain,
|
||||
where: is_nil(gm.id) or gm.site_id == s.id,
|
||||
select: s
|
||||
)
|
||||
end
|
||||
|
||||
defp maybe_filter_by_domain(query, domain)
|
||||
when byte_size(domain) >= 1 and byte_size(domain) <= 64 do
|
||||
where(query, [s], ilike(s.domain, ^"%#{domain}%"))
|
||||
end
|
||||
|
||||
defp maybe_filter_by_domain(query, _), do: query
|
||||
end
|
@ -1,27 +0,0 @@
|
||||
defmodule Plausible.Teams.Adapter.Read.Teams do
|
||||
@moduledoc """
|
||||
Transition adapter for new schema reads
|
||||
"""
|
||||
use Plausible.Teams.Adapter
|
||||
|
||||
def trial_expiry_date(user) do
|
||||
switch(user,
|
||||
team_fn: &(&1 && &1.trial_expiry_date),
|
||||
user_fn: & &1.trial_expiry_date
|
||||
)
|
||||
end
|
||||
|
||||
def on_trial?(user) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.on_trial?/1,
|
||||
user_fn: &Plausible.Users.on_trial?/1
|
||||
)
|
||||
end
|
||||
|
||||
def trial_days_left(user) do
|
||||
switch(user,
|
||||
team_fn: &Plausible.Teams.trial_days_left/1,
|
||||
user_fn: &Plausible.Users.trial_days_left/1
|
||||
)
|
||||
end
|
||||
end
|
@ -1,10 +1,13 @@
|
||||
defmodule Plausible.Teams.Billing do
|
||||
@moduledoc false
|
||||
|
||||
use Plausible
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Billing.EnterprisePlan
|
||||
alias Plausible.Billing.Plans
|
||||
alias Plausible.Billing.Subscription
|
||||
alias Plausible.Billing.Subscriptions
|
||||
alias Plausible.Repo
|
||||
alias Plausible.Teams
|
||||
@ -18,6 +21,29 @@ defmodule Plausible.Teams.Billing do
|
||||
@limit_sites_since ~D[2021-05-05]
|
||||
@site_limit_for_trials 10
|
||||
|
||||
@type cycles_usage() :: %{cycle() => usage_cycle()}
|
||||
|
||||
@typep cycle :: :current_cycle | :last_cycle | :penultimate_cycle
|
||||
|
||||
@typep usage_cycle :: %{
|
||||
date_range: Date.Range.t(),
|
||||
pageviews: non_neg_integer(),
|
||||
custom_events: non_neg_integer(),
|
||||
total: non_neg_integer()
|
||||
}
|
||||
|
||||
@typep last_30_days_usage() :: %{:last_30_days => usage_cycle()}
|
||||
@typep monthly_pageview_usage() :: cycles_usage() | last_30_days_usage()
|
||||
|
||||
def get_subscription(nil), do: nil
|
||||
|
||||
def get_subscription(%Teams.Team{subscription: %Subscription{} = subscription}),
|
||||
do: subscription
|
||||
|
||||
def get_subscription(%Teams.Team{} = team) do
|
||||
Teams.with_subscription(team).subscription
|
||||
end
|
||||
|
||||
def change_plan(team, new_plan_id) do
|
||||
subscription = active_subscription_for(team)
|
||||
plan = Plausible.Billing.Plans.find(new_plan_id)
|
||||
@ -33,7 +59,29 @@ defmodule Plausible.Teams.Billing do
|
||||
|
||||
with :ok <-
|
||||
Plausible.Billing.Quota.ensure_within_plan_limits(usage, plan, limit_checking_opts),
|
||||
do: Plausible.Billing.do_change_plan(subscription, new_plan_id)
|
||||
do: do_change_plan(subscription, new_plan_id)
|
||||
end
|
||||
|
||||
defp do_change_plan(subscription, new_plan_id) do
|
||||
res =
|
||||
Plausible.Billing.paddle_api().update_subscription(subscription.paddle_subscription_id, %{
|
||||
plan_id: new_plan_id
|
||||
})
|
||||
|
||||
case res do
|
||||
{:ok, response} ->
|
||||
amount = :erlang.float_to_binary(response["next_payment"]["amount"] / 1, decimals: 2)
|
||||
|
||||
Subscription.changeset(subscription, %{
|
||||
paddle_plan_id: Integer.to_string(response["plan_id"]),
|
||||
next_bill_amount: amount,
|
||||
next_bill_date: response["next_payment"]["date"]
|
||||
})
|
||||
|> Repo.update()
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def enterprise_configured?(nil), do: false
|
||||
@ -65,12 +113,17 @@ defmodule Plausible.Teams.Billing do
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def active_subscription_for(nil), do: nil
|
||||
|
||||
def active_subscription_for(team) do
|
||||
team
|
||||
|> active_subscription_query()
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec check_needs_to_upgrade(Teams.Team.t() | nil) ::
|
||||
{:needs_to_upgrade, :no_trial | :no_active_subscription | :grace_period_ended}
|
||||
| :no_upgrade_needed
|
||||
def check_needs_to_upgrade(nil), do: {:needs_to_upgrade, :no_trial}
|
||||
|
||||
def check_needs_to_upgrade(team) do
|
||||
@ -97,6 +150,11 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enterprise plans are always allowed to add more sites (even when
|
||||
over limit) to avoid service disruption. Their usage is checked
|
||||
in a background job instead (see `check_usage.ex`).
|
||||
"""
|
||||
def ensure_can_add_new_site(nil) do
|
||||
:ok
|
||||
end
|
||||
@ -132,6 +190,10 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of sites the given team owns.
|
||||
"""
|
||||
@spec site_usage(Teams.Team.t()) :: non_neg_integer()
|
||||
def site_usage(nil), do: 0
|
||||
|
||||
def site_usage(team) do
|
||||
@ -169,6 +231,21 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a full usage report for the team.
|
||||
|
||||
### 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 team owner 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 quota_usage(team, opts \\ []) do
|
||||
team = Teams.with_subscription(team)
|
||||
with_features? = Keyword.get(opts, :with_features, false)
|
||||
@ -226,6 +303,31 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Queries the ClickHouse database for the monthly pageview usage. If the given team's
|
||||
subscription is `active`, `past_due`, or a `deleted` (but not yet expired), a map
|
||||
with the following structure is returned:
|
||||
|
||||
```elixir
|
||||
%{
|
||||
current_cycle: usage_cycle(),
|
||||
last_cycle: usage_cycle(),
|
||||
penultimate_cycle: usage_cycle()
|
||||
}
|
||||
```
|
||||
|
||||
In all other cases of the subscription status (or a `free_10k` subscription which
|
||||
does not have a `last_bill_date` defined) - the following structure is returned:
|
||||
|
||||
```elixir
|
||||
%{last_30_days: usage_cycle()}
|
||||
```
|
||||
|
||||
Given only a team as input, the usage is queried from across all the sites that the
|
||||
team owns. Alternatively, given an optional argument of `site_ids`, the usage from
|
||||
across all those sites is queried instead.
|
||||
"""
|
||||
@spec monthly_pageview_usage(Teams.Team.t(), list() | nil) :: monthly_pageview_usage()
|
||||
def monthly_pageview_usage(team, site_ids \\ nil)
|
||||
|
||||
def monthly_pageview_usage(team, nil) do
|
||||
@ -251,10 +353,33 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
@spec team_member_usage(Teams.Team.t(), Keyword.t()) :: non_neg_integer()
|
||||
@doc """
|
||||
Returns the total count of team members associated with the team's sites.
|
||||
|
||||
* The given team's owner is not counted as a team member.
|
||||
|
||||
* 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.
|
||||
|
||||
### Options
|
||||
|
||||
* `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(team, opts \\ [])
|
||||
def team_member_usage(nil, _), do: 0
|
||||
|
||||
def team_member_usage(team, opts) do
|
||||
{:ok, owner} = Teams.Sites.get_owner(team)
|
||||
{:ok, owner} = Teams.get_owner(team)
|
||||
exclude_emails = Keyword.get(opts, :exclude_emails, []) ++ [owner.email]
|
||||
|
||||
pending_site_ids = Keyword.get(opts, :pending_ownership_site_ids, [])
|
||||
@ -324,6 +449,17 @@ defmodule Plausible.Teams.Billing do
|
||||
}
|
||||
end
|
||||
|
||||
@spec features_usage(Teams.Team.t() | nil, list() | nil) :: [atom()]
|
||||
@doc """
|
||||
Given only a team, this function returns the features used across all the
|
||||
sites this team owns + StatsAPI if any team user has a configured Stats API key.
|
||||
|
||||
Given a team, and a list of site_ids, returns the features used by those
|
||||
sites instead + StatsAPI if any user in the team has a configured Stats API key.
|
||||
|
||||
The team can also be passed as `nil`, in which case we will never return
|
||||
Stats API as a used feature.
|
||||
"""
|
||||
def features_usage(team, site_ids \\ nil)
|
||||
|
||||
def features_usage(nil, nil), do: []
|
||||
@ -337,7 +473,7 @@ defmodule Plausible.Teams.Billing do
|
||||
site_scoped_feature_usage = features_usage(nil, owned_site_ids)
|
||||
|
||||
stats_api_used? =
|
||||
Plausible.Repo.exists?(
|
||||
Repo.exists?(
|
||||
from tm in Plausible.Teams.Membership,
|
||||
as: :team_membership,
|
||||
where: tm.team_id == ^team.id,
|
||||
@ -355,8 +491,34 @@ defmodule Plausible.Teams.Billing do
|
||||
end
|
||||
end
|
||||
|
||||
def features_usage(nil, owned_site_ids) when is_list(owned_site_ids) do
|
||||
Plausible.Billing.Quota.Usage.features_usage(nil, owned_site_ids)
|
||||
def features_usage(nil, site_ids) when is_list(site_ids) do
|
||||
props_usage_q =
|
||||
from s in Plausible.Site,
|
||||
where: s.id in ^site_ids and fragment("cardinality(?) > 0", s.allowed_event_props)
|
||||
|
||||
revenue_goals_usage_q =
|
||||
from g in Plausible.Goal,
|
||||
where: g.site_id in ^site_ids and not is_nil(g.currency)
|
||||
|
||||
queries =
|
||||
on_ee do
|
||||
funnels_usage_q = from f in "funnels", where: f.site_id in ^site_ids
|
||||
|
||||
[
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.Funnels, funnels_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
else
|
||||
[
|
||||
{Feature.Props, props_usage_q},
|
||||
{Feature.RevenueGoals, revenue_goals_usage_q}
|
||||
]
|
||||
end
|
||||
|
||||
Enum.reduce(queries, [], fn {feature, query}, acc ->
|
||||
if Repo.exists?(query), do: acc ++ [feature], else: acc
|
||||
end)
|
||||
end
|
||||
|
||||
defp query_team_member_emails(team, pending_ownership_site_ids, exclude_emails) do
|
||||
|
@ -1,6 +1,8 @@
|
||||
defmodule Plausible.Teams.Invitations do
|
||||
@moduledoc false
|
||||
|
||||
use Plausible
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Plausible.Billing
|
||||
@ -274,7 +276,7 @@ defmodule Plausible.Teams.Invitations do
|
||||
Repo.delete_all(from gm in Teams.GuestMembership, where: gm.id in ^old_guest_ids)
|
||||
:ok = Teams.Memberships.prune_guests(prior_team)
|
||||
|
||||
{:ok, prior_owner} = Teams.Sites.get_owner(prior_team)
|
||||
{:ok, prior_owner} = Teams.get_owner(prior_team)
|
||||
|
||||
{:ok, prior_owner_team_membership} = create_team_membership(team, :guest, prior_owner, now)
|
||||
|
||||
@ -312,19 +314,25 @@ defmodule Plausible.Teams.Invitations do
|
||||
:ok
|
||||
end
|
||||
|
||||
def ensure_can_take_ownership(_site, nil), do: {:error, :no_plan}
|
||||
on_ee do
|
||||
def ensure_can_take_ownership(_site, nil), do: {:error, :no_plan}
|
||||
|
||||
def ensure_can_take_ownership(site, team) do
|
||||
team = Teams.with_subscription(team)
|
||||
plan = Billing.Plans.get_subscription_plan(team.subscription)
|
||||
active_subscription? = Billing.Subscriptions.active?(team.subscription)
|
||||
def ensure_can_take_ownership(site, team) do
|
||||
team = Teams.with_subscription(team)
|
||||
plan = Billing.Plans.get_subscription_plan(team.subscription)
|
||||
active_subscription? = Billing.Subscriptions.active?(team.subscription)
|
||||
|
||||
if active_subscription? and plan != :free_10k do
|
||||
team
|
||||
|> Teams.Billing.quota_usage(pending_ownership_site_ids: [site.id])
|
||||
|> Billing.Quota.ensure_within_plan_limits(plan)
|
||||
else
|
||||
{:error, :no_plan}
|
||||
if active_subscription? and plan != :free_10k do
|
||||
team
|
||||
|> Teams.Billing.quota_usage(pending_ownership_site_ids: [site.id])
|
||||
|> Billing.Quota.ensure_within_plan_limits(plan)
|
||||
else
|
||||
{:error, :no_plan}
|
||||
end
|
||||
end
|
||||
else
|
||||
def ensure_can_take_ownership(_site, _team) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -47,23 +47,6 @@ defmodule Plausible.Teams.Sites do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_owner(Teams.Team.t()) :: {:ok, Auth.User.t()} | {:error, :no_owner | :multiple_owners}
|
||||
def get_owner(team) do
|
||||
owner_query =
|
||||
from(
|
||||
tm in Teams.Membership,
|
||||
inner_join: u in assoc(tm, :user),
|
||||
where: tm.team_id == ^team.id and tm.role == :owner,
|
||||
select: u
|
||||
)
|
||||
|
||||
case Repo.all(owner_query) do
|
||||
[owner_user] -> {:ok, owner_user}
|
||||
[] -> {:error, :no_owner}
|
||||
_ -> {:error, :multiple_owners}
|
||||
end
|
||||
end
|
||||
|
||||
@spec list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t()
|
||||
def list(user, pagination_params, opts \\ []) do
|
||||
domain_filter = Keyword.get(opts, :filter_by_domain)
|
||||
|
@ -189,7 +189,7 @@ defmodule PlausibleWeb.Components.Billing do
|
||||
</div>
|
||||
<.styled_link
|
||||
:if={
|
||||
not (Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@user) &&
|
||||
not (Plausible.Teams.Billing.enterprise_configured?(@team) &&
|
||||
Subscriptions.halted?(@subscription))
|
||||
}
|
||||
id="#upgrade-or-change-plan-link"
|
||||
|
@ -71,7 +71,7 @@ defmodule PlausibleWeb.Components.Billing.Notice do
|
||||
def premium_feature(assigns) do
|
||||
~H"""
|
||||
<.notice
|
||||
:if={@feature_mod.check_availability(@billable_user) !== :ok}
|
||||
:if={@feature_mod.check_availability(@current_team) !== :ok}
|
||||
class="rounded-t-md rounded-b-none"
|
||||
title="Notice"
|
||||
{@rest}
|
||||
|
@ -175,7 +175,7 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
|
||||
change_plan_link_text = change_plan_link_text(assigns)
|
||||
|
||||
subscription =
|
||||
Plausible.Teams.Adapter.Read.Billing.get_subscription(assigns.current_user)
|
||||
Plausible.Teams.Billing.get_subscription(assigns.current_team)
|
||||
|
||||
billing_details_expired =
|
||||
Subscription.Status.in?(subscription, [
|
||||
@ -264,22 +264,18 @@ defmodule PlausibleWeb.Components.Billing.PlanBox do
|
||||
defp check_usage_within_plan_limits(%{
|
||||
available: true,
|
||||
usage: usage,
|
||||
current_user: current_user,
|
||||
current_team: current_team,
|
||||
plan_to_render: plan
|
||||
}) do
|
||||
# At this point, the user is *not guaranteed* to have a `trial_expiry_date`,
|
||||
# because in the past we've let users upgrade without that constraint, as
|
||||
# well as transfer sites to those accounts. to these accounts we won't be
|
||||
# offering an extra pageview limit allowance margin though.
|
||||
invited_user? = is_nil(Plausible.Teams.Adapter.Read.Teams.trial_expiry_date(current_user))
|
||||
|
||||
# At this point, the user is *not guaranteed* to have a team,
|
||||
# with ongoing trial.
|
||||
trial_active_or_ended_recently? =
|
||||
not invited_user? &&
|
||||
Plausible.Teams.Adapter.Read.Teams.trial_days_left(current_user) >= -10
|
||||
not is_nil(current_team) and not is_nil(current_team.trial_expiry_date) and
|
||||
Plausible.Teams.trial_days_left(current_team) >= -10
|
||||
|
||||
limit_checking_opts =
|
||||
cond do
|
||||
Plausible.Teams.Adapter.Read.Billing.allow_next_upgrade_override?(current_user) ->
|
||||
current_team && current_team.allow_next_upgrade_override ->
|
||||
[ignore_pageview_limit: true]
|
||||
|
||||
trial_active_or_ended_recently? && plan.volume == "10k" ->
|
||||
|
@ -9,16 +9,15 @@ defmodule PlausibleWeb.BillingController do
|
||||
plug PlausibleWeb.RequireAccountPlug
|
||||
|
||||
def ping_subscription(%Plug.Conn{} = conn, _params) do
|
||||
subscribed? =
|
||||
Plausible.Teams.Adapter.Read.Billing.has_active_subscription?(conn.assigns.current_user)
|
||||
subscribed? = Plausible.Teams.Billing.has_active_subscription?(conn.assigns.current_team)
|
||||
|
||||
json(conn, %{is_subscribed: subscribed?})
|
||||
end
|
||||
|
||||
def choose_plan(conn, _params) do
|
||||
current_user = conn.assigns.current_user
|
||||
current_team = conn.assigns.current_team
|
||||
|
||||
if Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(current_user) do
|
||||
if Plausible.Teams.Billing.enterprise_configured?(current_team) do
|
||||
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|
||||
else
|
||||
render(conn, "choose_plan.html",
|
||||
@ -29,9 +28,8 @@ defmodule PlausibleWeb.BillingController do
|
||||
end
|
||||
|
||||
def upgrade_to_enterprise_plan(conn, _params) do
|
||||
current_user = conn.assigns.current_user
|
||||
current_team = conn.assigns.current_team
|
||||
subscription = Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
|
||||
subscription = Plausible.Teams.Billing.get_subscription(current_team)
|
||||
|
||||
{latest_enterprise_plan, price} =
|
||||
Plausible.Teams.Billing.latest_enterprise_plan_with_price(
|
||||
@ -72,8 +70,9 @@ defmodule PlausibleWeb.BillingController do
|
||||
end
|
||||
|
||||
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
|
||||
current_team = conn.assigns.current_team
|
||||
current_user = conn.assigns.current_user
|
||||
subscription = Plausible.Teams.Adapter.Read.Billing.active_subscription_for(current_user)
|
||||
subscription = Plausible.Teams.Billing.active_subscription_for(current_team)
|
||||
|
||||
case preview_subscription(subscription, new_plan_id) do
|
||||
{:ok, {subscription, preview_info}} ->
|
||||
@ -92,6 +91,7 @@ defmodule PlausibleWeb.BillingController do
|
||||
extra: %{
|
||||
message: msg,
|
||||
new_plan_id: new_plan_id,
|
||||
current_team: current_team.id,
|
||||
user_id: current_user.id
|
||||
}
|
||||
)
|
||||
@ -103,9 +103,9 @@ defmodule PlausibleWeb.BillingController do
|
||||
end
|
||||
|
||||
def change_plan(conn, %{"new_plan_id" => new_plan_id}) do
|
||||
current_user = conn.assigns.current_user
|
||||
current_team = conn.assigns.current_team
|
||||
|
||||
case Plausible.Teams.Adapter.Read.Billing.change_plan(current_user, new_plan_id) do
|
||||
case Plausible.Teams.Billing.change_plan(current_team, new_plan_id) do
|
||||
{:ok, _subscription} ->
|
||||
conn
|
||||
|> put_flash(:success, "Plan changed successfully")
|
||||
@ -133,7 +133,8 @@ defmodule PlausibleWeb.BillingController do
|
||||
errors: inspect(e),
|
||||
message: msg,
|
||||
new_plan_id: new_plan_id,
|
||||
user_id: current_user.id
|
||||
current_team: current_team.id,
|
||||
user_id: conn.assigns.current_user.id
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -4,6 +4,7 @@ defmodule PlausibleWeb.SettingsController do
|
||||
|
||||
alias Plausible.Auth
|
||||
alias PlausibleWeb.UserAuth
|
||||
alias Plausible.Teams
|
||||
|
||||
require Logger
|
||||
|
||||
@ -20,24 +21,23 @@ defmodule PlausibleWeb.SettingsController do
|
||||
end
|
||||
|
||||
def subscription(conn, _params) do
|
||||
current_user = conn.assigns.current_user
|
||||
subscription = Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
|
||||
current_team = conn.assigns.current_team
|
||||
subscription = Teams.Billing.get_subscription(current_team)
|
||||
|
||||
render(conn, :subscription,
|
||||
layout: {PlausibleWeb.LayoutView, :settings},
|
||||
subscription: subscription,
|
||||
pageview_limit: Plausible.Teams.Adapter.Read.Billing.monthly_pageview_limit(current_user),
|
||||
pageview_usage: Plausible.Teams.Adapter.Read.Billing.monthly_pageview_usage(current_user),
|
||||
site_usage: Plausible.Teams.Adapter.Read.Billing.site_usage(current_user),
|
||||
site_limit: Plausible.Teams.Adapter.Read.Billing.site_limit(current_user),
|
||||
team_member_limit: Plausible.Teams.Adapter.Read.Billing.team_member_limit(current_user),
|
||||
team_member_usage: Plausible.Teams.Adapter.Read.Billing.team_member_usage(current_user)
|
||||
pageview_limit: Teams.Billing.monthly_pageview_limit(subscription),
|
||||
pageview_usage: Teams.Billing.monthly_pageview_usage(current_team),
|
||||
site_usage: Teams.Billing.site_usage(current_team),
|
||||
site_limit: Teams.Billing.site_limit(current_team),
|
||||
team_member_limit: Teams.Billing.team_member_limit(current_team),
|
||||
team_member_usage: Teams.Billing.team_member_usage(current_team)
|
||||
)
|
||||
end
|
||||
|
||||
def invoices(conn, _params) do
|
||||
subscription =
|
||||
Plausible.Teams.Adapter.Read.Billing.get_subscription(conn.assigns.current_user)
|
||||
subscription = Teams.Billing.get_subscription(conn.assigns.current_team)
|
||||
|
||||
invoices = Plausible.Billing.paddle_api().get_invoices(subscription)
|
||||
render(conn, :invoices, layout: {PlausibleWeb.LayoutView, :settings}, invoices: invoices)
|
||||
|
@ -26,11 +26,11 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
def invite_member_form(conn, _params) do
|
||||
site =
|
||||
conn.assigns.current_user
|
||||
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.site.domain)
|
||||
|> Plausible.Sites.get_for_user!(conn.assigns.site.domain)
|
||||
|> Plausible.Repo.preload(:owner)
|
||||
|
||||
limit = Plausible.Teams.Adapter.Read.Billing.team_member_limit(site.owner)
|
||||
usage = Plausible.Teams.Adapter.Read.Billing.team_member_usage(site.owner)
|
||||
limit = Plausible.Teams.Billing.team_member_limit(site.team)
|
||||
usage = Plausible.Teams.Billing.team_member_usage(site.team)
|
||||
below_limit? = Plausible.Billing.Quota.below_limit?(usage, limit)
|
||||
|
||||
render(
|
||||
@ -47,7 +47,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
site_domain = conn.assigns.site.domain
|
||||
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
Plausible.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
|> Plausible.Repo.preload(:owner)
|
||||
|
||||
case Memberships.create_invitation(site, conn.assigns.current_user, email, role) do
|
||||
@ -96,7 +96,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
site_domain = conn.assigns.site.domain
|
||||
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
Plausible.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
|
||||
render(
|
||||
conn,
|
||||
@ -110,7 +110,7 @@ defmodule PlausibleWeb.Site.MembershipController do
|
||||
site_domain = conn.assigns.site.domain
|
||||
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
Plausible.Sites.get_for_user!(conn.assigns.current_user, site_domain)
|
||||
|
||||
case Memberships.create_invitation(site, conn.assigns.current_user, email, :owner) do
|
||||
{:ok, _invitation} ->
|
||||
|
@ -14,21 +14,21 @@ defmodule PlausibleWeb.SiteController do
|
||||
|
||||
def new(conn, params) do
|
||||
flow = params["flow"] || PlausibleWeb.Flows.register()
|
||||
current_user = conn.assigns[:current_user]
|
||||
current_team = conn.assigns.current_team
|
||||
|
||||
render(conn, "new.html",
|
||||
changeset: Plausible.Site.changeset(%Plausible.Site{}),
|
||||
site_limit: Plausible.Teams.Adapter.Read.Billing.site_limit(current_user),
|
||||
site_limit_exceeded?:
|
||||
Plausible.Teams.Adapter.Read.Billing.ensure_can_add_new_site(current_user) != :ok,
|
||||
site_limit: Plausible.Teams.Billing.site_limit(current_team),
|
||||
site_limit_exceeded?: Plausible.Teams.Billing.ensure_can_add_new_site(current_team) != :ok,
|
||||
form_submit_url: "/sites?flow=#{flow}",
|
||||
flow: flow
|
||||
)
|
||||
end
|
||||
|
||||
def create_site(conn, %{"site" => site_params}) do
|
||||
user = conn.assigns[:current_user]
|
||||
first_site? = Plausible.Teams.Adapter.Read.Billing.site_usage(user) == 0
|
||||
team = conn.assigns.current_team
|
||||
user = conn.assigns.current_user
|
||||
first_site? = Plausible.Teams.Billing.site_usage(team) == 0
|
||||
flow = conn.params["flow"]
|
||||
|
||||
case Sites.create(user, site_params) do
|
||||
@ -46,7 +46,7 @@ defmodule PlausibleWeb.SiteController do
|
||||
)
|
||||
)
|
||||
|
||||
{:error, {:over_limit, limit}} ->
|
||||
{:error, _, {:over_limit, limit}, _} ->
|
||||
render(conn, "new.html",
|
||||
changeset: Plausible.Site.changeset(%Plausible.Site{}),
|
||||
first_site?: first_site?,
|
||||
@ -60,7 +60,7 @@ defmodule PlausibleWeb.SiteController do
|
||||
render(conn, "new.html",
|
||||
changeset: changeset,
|
||||
first_site?: first_site?,
|
||||
site_limit: Plausible.Teams.Adapter.Read.Billing.site_limit(user),
|
||||
site_limit: Plausible.Teams.Billing.site_limit(team),
|
||||
site_limit_exceeded?: false,
|
||||
flow: flow,
|
||||
form_submit_url: "/sites?flow=#{flow}"
|
||||
@ -122,11 +122,10 @@ defmodule PlausibleWeb.SiteController do
|
||||
end
|
||||
|
||||
def settings_people(conn, _params) do
|
||||
current_user = conn.assigns.current_user
|
||||
site = conn.assigns.site
|
||||
|
||||
%{memberships: memberships, invitations: invitations} =
|
||||
Plausible.Teams.Adapter.Read.Sites.list_people(site, current_user)
|
||||
Sites.list_people(site)
|
||||
|
||||
conn
|
||||
|> render("settings_people.html",
|
||||
|
@ -94,7 +94,13 @@ defmodule PlausibleWeb.Email do
|
||||
end
|
||||
|
||||
def trial_upgrade_email(user, day, usage) do
|
||||
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.total)
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
suggested_plan = Plausible.Billing.Plans.suggest(team, usage.total)
|
||||
|
||||
base_email()
|
||||
|> to(user)
|
||||
|
@ -18,20 +18,20 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
||||
socket
|
||||
|> assign_new(:pending_ownership_site_ids, fn %{current_user: current_user} ->
|
||||
current_user.email
|
||||
|> Plausible.Teams.Adapter.Read.Ownership.all_pending_site_transfers(current_user)
|
||||
|> Plausible.Teams.Memberships.all_pending_site_transfers()
|
||||
|> Enum.map(& &1.site_id)
|
||||
end)
|
||||
|> assign_new(:usage, fn %{
|
||||
current_user: current_user,
|
||||
current_team: current_team,
|
||||
pending_ownership_site_ids: pending_ownership_site_ids
|
||||
} ->
|
||||
Plausible.Teams.Adapter.Read.Billing.quota_usage(current_user,
|
||||
Plausible.Teams.Billing.quota_usage(current_team,
|
||||
with_features: true,
|
||||
pending_ownership_site_ids: pending_ownership_site_ids
|
||||
)
|
||||
end)
|
||||
|> assign_new(:subscription, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
|
||||
|> assign_new(:subscription, fn %{current_team: current_team} ->
|
||||
Plausible.Teams.Billing.get_subscription(current_team)
|
||||
end)
|
||||
|> assign_new(:owned_plan, fn %{subscription: subscription} ->
|
||||
Plans.get_regular_plan(subscription, only_non_expired: true)
|
||||
|
@ -16,7 +16,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
current_user
|
||||
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|
||||
|> Plausible.Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|
||||
|> Plausible.Imported.load_import_data()
|
||||
end)
|
||||
|> assign_new(:all_goals, fn %{site: site} ->
|
||||
|
@ -15,7 +15,7 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -32,7 +32,7 @@ defmodule PlausibleWeb.Live.Installation do
|
||||
socket
|
||||
) do
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin,
|
||||
|
@ -11,7 +11,7 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -19,7 +19,7 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -11,7 +11,7 @@ defmodule PlausibleWeb.Live.PropsSettings do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -18,7 +18,7 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -14,7 +14,7 @@ defmodule PlausibleWeb.Live.Shields.Countries do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -10,7 +10,7 @@ defmodule PlausibleWeb.Live.Shields.Hostnames do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -17,7 +17,7 @@ defmodule PlausibleWeb.Live.Shields.IPAddresses do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -10,7 +10,7 @@ defmodule PlausibleWeb.Live.Shields.Pages do
|
||||
socket =
|
||||
socket
|
||||
|> assign_new(:site, fn %{current_user: current_user} ->
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin
|
||||
|
@ -27,10 +27,14 @@ defmodule PlausibleWeb.Live.Sites do
|
||||
|> assign(:params, params)
|
||||
|> load_sites()
|
||||
|> assign_new(:has_sites?, fn %{current_user: current_user} ->
|
||||
has_sites?(current_user)
|
||||
Plausible.Teams.Users.has_sites?(current_user, include_pending?: true)
|
||||
end)
|
||||
|> assign_new(:needs_to_upgrade, fn %{current_user: current_user, sites: sites} ->
|
||||
owns_sites?(current_user, sites) && check_needs_to_upgrade(current_user)
|
||||
|> assign_new(:needs_to_upgrade, fn %{
|
||||
current_user: current_user,
|
||||
current_team: current_team
|
||||
} ->
|
||||
Plausible.Teams.Users.owns_sites?(current_user, include_pending?: true) &&
|
||||
Plausible.Teams.Billing.check_needs_to_upgrade(current_team)
|
||||
end)
|
||||
|
||||
{:noreply, socket}
|
||||
@ -640,12 +644,6 @@ defmodule PlausibleWeb.Live.Sites do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defdelegate has_sites?(user), to: Plausible.Teams.Adapter.Read.Ownership
|
||||
|
||||
defdelegate owns_sites?(user, sites), to: Plausible.Teams.Adapter.Read.Ownership
|
||||
|
||||
defdelegate check_needs_to_upgrade(user), to: Plausible.Teams.Adapter.Read.Billing
|
||||
|
||||
defp load_sites(%{assigns: assigns} = socket) do
|
||||
sites =
|
||||
Sites.list_with_invitations(assigns.current_user, assigns.params,
|
||||
@ -680,7 +678,13 @@ defmodule PlausibleWeb.Live.Sites do
|
||||
end
|
||||
|
||||
defp check_limits(%{role: :owner, site: site} = invitation, user) do
|
||||
case ensure_can_take_ownership(site, user) do
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
case ensure_can_take_ownership(site, team) do
|
||||
:ok ->
|
||||
check_features(invitation, user)
|
||||
|
||||
@ -695,9 +699,7 @@ defmodule PlausibleWeb.Live.Sites do
|
||||
|
||||
defp check_limits(invitation, _), do: %{invitation: invitation}
|
||||
|
||||
defdelegate ensure_can_take_ownership(site, user), to: Plausible.Teams.Adapter.Read.Ownership
|
||||
|
||||
defdelegate check_feature_access(site, user), to: Plausible.Teams.Adapter.Read.Ownership
|
||||
defdelegate ensure_can_take_ownership(site, team), to: Plausible.Teams.Invitations
|
||||
|
||||
def check_features(%{role: :owner, site: site} = invitation, user) do
|
||||
case check_feature_access(site, user) do
|
||||
@ -714,6 +716,24 @@ defmodule PlausibleWeb.Live.Sites do
|
||||
end
|
||||
end
|
||||
|
||||
on_ee do
|
||||
defp check_feature_access(site, new_owner) do
|
||||
missing_features =
|
||||
Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||
|> Enum.filter(&(&1.check_availability(new_owner) != :ok))
|
||||
|
||||
if missing_features == [] do
|
||||
:ok
|
||||
else
|
||||
{:error, {:missing_features, missing_features}}
|
||||
end
|
||||
end
|
||||
else
|
||||
defp check_feature_access(_site, _new_owner) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp set_filter_text(socket, filter_text) do
|
||||
uri = socket.assigns.uri
|
||||
|
||||
|
@ -18,7 +18,7 @@ defmodule PlausibleWeb.Live.Verification do
|
||||
socket
|
||||
) do
|
||||
site =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
|
||||
:owner,
|
||||
:admin,
|
||||
:super_admin,
|
||||
|
@ -134,6 +134,12 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||
end
|
||||
|
||||
defp verify_site_access(api_key, site) do
|
||||
team =
|
||||
case Plausible.Teams.get_by_owner(api_key.user) do
|
||||
{:ok, team} -> team
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
is_member? = Plausible.Teams.Memberships.site_member?(site, api_key.user)
|
||||
is_super_admin? = Auth.is_super_admin?(api_key.user_id)
|
||||
|
||||
@ -144,8 +150,7 @@ defmodule PlausibleWeb.Plugs.AuthorizePublicAPI do
|
||||
Sites.locked?(site) ->
|
||||
{:error, :site_locked}
|
||||
|
||||
Plausible.Teams.Adapter.Read.Billing.check_feature_availability_for_stats_api(api_key.user) !==
|
||||
:ok ->
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(team) !== :ok ->
|
||||
{:error, :upgrade_required}
|
||||
|
||||
is_member? ->
|
||||
|
@ -2,28 +2,18 @@
|
||||
<%= render("_flash.html", assigns) %>
|
||||
<% end %>
|
||||
|
||||
<%= if @conn.assigns[:current_team] do %>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<Notice.active_grace_period
|
||||
:if={Plausible.Auth.GracePeriod.active?(@conn.assigns.current_team)}
|
||||
enterprise?={
|
||||
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@conn.assigns.current_team)
|
||||
}
|
||||
grace_period_end={grace_period_end(@conn.assigns.current_team)}
|
||||
/>
|
||||
<div :if={assigns[:current_team]} class="flex flex-col gap-y-2">
|
||||
<Notice.active_grace_period
|
||||
:if={Plausible.Auth.GracePeriod.active?(@current_team)}
|
||||
enterprise?={Plausible.Teams.Billing.enterprise_configured?(@current_team)}
|
||||
grace_period_end={grace_period_end(@current_team)}
|
||||
/>
|
||||
|
||||
<Notice.dashboard_locked :if={Plausible.Auth.GracePeriod.expired?(@conn.assigns.current_team)} />
|
||||
<Notice.dashboard_locked :if={Plausible.Auth.GracePeriod.expired?(@current_team)} />
|
||||
|
||||
<Notice.subscription_cancelled subscription={@conn.assigns.current_team.subscription} />
|
||||
<Notice.subscription_cancelled subscription={@current_team.subscription} />
|
||||
|
||||
<Notice.subscription_past_due
|
||||
subscription={@conn.assigns.current_team.subscription}
|
||||
class="container"
|
||||
/>
|
||||
<Notice.subscription_past_due subscription={@current_team.subscription} class="container" />
|
||||
|
||||
<Notice.subscription_paused
|
||||
subscription={@conn.assigns.current_team.subscription}
|
||||
class="container"
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
<Notice.subscription_paused subscription={@current_team.subscription} class="container" />
|
||||
</div>
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<.filter_bar filtering_enabled?={false}>
|
||||
<.button_link
|
||||
:if={Plausible.Billing.Feature.StatsAPI.check_availability(@current_user) == :ok}
|
||||
:if={Plausible.Billing.Feature.StatsAPI.check_availability(@current_team) == :ok}
|
||||
href={Routes.settings_path(@conn, :new_api_key)}
|
||||
>
|
||||
New API Key
|
||||
|
@ -33,7 +33,7 @@
|
||||
|
||||
<div class="flex flex-col items-center justify-between sm:flex-row sm:items-start">
|
||||
<PlausibleWeb.Components.Billing.monthly_quota_box
|
||||
user={@current_user}
|
||||
team={@current_team}
|
||||
subscription={@subscription}
|
||||
/>
|
||||
<div class="w-full md:w-1/3 h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900">
|
||||
|
@ -8,7 +8,7 @@ defmodule PlausibleWeb.AuthView do
|
||||
def subscription_quota(nil, _options), do: "Free trial"
|
||||
|
||||
def subscription_quota(subscription, options) do
|
||||
pageview_limit = Plausible.Billing.Quota.Limits.monthly_pageview_limit(subscription)
|
||||
pageview_limit = Plausible.Teams.Billing.monthly_pageview_limit(subscription)
|
||||
|
||||
quota =
|
||||
if pageview_limit == :unlimited do
|
||||
|
@ -3,7 +3,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
use Oban.Worker, queue: :check_usage
|
||||
require Plausible.Billing.Subscription.Status
|
||||
alias Plausible.Billing.{Subscription, Quota}
|
||||
alias Plausible.Auth.User
|
||||
alias Plausible.Teams
|
||||
|
||||
defmacro yesterday() do
|
||||
quote do
|
||||
@ -33,26 +33,17 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
end
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(_job, usage_mod \\ Quota.Usage, today \\ Date.utc_today()) do
|
||||
def perform(_job, usage_mod \\ Teams.Billing, today \\ Date.utc_today()) do
|
||||
yesterday = today |> Date.shift(day: -1)
|
||||
|
||||
last_subscription_query =
|
||||
from(s in Subscription,
|
||||
order_by: [desc: s.inserted_at],
|
||||
where: s.user_id == parent_as(:user).id,
|
||||
limit: 1
|
||||
)
|
||||
|
||||
active_subscribers =
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
as: :user,
|
||||
inner_join: s in Plausible.Billing.Subscription,
|
||||
on: s.user_id == u.id,
|
||||
inner_lateral_join: ls in subquery(last_subscription_query),
|
||||
on: ls.id == s.id,
|
||||
left_join: ep in Plausible.Billing.EnterprisePlan,
|
||||
on: ep.user_id == u.id,
|
||||
from(t in Teams.Team,
|
||||
as: :team,
|
||||
inner_join: o in assoc(t, :owner),
|
||||
inner_lateral_join: s in subquery(Teams.last_subscription_join_query()),
|
||||
on: true,
|
||||
left_join: ep in assoc(t, :enterprise_plan),
|
||||
where:
|
||||
s.status in [
|
||||
^Subscription.Status.active(),
|
||||
@ -65,8 +56,8 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
where:
|
||||
least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) ==
|
||||
day_of_month(^yesterday),
|
||||
order_by: u.id,
|
||||
preload: [subscription: s, enterprise_plan: ep]
|
||||
order_by: t.id,
|
||||
preload: [subscription: s, enterprise_plan: ep, owner: o]
|
||||
)
|
||||
)
|
||||
|
||||
@ -91,7 +82,8 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
|
||||
defp check_site_usage_for_enterprise(subscriber) do
|
||||
limit = subscriber.enterprise_plan.site_limit
|
||||
usage = Quota.Usage.site_usage(subscriber)
|
||||
|
||||
usage = Teams.Billing.site_usage(subscriber)
|
||||
|
||||
if Quota.below_limit?(usage, limit) do
|
||||
{:below_limit, {usage, limit}}
|
||||
@ -103,7 +95,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
def maybe_remove_grace_period(subscriber, usage_mod) do
|
||||
case check_pageview_usage_last_cycle(subscriber, usage_mod) do
|
||||
{:below_limit, _} ->
|
||||
Plausible.Users.remove_grace_period(subscriber)
|
||||
Plausible.Users.remove_grace_period(subscriber.owner)
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
@ -117,10 +109,10 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
suggested_plan =
|
||||
Plausible.Billing.Plans.suggest(subscriber, pageview_usage.last_cycle.total)
|
||||
|
||||
PlausibleWeb.Email.over_limit_email(subscriber, pageview_usage, suggested_plan)
|
||||
PlausibleWeb.Email.over_limit_email(subscriber.owner, pageview_usage, suggested_plan)
|
||||
|> Plausible.Mailer.send()
|
||||
|
||||
Plausible.Users.start_grace_period(subscriber)
|
||||
Plausible.Users.start_grace_period(subscriber.owner)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
@ -137,20 +129,20 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
|
||||
{{_, pageview_usage}, {_, {site_usage, site_allowance}}} ->
|
||||
PlausibleWeb.Email.enterprise_over_limit_internal_email(
|
||||
subscriber,
|
||||
subscriber.owner,
|
||||
pageview_usage,
|
||||
site_usage,
|
||||
site_allowance
|
||||
)
|
||||
|> Plausible.Mailer.send()
|
||||
|
||||
Plausible.Users.start_manual_lock_grace_period(subscriber)
|
||||
Plausible.Users.start_manual_lock_grace_period(subscriber.owner)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_pageview_usage_two_cycles(subscriber, usage_mod) do
|
||||
usage = usage_mod.monthly_pageview_usage(subscriber)
|
||||
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
|
||||
limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)
|
||||
|
||||
if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
|
||||
{:over_limit, usage}
|
||||
@ -161,7 +153,7 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
|
||||
defp check_pageview_usage_last_cycle(subscriber, usage_mod) do
|
||||
usage = usage_mod.monthly_pageview_usage(subscriber)
|
||||
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
|
||||
limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)
|
||||
|
||||
if :last_cycle in Quota.exceeded_cycles(usage, limit) do
|
||||
{:over_limit, usage}
|
||||
|
@ -5,40 +5,43 @@ defmodule Plausible.Workers.SendTrialNotifications do
|
||||
queue: :trial_notification_emails,
|
||||
max_attempts: 1
|
||||
|
||||
alias Plausible.Teams
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Oban.Worker
|
||||
def perform(_job) do
|
||||
users =
|
||||
teams =
|
||||
Repo.all(
|
||||
from u in Plausible.Auth.User,
|
||||
left_join: s in Plausible.Billing.Subscription,
|
||||
on: s.user_id == u.id,
|
||||
where: not is_nil(u.trial_expiry_date),
|
||||
from t in Teams.Team,
|
||||
inner_join: o in assoc(t, :owner),
|
||||
left_join: s in assoc(t, :subscription),
|
||||
where: not is_nil(t.trial_expiry_date),
|
||||
where: is_nil(s.id),
|
||||
order_by: u.inserted_at
|
||||
order_by: t.inserted_at,
|
||||
preload: [owner: o]
|
||||
)
|
||||
|
||||
for user <- users do
|
||||
case Date.diff(user.trial_expiry_date, Date.utc_today()) do
|
||||
for team <- teams do
|
||||
case Date.diff(team.trial_expiry_date, Date.utc_today()) do
|
||||
7 ->
|
||||
if Plausible.Auth.has_active_sites?(user, [:owner]) do
|
||||
send_one_week_reminder(user)
|
||||
if Teams.has_active_sites?(team) do
|
||||
send_one_week_reminder(team.owner)
|
||||
end
|
||||
|
||||
1 ->
|
||||
if Plausible.Auth.has_active_sites?(user, [:owner]) do
|
||||
send_tomorrow_reminder(user)
|
||||
if Teams.has_active_sites?(team) do
|
||||
send_tomorrow_reminder(team.owner, team)
|
||||
end
|
||||
|
||||
0 ->
|
||||
if Plausible.Auth.has_active_sites?(user, [:owner]) do
|
||||
send_today_reminder(user)
|
||||
if Teams.has_active_sites?(team) do
|
||||
send_today_reminder(team.owner, team)
|
||||
end
|
||||
|
||||
-1 ->
|
||||
if Plausible.Auth.has_active_sites?(user, [:owner]) do
|
||||
send_over_reminder(user)
|
||||
if Teams.has_active_sites?(team) do
|
||||
send_over_reminder(team.owner)
|
||||
end
|
||||
|
||||
_ ->
|
||||
@ -54,15 +57,15 @@ defmodule Plausible.Workers.SendTrialNotifications do
|
||||
|> Plausible.Mailer.send()
|
||||
end
|
||||
|
||||
defp send_tomorrow_reminder(user) do
|
||||
usage = Plausible.Billing.Quota.Usage.usage_cycle(user, :last_30_days)
|
||||
defp send_tomorrow_reminder(user, team) do
|
||||
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
|
||||
|
||||
PlausibleWeb.Email.trial_upgrade_email(user, "tomorrow", usage)
|
||||
|> Plausible.Mailer.send()
|
||||
end
|
||||
|
||||
defp send_today_reminder(user) do
|
||||
usage = Plausible.Billing.Quota.Usage.usage_cycle(user, :last_30_days)
|
||||
defp send_today_reminder(user, team) do
|
||||
usage = Plausible.Teams.Billing.usage_cycle(team, :last_30_days)
|
||||
|
||||
PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
|> Plausible.Mailer.send()
|
||||
|
@ -49,14 +49,12 @@ defmodule Plausible.AuthTest do
|
||||
user_with_plan_no_subscription =
|
||||
new_user() |> subscribe_to_enterprise_plan(subscription?: false)
|
||||
|
||||
assert Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user_with_plan)
|
||||
assert Plausible.Teams.Billing.enterprise_configured?(team_of(user_with_plan))
|
||||
|
||||
assert Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(
|
||||
user_with_plan_no_subscription
|
||||
)
|
||||
assert Plausible.Teams.Billing.enterprise_configured?(team_of(user_with_plan_no_subscription))
|
||||
|
||||
refute Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user_without_plan)
|
||||
refute Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(nil)
|
||||
refute Plausible.Teams.Billing.enterprise_configured?(team_of(user_without_plan))
|
||||
refute Plausible.Teams.Billing.enterprise_configured?(nil)
|
||||
end
|
||||
|
||||
describe "create_api_key/3" do
|
||||
@ -78,7 +76,7 @@ defmodule Plausible.AuthTest do
|
||||
|
||||
@tag :ce_build_only
|
||||
test "defaults to 1000000 requests per hour limit in CE" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
|
||||
{:ok, %Auth.ApiKey{hourly_request_limit: hourly_request_limit}} =
|
||||
Auth.create_api_key(user, "my new CE key", Ecto.UUID.generate())
|
||||
|
@ -8,79 +8,94 @@ defmodule Plausible.BillingTest do
|
||||
|
||||
describe "check_needs_to_upgrade" do
|
||||
test "is false for a trial user" do
|
||||
user = insert(:user)
|
||||
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
|
||||
team = new_user() |> team_of()
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) == :no_upgrade_needed
|
||||
end
|
||||
|
||||
test "is true for a user with an expired trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
team = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1)) |> team_of()
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) ==
|
||||
{:needs_to_upgrade, :no_active_subscription}
|
||||
end
|
||||
|
||||
test "is true for a user with empty trial expiry date" do
|
||||
user = insert(:user, trial_expiry_date: nil)
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_trial}
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(nil) ==
|
||||
{:needs_to_upgrade, :no_trial}
|
||||
end
|
||||
|
||||
test "is false for user with empty trial expiry date but with an active subscription" do
|
||||
user = insert(:user, trial_expiry_date: nil)
|
||||
insert(:subscription, user: user)
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_growth_plan()
|
||||
|> team_of()
|
||||
|> Ecto.Changeset.change(trial_expiry_date: nil)
|
||||
|> Repo.update!()
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) == :no_upgrade_needed
|
||||
end
|
||||
|
||||
test "is false for a user with an expired trial but an active subscription" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
insert(:subscription, user: user)
|
||||
team =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan()
|
||||
|> team_of()
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) == :no_upgrade_needed
|
||||
end
|
||||
|
||||
test "is false for a user with a cancelled subscription IF the billing cycle isn't completed yet" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
team =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Date.utc_today()
|
||||
)
|
||||
|> team_of()
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Timex.today()
|
||||
)
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) == :no_upgrade_needed
|
||||
end
|
||||
|
||||
test "is true for a user with a cancelled subscription IF the billing cycle is complete" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
team =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Date.shift(Date.utc_today(), day: -1)
|
||||
)
|
||||
|> team_of()
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Timex.shift(Timex.today(), days: -1)
|
||||
)
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) ==
|
||||
{:needs_to_upgrade, :no_active_subscription}
|
||||
end
|
||||
|
||||
test "is true for a deleted subscription if no next_bill_date specified" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
team =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: nil
|
||||
)
|
||||
|> team_of()
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: nil
|
||||
)
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) ==
|
||||
{:needs_to_upgrade, :no_active_subscription}
|
||||
end
|
||||
|
||||
test "is true for a user past their grace period" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
insert(:subscription, user: user, next_bill_date: Timex.today())
|
||||
user =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Date.utc_today()
|
||||
)
|
||||
|
||||
user = Plausible.Users.end_grace_period(user)
|
||||
|
||||
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :grace_period_ended}
|
||||
team = user |> Repo.reload!() |> team_of()
|
||||
|
||||
assert Plausible.Teams.Billing.check_needs_to_upgrade(team) ==
|
||||
{:needs_to_upgrade, :grace_period_ended}
|
||||
end
|
||||
end
|
||||
|
||||
@ -514,12 +529,11 @@ defmodule Plausible.BillingTest do
|
||||
|
||||
describe "change_plan" do
|
||||
test "sets the next bill amount and date" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, user: user)
|
||||
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||
|
||||
Billing.change_plan(user, "123123")
|
||||
Plausible.Teams.Billing.change_plan(team, "123123")
|
||||
|
||||
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
|
||||
subscription = Repo.get_by(Plausible.Billing.Subscription, team_id: team.id)
|
||||
assert subscription.paddle_plan_id == "123123"
|
||||
assert subscription.next_bill_date == ~D[2019-07-10]
|
||||
assert subscription.next_bill_amount == "6.00"
|
||||
@ -527,22 +541,33 @@ defmodule Plausible.BillingTest do
|
||||
end
|
||||
|
||||
test "active_subscription_for/1 returns active subscription" do
|
||||
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
|
||||
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
|
||||
user_without_subscription = insert(:user)
|
||||
active_team =
|
||||
new_user()
|
||||
|> subscribe_to_growth_plan(status: Subscription.Status.active())
|
||||
|> team_of()
|
||||
|> Plausible.Teams.with_subscription()
|
||||
|
||||
assert Billing.active_subscription_for(active.user).id == active.id
|
||||
assert Billing.active_subscription_for(paused.user) == nil
|
||||
assert Billing.active_subscription_for(user_without_subscription) == nil
|
||||
paused_team =
|
||||
new_user()
|
||||
|> subscribe_to_growth_plan(status: Subscription.Status.paused())
|
||||
|> team_of()
|
||||
|
||||
assert Plausible.Teams.Billing.active_subscription_for(active_team).id ==
|
||||
active_team.subscription.id
|
||||
|
||||
assert Plausible.Teams.Billing.active_subscription_for(paused_team) == nil
|
||||
assert Plausible.Teams.Billing.active_subscription_for(nil) == nil
|
||||
end
|
||||
|
||||
test "has_active_subscription?/1 returns whether the user has an active subscription" do
|
||||
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
|
||||
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
|
||||
user_without_subscription = insert(:user)
|
||||
active_team =
|
||||
new_user() |> subscribe_to_growth_plan(status: Subscription.Status.active()) |> team_of()
|
||||
|
||||
assert Billing.has_active_subscription?(active.user)
|
||||
refute Billing.has_active_subscription?(paused.user)
|
||||
refute Billing.has_active_subscription?(user_without_subscription)
|
||||
paused_team =
|
||||
new_user() |> subscribe_to_growth_plan(status: Subscription.Status.paused()) |> team_of()
|
||||
|
||||
assert Plausible.Teams.Billing.has_active_subscription?(active_team)
|
||||
refute Plausible.Teams.Billing.has_active_subscription?(paused_team)
|
||||
refute Plausible.Teams.Billing.has_active_subscription?(nil)
|
||||
end
|
||||
end
|
||||
|
@ -6,96 +6,88 @@ defmodule Plausible.Billing.FeatureTest do
|
||||
|
||||
for mod <- [Plausible.Billing.Feature.Funnels, Plausible.Billing.Feature.RevenueGoals] do
|
||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a enterprise plan" do
|
||||
user =
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", features: [unquote(mod)])
|
||||
|> team_of()
|
||||
|
||||
assert :ok == unquote(mod).check_availability(user)
|
||||
assert :ok == unquote(mod).check_availability(team)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns :ok when site owner is on a business plan" do
|
||||
user = new_user() |> subscribe_to_business_plan()
|
||||
assert :ok == unquote(mod).check_availability(user)
|
||||
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
assert :ok == unquote(mod).check_availability(team)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns error when site owner is on a growth plan" do
|
||||
user = new_user() |> subscribe_to_growth_plan()
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(user)
|
||||
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||
end
|
||||
|
||||
test "#{mod}.check_availability/1 returns error when site owner is on an old plan" do
|
||||
user = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(user)
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
assert {:error, :upgrade_required} == unquote(mod).check_availability(team)
|
||||
end
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on a business plan" do
|
||||
user = new_user() |> subscribe_to_business_plan()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
team = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an old plan" do
|
||||
user = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on trial" do
|
||||
user = new_user()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
team = new_user() |> team_of()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user is on an enterprise plan" do
|
||||
user =
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(
|
||||
paddle_plan_id: "123321",
|
||||
features: [Plausible.Billing.Feature.StatsAPI]
|
||||
)
|
||||
|> team_of()
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user is on a growth plan" do
|
||||
user = new_user() |> subscribe_to_growth_plan()
|
||||
team = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||
|
||||
assert {:error, :upgrade_required} ==
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok when user trial hasn't started and was created before the business tier launch" do
|
||||
user = new_user(inserted_at: ~N[2020-01-01T00:00:00], trial_expiry_date: nil)
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns :ok if user is subscribed and account was created after business tier launch" do
|
||||
user = new_user(trial_expiry_date: nil) |> subscribe_to_business_plan()
|
||||
assert :ok == Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(team)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
test "Plausible.Billing.Feature.StatsAPI.check_availability/2 returns error when user trial hasn't started and was created after the business tier launch" do
|
||||
user = new_user(trial_expiry_date: nil)
|
||||
_user = new_user(trial_expiry_date: nil)
|
||||
|
||||
assert {:error, :upgrade_required} ==
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(user)
|
||||
Plausible.Billing.Feature.StatsAPI.check_availability(nil)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.Props.check_availability/1 applies grandfathering to old plans" do
|
||||
user = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
assert :ok == Plausible.Billing.Feature.Props.check_availability(user)
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
assert :ok == Plausible.Billing.Feature.Props.check_availability(team)
|
||||
end
|
||||
|
||||
test "Plausible.Billing.Feature.Goals.check_availability/2 always returns :ok" do
|
||||
u1 = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
u2 = new_user() |> subscribe_to_growth_plan()
|
||||
u3 = new_user() |> subscribe_to_business_plan()
|
||||
u4 = new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321")
|
||||
t1 = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
t2 = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||
t3 = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
t4 = new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321") |> team_of()
|
||||
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u1)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u2)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u3)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(u4)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t1)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t2)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t3)
|
||||
assert :ok == Plausible.Billing.Feature.Goals.check_availability(t4)
|
||||
end
|
||||
|
||||
for {mod, property} <- [
|
||||
|
@ -155,22 +155,30 @@ defmodule Plausible.Billing.PlansTest do
|
||||
end
|
||||
|
||||
test "latest_enterprise_plan_with_price/1" do
|
||||
user = insert(:user)
|
||||
insert(:enterprise_plan, user: user, paddle_plan_id: "123", inserted_at: Timex.now())
|
||||
now = NaiveDateTime.utc_now()
|
||||
user = new_user()
|
||||
team = team_of(user)
|
||||
|
||||
insert(:enterprise_plan,
|
||||
user: user,
|
||||
subscribe_to_enterprise_plan(user,
|
||||
paddle_plan_id: "123",
|
||||
inserted_at: now,
|
||||
subscription?: false
|
||||
)
|
||||
|
||||
subscribe_to_enterprise_plan(user,
|
||||
paddle_plan_id: "456",
|
||||
inserted_at: Timex.shift(Timex.now(), hours: -10)
|
||||
inserted_at: NaiveDateTime.shift(now, hour: -10),
|
||||
subscription?: false
|
||||
)
|
||||
|
||||
insert(:enterprise_plan,
|
||||
user: user,
|
||||
subscribe_to_enterprise_plan(user,
|
||||
paddle_plan_id: "789",
|
||||
inserted_at: Timex.shift(Timex.now(), minutes: -2)
|
||||
inserted_at: NaiveDateTime.shift(now, minute: -2),
|
||||
subscription?: false
|
||||
)
|
||||
|
||||
{enterprise_plan, price} = Plans.latest_enterprise_plan_with_price(user, "127.0.0.1")
|
||||
{enterprise_plan, price} =
|
||||
Plausible.Teams.Billing.latest_enterprise_plan_with_price(team, "127.0.0.1")
|
||||
|
||||
assert enterprise_plan.paddle_plan_id == "123"
|
||||
assert price == Money.new(:EUR, "10.0")
|
||||
@ -210,7 +218,7 @@ defmodule Plausible.Billing.PlansTest do
|
||||
|
||||
describe "suggested_plan/2" do
|
||||
test "returns suggested plan based on usage" do
|
||||
user = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
|
||||
assert %Plausible.Billing.Plan{
|
||||
monthly_pageview_limit: 100_000,
|
||||
@ -219,7 +227,7 @@ defmodule Plausible.Billing.PlansTest do
|
||||
volume: "100k",
|
||||
yearly_cost: nil,
|
||||
yearly_product_id: "590752"
|
||||
} = Plans.suggest(user, 10_000)
|
||||
} = Plans.suggest(team, 10_000)
|
||||
|
||||
assert %Plausible.Billing.Plan{
|
||||
monthly_pageview_limit: 200_000,
|
||||
@ -228,21 +236,22 @@ defmodule Plausible.Billing.PlansTest do
|
||||
volume: "200k",
|
||||
yearly_cost: nil,
|
||||
yearly_product_id: "597486"
|
||||
} = Plans.suggest(user, 100_000)
|
||||
} = Plans.suggest(team, 100_000)
|
||||
end
|
||||
|
||||
test "returns nil when user has enterprise-level usage" do
|
||||
user = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
assert :enterprise == Plans.suggest(user, 100_000_000)
|
||||
team = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
assert :enterprise == Plans.suggest(team, 100_000_000)
|
||||
end
|
||||
|
||||
test "returns nil when user is on an enterprise plan" do
|
||||
user =
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_plan(@v1_plan_id)
|
||||
|> subscribe_to_enterprise_plan(billing_interval: :yearly, subscription?: false)
|
||||
|> team_of()
|
||||
|
||||
assert :enterprise == Plans.suggest(user, 10_000)
|
||||
assert :enterprise == Plans.suggest(team, 10_000)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -24,54 +24,58 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
@describetag :ee_only
|
||||
|
||||
test "returns 50 when user is on an old plan" do
|
||||
user_on_v1 = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
user_on_v2 = new_user() |> subscribe_to_plan(@v2_plan_id)
|
||||
user_on_v3 = new_user() |> subscribe_to_plan(@v3_plan_id)
|
||||
team_on_v1 = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
team_on_v2 = new_user() |> subscribe_to_plan(@v2_plan_id) |> team_of()
|
||||
team_on_v3 = new_user() |> subscribe_to_plan(@v3_plan_id) |> team_of()
|
||||
|
||||
assert 50 == Plausible.Teams.Adapter.Read.Billing.site_limit(user_on_v1)
|
||||
assert 50 == Plausible.Teams.Adapter.Read.Billing.site_limit(user_on_v2)
|
||||
assert 50 == Plausible.Teams.Adapter.Read.Billing.site_limit(user_on_v3)
|
||||
assert 50 == Plausible.Teams.Billing.site_limit(team_on_v1)
|
||||
assert 50 == Plausible.Teams.Billing.site_limit(team_on_v2)
|
||||
assert 50 == Plausible.Teams.Billing.site_limit(team_on_v3)
|
||||
end
|
||||
|
||||
test "returns 50 when user is on free_10k plan" do
|
||||
user = new_user() |> subscribe_to_plan("free_10k")
|
||||
assert 50 == Plausible.Teams.Adapter.Read.Billing.site_limit(user)
|
||||
team = new_user() |> subscribe_to_plan("free_10k") |> team_of()
|
||||
assert 50 == Plausible.Teams.Billing.site_limit(team)
|
||||
end
|
||||
|
||||
test "returns the configured site limit for enterprise plan" do
|
||||
user = new_user() |> subscribe_to_enterprise_plan(site_limit: 500)
|
||||
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 500
|
||||
team = new_user() |> subscribe_to_enterprise_plan(site_limit: 500) |> team_of()
|
||||
assert Plausible.Teams.Billing.site_limit(team) == 500
|
||||
end
|
||||
|
||||
test "returns 10 when user in on trial" do
|
||||
user = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: 7))
|
||||
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 10
|
||||
team = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: 7)) |> team_of()
|
||||
assert Plausible.Teams.Billing.site_limit(team) == 10
|
||||
end
|
||||
|
||||
test "returns the subscription limit for enterprise users who have not paid yet" do
|
||||
user =
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_plan(@v1_plan_id)
|
||||
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", subscription?: false)
|
||||
|> team_of()
|
||||
|
||||
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 50
|
||||
assert Plausible.Teams.Billing.site_limit(team) == 50
|
||||
end
|
||||
|
||||
test "returns 10 for enterprise users who have not upgraded yet and are on trial" do
|
||||
user =
|
||||
new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321", subscription?: false)
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", subscription?: false)
|
||||
|> team_of()
|
||||
|
||||
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 10
|
||||
assert Plausible.Teams.Billing.site_limit(team) == 10
|
||||
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)])
|
||||
user = new_user()
|
||||
for _ <- 1..3, do: new_site(owner: user)
|
||||
add_guest(new_site(), user: user, role: :editor)
|
||||
add_guest(new_site(), user: user, role: :viewer)
|
||||
team = team_of(user)
|
||||
|
||||
assert Quota.Usage.site_usage(user) == 3
|
||||
assert Plausible.Teams.Billing.site_usage(team) == 3
|
||||
end
|
||||
|
||||
describe "below_limit?/2" do
|
||||
@ -199,21 +203,33 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
|
||||
describe "monthly_pageview_limit/1" do
|
||||
test "is based on the plan if user is on a legacy plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_plan(@legacy_plan_id)
|
||||
|> team_of()
|
||||
|> Plausible.Teams.with_subscription()
|
||||
|
||||
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 1_000_000
|
||||
assert Plausible.Teams.Billing.monthly_pageview_limit(team.subscription) == 1_000_000
|
||||
end
|
||||
|
||||
test "is based on the plan if user is on a standard plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_plan(@v1_plan_id)
|
||||
|> team_of()
|
||||
|> Plausible.Teams.with_subscription()
|
||||
|
||||
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 10_000
|
||||
assert Plausible.Teams.Billing.monthly_pageview_limit(team.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "free_10k has 10k monthly_pageview_limit" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
team =
|
||||
new_user()
|
||||
|> subscribe_to_plan("free_10k")
|
||||
|> team_of()
|
||||
|> Plausible.Teams.with_subscription()
|
||||
|
||||
assert Quota.Limits.monthly_pageview_limit(user.subscription) == 10_000
|
||||
assert Plausible.Teams.Billing.monthly_pageview_limit(team.subscription) == 10_000
|
||||
end
|
||||
|
||||
test "is based on the enterprise plan if user is on an enterprise plan" do
|
||||
@ -226,7 +242,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
|> Repo.preload(:subscription)
|
||||
|> Map.fetch!(:subscription)
|
||||
|
||||
assert Quota.Limits.monthly_pageview_limit(subscription) == 100_000
|
||||
assert Plausible.Teams.Billing.monthly_pageview_limit(subscription) == 100_000
|
||||
end
|
||||
|
||||
test "does not limit pageviews when user has a pending enterprise plan" do
|
||||
@ -239,233 +255,169 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
|> Repo.preload(:subscription)
|
||||
|> Map.fetch!(:subscription)
|
||||
|
||||
assert Quota.Limits.monthly_pageview_limit(subscription) == :unlimited
|
||||
assert Plausible.Teams.Billing.monthly_pageview_limit(subscription) == :unlimited
|
||||
end
|
||||
end
|
||||
|
||||
describe "team_member_usage/2" do
|
||||
test "returns the number of members in all of the sites the user owns" do
|
||||
me = insert(:user)
|
||||
me = new_user()
|
||||
site_i_own_1 = new_site(owner: me)
|
||||
add_guest(site_i_own_1, role: :viewer)
|
||||
site_i_own_2 = new_site(owner: me)
|
||||
add_guest(site_i_own_2, role: :editor)
|
||||
add_guest(site_i_own_2, role: :viewer)
|
||||
_site_i_own_3 = new_site(owner: me)
|
||||
site_i_have_access = new_site()
|
||||
add_guest(site_i_have_access, user: me, role: :viewer)
|
||||
add_guest(site_i_have_access, role: :viewer)
|
||||
add_guest(site_i_have_access, role: :viewer)
|
||||
add_guest(site_i_have_access, role: :viewer)
|
||||
team = team_of(me)
|
||||
|
||||
_site_i_own_1 =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: build(:user), role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
_site_i_own_2 =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: build(:user), role: :admin),
|
||||
build(:site_membership, user: build(:user), role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
_site_i_own_3 =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner)
|
||||
]
|
||||
)
|
||||
|
||||
_site_i_have_access =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :viewer),
|
||||
build(:site_membership, user: build(:user), role: :viewer),
|
||||
build(:site_membership, user: build(:user), role: :viewer),
|
||||
build(:site_membership, user: build(:user), role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 3
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 3
|
||||
end
|
||||
|
||||
test "counts the same email address as one team member" do
|
||||
me = insert(:user)
|
||||
joe = insert(:user, email: "joe@plausible.test")
|
||||
me = new_user()
|
||||
joe = new_user(email: "joe@plausible.test")
|
||||
site_i_own_1 = new_site(owner: me)
|
||||
add_guest(site_i_own_1, user: joe, role: :viewer)
|
||||
site_i_own_2 = new_site(owner: me)
|
||||
add_guest(site_i_own_2, user: new_user(), role: :editor)
|
||||
add_guest(site_i_own_2, user: joe, role: :viewer)
|
||||
site_i_own_3 = new_site(owner: me)
|
||||
invite_guest(site_i_own_3, joe, role: :viewer, inviter: me)
|
||||
team = team_of(me)
|
||||
|
||||
_site_i_own_1 =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: joe, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
_site_i_own_2 =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: build(:user), role: :admin),
|
||||
build(:site_membership, user: joe, role: :viewer)
|
||||
]
|
||||
)
|
||||
|
||||
site_i_own_3 = insert(:site, memberships: [build(:site_membership, user: me, role: :owner)])
|
||||
|
||||
insert(:invitation, site: site_i_own_3, inviter: me, email: "joe@plausible.test")
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 2
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 2
|
||||
end
|
||||
|
||||
test "counts pending invitations as team members" do
|
||||
me = insert(:user)
|
||||
member = insert(:user)
|
||||
me = new_user()
|
||||
member = new_user()
|
||||
site_i_own = new_site(owner: me)
|
||||
add_guest(site_i_own, user: member, role: :editor)
|
||||
site_i_have_access = new_site()
|
||||
add_guest(site_i_have_access, user: me, role: :editor)
|
||||
team = team_of(me)
|
||||
|
||||
site_i_own =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: member, role: :admin)
|
||||
]
|
||||
)
|
||||
invite_guest(site_i_own, new_user(), role: :viewer, inviter: me)
|
||||
invite_guest(site_i_own, new_user(), role: :viewer, inviter: member)
|
||||
invite_guest(site_i_have_access, new_user(), role: :viewer, inviter: me)
|
||||
|
||||
site_i_have_access =
|
||||
insert(:site, memberships: [build(:site_membership, user: me, role: :admin)])
|
||||
|
||||
insert(:invitation, site: site_i_own, inviter: me)
|
||||
insert(:invitation, site: site_i_own, inviter: member)
|
||||
insert(:invitation, site: site_i_have_access, inviter: me)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 3
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 3
|
||||
end
|
||||
|
||||
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)])
|
||||
me = new_user()
|
||||
site_i_own = new_site(owner: me)
|
||||
invite_transfer(site_i_own, new_user(), inviter: me)
|
||||
team = team_of(me)
|
||||
|
||||
insert(:invitation, site: site_i_own, inviter: me, role: :owner)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 0
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 0
|
||||
end
|
||||
|
||||
test "counts team members from pending ownerships when specified" do
|
||||
me = insert(:user)
|
||||
me = new_user()
|
||||
my_team = team_of(me)
|
||||
|
||||
user_1 = insert(:user)
|
||||
user_2 = insert(:user)
|
||||
user_1 = new_user()
|
||||
user_2 = new_user()
|
||||
|
||||
pending_ownership_site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user_1, role: :owner),
|
||||
build(:site_membership, user: user_2, role: :admin)
|
||||
]
|
||||
)
|
||||
pending_ownership_site = new_site(owner: user_1)
|
||||
add_guest(pending_ownership_site, user: user_2, role: :editor)
|
||||
|
||||
insert(:invitation,
|
||||
site: pending_ownership_site,
|
||||
inviter: user_1,
|
||||
email: me.email,
|
||||
role: :owner
|
||||
)
|
||||
invite_transfer(pending_ownership_site, me, inviter: user_1)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me,
|
||||
assert Plausible.Teams.Billing.team_member_usage(my_team,
|
||||
pending_ownership_site_ids: [pending_ownership_site.id]
|
||||
) == 2
|
||||
end
|
||||
|
||||
test "counts invitations towards team members from pending ownership sites" do
|
||||
me = insert(:user)
|
||||
me = new_user()
|
||||
user_1 = new_user()
|
||||
user_2 = new_user()
|
||||
pending_ownership_site = new_site(owner: user_1)
|
||||
invite_transfer(pending_ownership_site, me, inviter: user_1)
|
||||
invite_guest(pending_ownership_site, user_2, role: :editor, inviter: user_1)
|
||||
team = team_of(me)
|
||||
|
||||
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,
|
||||
assert Plausible.Teams.Billing.team_member_usage(team,
|
||||
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
|
||||
team = new_user() |> team_of()
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 0
|
||||
end
|
||||
|
||||
test "does not count email report recipients as team members" do
|
||||
me = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: me, role: :owner)])
|
||||
me = new_user()
|
||||
site = new_site(owner: me)
|
||||
team = team_of(me)
|
||||
|
||||
insert(:weekly_report,
|
||||
site: site,
|
||||
recipients: ["adam@plausible.test", "vini@plausible.test"]
|
||||
)
|
||||
|
||||
assert Quota.Usage.team_member_usage(me) == 0
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 0
|
||||
end
|
||||
|
||||
test "excludes specific emails from limit calculation" do
|
||||
me = insert(:user)
|
||||
member = insert(:user)
|
||||
me = new_user()
|
||||
member = new_user()
|
||||
site_i_own = new_site(owner: me)
|
||||
add_guest(site_i_own, user: member, role: :editor)
|
||||
team = team_of(me)
|
||||
|
||||
site_i_own =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: me, role: :owner),
|
||||
build(:site_membership, user: member, role: :admin)
|
||||
]
|
||||
)
|
||||
invite_guest(site_i_own, new_user(), role: :viewer, inviter: me)
|
||||
invite_guest(site_i_own, new_user(), role: :viewer, inviter: member)
|
||||
invitation = invite_guest(site_i_own, "foo@example.com", role: :viewer, inviter: me)
|
||||
|
||||
insert(:invitation, site: site_i_own, inviter: me)
|
||||
insert(:invitation, site: site_i_own, inviter: member)
|
||||
invitation = insert(:invitation, site: site_i_own, inviter: me, email: "foo@example.com")
|
||||
assert Plausible.Teams.Billing.team_member_usage(team) == 4
|
||||
|
||||
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 Plausible.Teams.Billing.team_member_usage(team,
|
||||
exclude_emails: ["arbitrary@example.com"]
|
||||
) == 4
|
||||
|
||||
assert Quota.Usage.team_member_usage(me, exclude_emails: [member.email, invitation.email]) ==
|
||||
2
|
||||
assert Plausible.Teams.Billing.team_member_usage(team, exclude_emails: [member.email]) == 3
|
||||
|
||||
assert Plausible.Teams.Billing.team_member_usage(team, exclude_emails: [invitation.email]) ==
|
||||
3
|
||||
|
||||
assert Plausible.Teams.Billing.team_member_usage(team,
|
||||
exclude_emails: [member.email, invitation.email]
|
||||
) == 2
|
||||
end
|
||||
end
|
||||
|
||||
describe "team_member_limit/1" do
|
||||
@describetag :ee_only
|
||||
test "returns unlimited 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))
|
||||
team_on_v1 = new_user() |> subscribe_to_plan(@v1_plan_id)
|
||||
team_on_v2 = new_user() |> subscribe_to_plan(@v2_plan_id)
|
||||
team_on_v3 = new_user() |> subscribe_to_plan(@v3_plan_id)
|
||||
|
||||
assert :unlimited == Quota.Limits.team_member_limit(user_on_v1)
|
||||
assert :unlimited == Quota.Limits.team_member_limit(user_on_v2)
|
||||
assert :unlimited == Quota.Limits.team_member_limit(user_on_v3)
|
||||
assert :unlimited == Plausible.Teams.Billing.team_member_limit(team_on_v1)
|
||||
assert :unlimited == Plausible.Teams.Billing.team_member_limit(team_on_v2)
|
||||
assert :unlimited == Plausible.Teams.Billing.team_member_limit(team_on_v3)
|
||||
end
|
||||
|
||||
test "returns unlimited when user is on free_10k plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
assert :unlimited == Quota.Limits.team_member_limit(user)
|
||||
user = new_user()
|
||||
subscribe_to_plan(user, "free_10k")
|
||||
team = team_of(user)
|
||||
assert :unlimited == Plausible.Teams.Billing.team_member_limit(team)
|
||||
end
|
||||
|
||||
test "returns 5 when user in on trial" do
|
||||
user =
|
||||
insert(:user,
|
||||
trial_expiry_date: Timex.shift(Timex.now(), days: 7)
|
||||
)
|
||||
team = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: 7)) |> team_of()
|
||||
|
||||
assert 3 == Quota.Limits.team_member_limit(user)
|
||||
assert 3 == Plausible.Teams.Billing.team_member_limit(team)
|
||||
end
|
||||
|
||||
test "returns the enterprise plan limit" do
|
||||
@ -477,39 +429,33 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
end
|
||||
|
||||
test "reads from json file when the user is on a v4 plan" do
|
||||
user_on_growth = insert(:user, subscription: build(:growth_subscription))
|
||||
team_on_growth = new_user() |> subscribe_to_growth_plan() |> team_of()
|
||||
team_on_business = new_user() |> subscribe_to_business_plan() |> team_of()
|
||||
|
||||
user_on_business = insert(:user, subscription: build(:business_subscription))
|
||||
|
||||
assert 3 == Quota.Limits.team_member_limit(user_on_growth)
|
||||
assert 10 == Quota.Limits.team_member_limit(user_on_business)
|
||||
assert 3 == Plausible.Teams.Billing.team_member_limit(team_on_growth)
|
||||
assert 10 == Plausible.Teams.Billing.team_member_limit(team_on_business)
|
||||
end
|
||||
|
||||
test "returns unlimited when user is on a v3 business plan" do
|
||||
user =
|
||||
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
|
||||
team = new_user() |> subscribe_to_plan(@v3_business_plan_id) |> team_of()
|
||||
|
||||
assert :unlimited == Quota.Limits.team_member_limit(user)
|
||||
assert :unlimited == Plausible.Teams.Billing.team_member_limit(team)
|
||||
end
|
||||
end
|
||||
|
||||
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(nil, [insert(:site).id])
|
||||
assert [] == Plausible.Teams.Billing.features_usage(team_of(new_user()))
|
||||
assert [] == Plausible.Teams.Billing.features_usage(nil, [new_site().id])
|
||||
end
|
||||
|
||||
test "returns [Props] when user/site uses custom props" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
site = new_site(owner: user, allowed_event_props: ["dummy"])
|
||||
team = team_of(user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
allowed_event_props: ["dummy"],
|
||||
memberships: [build(:site_membership, user: user, role: :owner)]
|
||||
)
|
||||
|
||||
assert [Props] == Quota.Usage.features_usage(nil, [site.id])
|
||||
assert [Props] == Quota.Usage.features_usage(user)
|
||||
assert [Props] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||
assert [Props] == Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
@ -527,30 +473,32 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
end
|
||||
|
||||
test "returns [RevenueGoals] when user/site uses revenue goals" do
|
||||
user = insert(:user)
|
||||
site = insert(:site, memberships: [build(:site_membership, user: user, role: :owner)])
|
||||
user = new_user()
|
||||
team = team_of(user)
|
||||
site = new_site(owner: user)
|
||||
insert(:goal, currency: :USD, site: site, event_name: "Purchase")
|
||||
|
||||
assert [RevenueGoals] == Quota.Usage.features_usage(nil, [site.id])
|
||||
assert [RevenueGoals] == Quota.Usage.features_usage(user)
|
||||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(nil, [site.id])
|
||||
assert [RevenueGoals] == Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns [StatsAPI] when user has a stats api key" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
team = team_of(user)
|
||||
insert(:api_key, user: user)
|
||||
|
||||
assert [StatsAPI] == Quota.Usage.features_usage(user)
|
||||
assert [StatsAPI] == Plausible.Teams.Billing.features_usage(team)
|
||||
end
|
||||
|
||||
test "returns feature usage based on a user and a custom list of site_ids" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
team = team_of(user)
|
||||
insert(:api_key, user: user)
|
||||
|
||||
site_using_props = insert(:site, allowed_event_props: ["dummy"])
|
||||
site_using_props = new_site(allowed_event_props: ["dummy"])
|
||||
|
||||
site_ids = [site_using_props.id]
|
||||
assert [Props, StatsAPI] == Quota.Usage.features_usage(user, site_ids)
|
||||
assert [Props, StatsAPI] == Plausible.Teams.Billing.features_usage(team, site_ids)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
@ -585,24 +533,26 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
describe "allowed_features_for/1" do
|
||||
on_ee do
|
||||
test "users with expired trials have no access to subscription features" do
|
||||
user = insert(:user, trial_expiry_date: ~D[2023-01-01])
|
||||
assert [Goals] == Quota.Limits.allowed_features_for(user)
|
||||
team = new_user(trial_expiry_date: ~D[2023-01-01]) |> team_of()
|
||||
assert [Goals] == Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns all grandfathered features 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))
|
||||
team_on_v1 = new_user() |> subscribe_to_plan(@v1_plan_id) |> team_of()
|
||||
team_on_v2 = new_user() |> subscribe_to_plan(@v2_plan_id) |> team_of()
|
||||
team_on_v3 = new_user() |> subscribe_to_plan(@v3_plan_id) |> team_of()
|
||||
|
||||
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v1)
|
||||
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v2)
|
||||
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user_on_v3)
|
||||
assert [Goals, Props, StatsAPI] == Plausible.Teams.Billing.allowed_features_for(team_on_v1)
|
||||
assert [Goals, Props, StatsAPI] == Plausible.Teams.Billing.allowed_features_for(team_on_v2)
|
||||
assert [Goals, Props, StatsAPI] == Plausible.Teams.Billing.allowed_features_for(team_on_v3)
|
||||
end
|
||||
|
||||
test "returns [Goals, Props, StatsAPI] when user is on free_10k plan" do
|
||||
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
|
||||
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user)
|
||||
user = new_user()
|
||||
subscribe_to_plan(user, "free_10k")
|
||||
team = team_of(user)
|
||||
assert [Goals, Props, StatsAPI] == Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
||||
on_ee do
|
||||
@ -623,29 +573,28 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
end
|
||||
|
||||
test "returns all features when user in on trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 7))
|
||||
team = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: 7))
|
||||
|
||||
assert Plausible.Billing.Feature.list() == Quota.Limits.allowed_features_for(user)
|
||||
assert Plausible.Billing.Feature.list() ==
|
||||
Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
||||
test "returns previous plan limits 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)
|
||||
)
|
||||
new_user()
|
||||
|> subscribe_to_plan(@v1_plan_id)
|
||||
|> subscribe_to_enterprise_plan(subscription?: false)
|
||||
|
||||
assert [Goals, Props, StatsAPI] == Quota.Limits.allowed_features_for(user)
|
||||
team = team_of(user)
|
||||
|
||||
assert [Goals, Props, StatsAPI] == Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
||||
test "returns all features 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
|
||||
)
|
||||
team = new_user() |> subscribe_to_enterprise_plan(subscription?: false) |> team_of()
|
||||
|
||||
assert Plausible.Billing.Feature.list() == Quota.Limits.allowed_features_for(user)
|
||||
assert Plausible.Billing.Feature.list() ==
|
||||
Plausible.Teams.Billing.allowed_features_for(team)
|
||||
end
|
||||
|
||||
test "returns old plan features for enterprise customers who are due to change a plan" do
|
||||
@ -670,9 +619,7 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
|
||||
describe "monthly_pageview_usage/2" do
|
||||
test "returns empty usage for user without subscription and without any sites" do
|
||||
user =
|
||||
insert(:user)
|
||||
|> Plausible.Users.with_subscription()
|
||||
team = new_user() |> team_of()
|
||||
|
||||
assert %{
|
||||
last_30_days: %{
|
||||
@ -681,18 +628,16 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
pageviews: 0,
|
||||
date_range: date_range
|
||||
}
|
||||
} = Quota.Usage.monthly_pageview_usage(user)
|
||||
} = Plausible.Teams.Billing.monthly_pageview_usage(team)
|
||||
|
||||
assert date_range.last == Date.utc_today()
|
||||
assert Date.compare(date_range.first, date_range.last) == :lt
|
||||
end
|
||||
|
||||
test "returns usage for user without subscription with a site" do
|
||||
user =
|
||||
insert(:user)
|
||||
|> Plausible.Users.with_subscription()
|
||||
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user()
|
||||
site = new_site(owner: user)
|
||||
team = team_of(user)
|
||||
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
@ -712,12 +657,13 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
pageviews: 3,
|
||||
date_range: %{}
|
||||
}
|
||||
} = Quota.Usage.monthly_pageview_usage(user)
|
||||
} = Plausible.Teams.Billing.monthly_pageview_usage(team)
|
||||
end
|
||||
|
||||
test "pageleave events are not counted towards monthly pageview usage" do
|
||||
user = insert(:user) |> Plausible.Users.with_subscription()
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user()
|
||||
site = new_site(owner: user)
|
||||
team = team_of(user)
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
populate_stats(site, [
|
||||
@ -733,18 +679,16 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
pageviews: 1,
|
||||
date_range: %{}
|
||||
}
|
||||
} = Quota.Usage.monthly_pageview_usage(user)
|
||||
} = Plausible.Teams.Billing.monthly_pageview_usage(team)
|
||||
end
|
||||
|
||||
test "returns usage for user with subscription and a site" do
|
||||
today = Date.utc_today()
|
||||
user = new_user()
|
||||
subscribe_to_growth_plan(user, last_bill_date: Date.shift(today, day: -8))
|
||||
team = team_of(user)
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
subscription: build(:subscription, last_bill_date: Timex.shift(today, days: -8))
|
||||
)
|
||||
|
||||
site = insert(:site, members: [user])
|
||||
site = new_site(owner: user)
|
||||
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
@ -776,20 +720,19 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
pageviews: 0,
|
||||
date_range: %{}
|
||||
}
|
||||
} = Quota.Usage.monthly_pageview_usage(user)
|
||||
} = Plausible.Teams.Billing.monthly_pageview_usage(team)
|
||||
end
|
||||
|
||||
test "returns usage for only a subset of site IDs" do
|
||||
today = Date.utc_today()
|
||||
|
||||
user =
|
||||
insert(:user,
|
||||
subscription: build(:subscription, last_bill_date: Timex.shift(today, days: -8))
|
||||
)
|
||||
user = new_user()
|
||||
subscribe_to_growth_plan(user, last_bill_date: Date.shift(today, day: -8))
|
||||
team = team_of(user)
|
||||
|
||||
site1 = insert(:site, members: [user])
|
||||
site2 = insert(:site, members: [user])
|
||||
site3 = insert(:site, members: [user])
|
||||
site1 = new_site(owner: user)
|
||||
site2 = new_site(owner: user)
|
||||
site3 = new_site(owner: user)
|
||||
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
@ -823,14 +766,15 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
pageviews: 0,
|
||||
date_range: %{}
|
||||
}
|
||||
} = Quota.Usage.monthly_pageview_usage(user, [site1.id, site3.id])
|
||||
} = Plausible.Teams.Billing.monthly_pageview_usage(team, [site1.id, site3.id])
|
||||
end
|
||||
end
|
||||
|
||||
describe "usage_cycle/1" do
|
||||
setup do
|
||||
user = insert(:user)
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user()
|
||||
site = new_site(owner: user)
|
||||
team = team_of(user)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:event, timestamp: ~N[2023-04-01 00:00:00], name: "custom"),
|
||||
@ -850,45 +794,41 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
build(:event, timestamp: ~N[2023-06-05 00:00:00], name: "custom")
|
||||
])
|
||||
|
||||
{:ok, %{user: user}}
|
||||
{:ok, %{user: user, team: team}}
|
||||
end
|
||||
|
||||
test "returns usage and date_range for the given billing month", %{user: user} do
|
||||
test "returns usage and date_range for the given billing month", %{user: user, team: team} 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)
|
||||
subscribe_to_growth_plan(user, last_bill_date: last_bill_date)
|
||||
|
||||
assert %{date_range: penultimate_cycle, pageviews: 2, custom_events: 3, total: 5} =
|
||||
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :penultimate_cycle, nil, today)
|
||||
|
||||
assert %{date_range: last_cycle, pageviews: 3, custom_events: 2, total: 5} =
|
||||
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :last_cycle, nil, today)
|
||||
|
||||
assert %{date_range: current_cycle, pageviews: 0, custom_events: 3, total: 3} =
|
||||
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :current_cycle, nil, today)
|
||||
|
||||
assert penultimate_cycle == Date.range(~D[2023-04-03], ~D[2023-05-02])
|
||||
assert last_cycle == Date.range(~D[2023-05-03], ~D[2023-06-02])
|
||||
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
|
||||
test "returns usage and date_range for the last 30 days", %{team: team} do
|
||||
today = ~D[2023-06-01]
|
||||
|
||||
assert %{date_range: last_30_days, pageviews: 4, custom_events: 1, total: 5} =
|
||||
Quota.Usage.usage_cycle(user, :last_30_days, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :last_30_days, nil, 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)
|
||||
]
|
||||
)
|
||||
test "only considers sites that the user owns", %{user: user, team: team} do
|
||||
different_site = new_site()
|
||||
add_guest(different_site, user: user, role: :editor)
|
||||
|
||||
populate_stats(different_site, [
|
||||
build(:event, timestamp: ~N[2023-05-05 00:00:00], name: "custom")
|
||||
@ -897,10 +837,10 @@ defmodule Plausible.Billing.QuotaTest 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)
|
||||
subscribe_to_growth_plan(user, last_bill_date: last_bill_date)
|
||||
|
||||
assert %{date_range: last_cycle, pageviews: 3, custom_events: 2, total: 5} =
|
||||
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :last_cycle, nil, today)
|
||||
|
||||
assert last_cycle == Date.range(~D[2023-05-03], ~D[2023-06-02])
|
||||
end
|
||||
@ -909,16 +849,18 @@ defmodule Plausible.Billing.QuotaTest 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))
|
||||
user = new_user()
|
||||
subscribe_to_growth_plan(user, last_bill_date: last_bill_date)
|
||||
team = team_of(user)
|
||||
|
||||
assert %{date_range: penultimate_cycle} =
|
||||
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :penultimate_cycle, nil, today)
|
||||
|
||||
assert %{date_range: last_cycle} =
|
||||
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :last_cycle, nil, today)
|
||||
|
||||
assert %{date_range: current_cycle} =
|
||||
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :current_cycle, nil, today)
|
||||
|
||||
assert penultimate_cycle == Date.range(~D[2020-12-01], ~D[2020-12-31])
|
||||
assert last_cycle == Date.range(~D[2021-01-01], ~D[2021-01-31])
|
||||
@ -929,16 +871,18 @@ defmodule Plausible.Billing.QuotaTest 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))
|
||||
user = new_user()
|
||||
subscribe_to_growth_plan(user, last_bill_date: last_bill_date)
|
||||
team = team_of(user)
|
||||
|
||||
assert %{date_range: penultimate_cycle, total: 0} =
|
||||
Quota.Usage.usage_cycle(user, :penultimate_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :penultimate_cycle, nil, today)
|
||||
|
||||
assert %{date_range: last_cycle, total: 0} =
|
||||
Quota.Usage.usage_cycle(user, :last_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :last_cycle, nil, today)
|
||||
|
||||
assert %{date_range: current_cycle, total: 0} =
|
||||
Quota.Usage.usage_cycle(user, :current_cycle, nil, today)
|
||||
Plausible.Teams.Billing.usage_cycle(team, :current_cycle, nil, today)
|
||||
|
||||
assert penultimate_cycle == Date.range(~D[2020-11-01], ~D[2020-11-30])
|
||||
assert last_cycle == Date.range(~D[2020-12-01], ~D[2020-12-31])
|
||||
@ -948,16 +892,19 @@ defmodule Plausible.Billing.QuotaTest do
|
||||
|
||||
describe "suggest_tier/2" do
|
||||
setup do
|
||||
%{user: insert(:user) |> Plausible.Users.with_subscription()}
|
||||
user = new_user()
|
||||
team = user |> team_of() |> Plausible.Teams.with_subscription()
|
||||
%{user: user, team: team}
|
||||
end
|
||||
|
||||
test "returns nil if the monthly pageview limit exceeds regular plans",
|
||||
%{user: user} do
|
||||
%{team: team} do
|
||||
highest_growth_plan = Plausible.Billing.Plans.find(@v4_10m_growth_plan_id)
|
||||
highest_business_plan = Plausible.Billing.Plans.find(@v4_10m_business_plan_id)
|
||||
|
||||
usage =
|
||||
Quota.Usage.usage(user)
|
||||
team
|
||||
|> Plausible.Teams.Billing.quota_usage()
|
||||
|> Map.replace!(:monthly_pageviews, %{last_30_days: %{total: 12_000_000}})
|
||||
|
||||
suggested_tier =
|
||||
|
@ -7,15 +7,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
|
||||
describe "update_sites_for/1" do
|
||||
test "does not lock sites if user is on trial" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today())
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
locked: true,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
site = new_site(owner: user, locked: true)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == :unlocked
|
||||
|
||||
@ -23,16 +16,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "does not lock if user has an active subscription" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
locked: true,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
user = new_user() |> subscribe_to_growth_plan()
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == :unlocked
|
||||
|
||||
@ -40,15 +25,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "does not lock user who is past due" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.past_due(), user: user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
user = new_user() |> subscribe_to_growth_plan(status: Subscription.Status.past_due())
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == :unlocked
|
||||
|
||||
@ -56,15 +34,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "does not lock user who cancelled subscription but it hasn't expired yet" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.deleted(), user: user)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
user = new_user() |> subscribe_to_growth_plan(status: Subscription.Status.deleted())
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == :unlocked
|
||||
|
||||
@ -73,16 +44,12 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
|
||||
test "does not lock user who has an active subscription and is on grace period" do
|
||||
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: 1)}
|
||||
user = insert(:user, grace_period: grace_period)
|
||||
|
||||
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||
user =
|
||||
new_user(grace_period: grace_period, team: [grace_period: grace_period])
|
||||
|> subscribe_to_growth_plan()
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == :unlocked
|
||||
|
||||
@ -90,21 +57,15 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "locks user who cancelled subscription and the cancelled subscription has expired" do
|
||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||
|
||||
insert(:subscription,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||
user: user
|
||||
)
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
user =
|
||||
new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
next_bill_date: Date.utc_today() |> Date.shift(day: -1),
|
||||
status: Subscription.Status.deleted()
|
||||
)
|
||||
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == {:locked, :no_active_subscription}
|
||||
|
||||
assert Repo.reload!(site).locked
|
||||
@ -112,7 +73,7 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
|
||||
test "locks all sites if user has active subscription but grace period has ended" do
|
||||
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
|
||||
user = new_user(grace_period: grace_period)
|
||||
user = new_user(grace_period: grace_period, team: [grace_period: grace_period])
|
||||
subscribe_to_plan(user, "123")
|
||||
site = new_site(owner: user)
|
||||
|
||||
@ -124,7 +85,7 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
@tag :teams
|
||||
test "syncs grace period end with teams" do
|
||||
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
|
||||
user = new_user(grace_period: grace_period)
|
||||
user = new_user(grace_period: grace_period, team: [grace_period: grace_period])
|
||||
subscribe_to_plan(user, "123")
|
||||
new_site(owner: user)
|
||||
|
||||
@ -138,7 +99,7 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
|
||||
test "sends email if grace period has ended" do
|
||||
grace_period = %Plausible.Auth.GracePeriod{end_date: Timex.shift(Timex.today(), days: -1)}
|
||||
user = new_user(grace_period: grace_period)
|
||||
user = new_user(grace_period: grace_period, team: [grace_period: grace_period])
|
||||
subscribe_to_plan(user, "123")
|
||||
new_site(owner: user)
|
||||
|
||||
@ -156,7 +117,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
is_over: false
|
||||
}
|
||||
|
||||
user = new_user(grace_period: grace_period)
|
||||
user = new_user(grace_period: grace_period, team: [grace_period: grace_period])
|
||||
|
||||
subscribe_to_plan(user, "123")
|
||||
new_site(owner: user)
|
||||
|
||||
@ -174,14 +136,8 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "locks all sites if user has no trial or active subscription" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
site = new_site(owner: user)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == {:locked, :no_active_subscription}
|
||||
|
||||
@ -204,21 +160,11 @@ defmodule Plausible.Billing.SiteLockerTest do
|
||||
end
|
||||
|
||||
test "only locks sites that the user owns" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
|
||||
owner_site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :owner)
|
||||
]
|
||||
)
|
||||
|
||||
viewer_site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :viewer)
|
||||
]
|
||||
)
|
||||
owner_site = new_site(owner: user)
|
||||
viewer_site = new_site()
|
||||
add_guest(viewer_site, user: user, role: :viewer)
|
||||
|
||||
assert SiteLocker.update_sites_for(user) == {:locked, :no_active_subscription}
|
||||
|
||||
|
@ -18,7 +18,6 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
|
||||
assert {:ok, _} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(
|
||||
current_owner,
|
||||
[site1, site2],
|
||||
new_owner
|
||||
)
|
||||
@ -39,7 +38,6 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
|
||||
assert {:error, :transfer_to_self} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(
|
||||
current_owner,
|
||||
[site1, site2],
|
||||
new_owner
|
||||
)
|
||||
@ -56,7 +54,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
for _ <- 1..3, do: add_guest(site, role: :editor)
|
||||
|
||||
assert {:error, {:over_plan_limits, [:team_member_limit]}} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(old_owner, [site], new_owner)
|
||||
AcceptInvitation.bulk_transfer_ownership_direct([site], new_owner)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
@ -68,7 +66,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
for _ <- 1..2, do: add_guest(site, role: :editor)
|
||||
|
||||
assert {:ok, _} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(old_owner, [site], new_owner)
|
||||
AcceptInvitation.bulk_transfer_ownership_direct([site], new_owner)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
@ -81,7 +79,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
site = new_site(owner: old_owner)
|
||||
|
||||
assert {:error, {:over_plan_limits, [:site_limit]}} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(old_owner, [site], new_owner)
|
||||
AcceptInvitation.bulk_transfer_ownership_direct([site], new_owner)
|
||||
end
|
||||
|
||||
@tag :ee_only
|
||||
@ -102,7 +100,7 @@ defmodule Plausible.Site.Memberships.AcceptInvitationTest do
|
||||
for _ <- 1..3, do: add_guest(site, role: :editor)
|
||||
|
||||
assert {:error, {:over_plan_limits, [:team_member_limit, :site_limit]}} =
|
||||
AcceptInvitation.bulk_transfer_ownership_direct(old_owner, [site], new_owner)
|
||||
AcceptInvitation.bulk_transfer_ownership_direct([site], new_owner)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,15 +162,15 @@ defmodule Plausible.SitesTest do
|
||||
patch_env(:super_admin_user_ids, [user2.id])
|
||||
|
||||
%{id: site_id, domain: domain} = new_site(owner: user1)
|
||||
assert %{id: ^site_id} = Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain)
|
||||
assert %{id: ^site_id} = Plausible.Sites.get_for_user(user1, domain)
|
||||
|
||||
assert %{id: ^site_id} =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain, [:owner])
|
||||
Plausible.Sites.get_for_user(user1, domain, [:owner])
|
||||
|
||||
assert is_nil(Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain))
|
||||
assert is_nil(Plausible.Sites.get_for_user(user2, domain))
|
||||
|
||||
assert %{id: ^site_id} =
|
||||
Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain, [:super_admin])
|
||||
Plausible.Sites.get_for_user(user2, domain, [:super_admin])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1063,12 +1063,9 @@ defmodule PlausibleWeb.SettingsControllerTest do
|
||||
end
|
||||
|
||||
test "can't create api key into another site", %{conn: conn, user: me} do
|
||||
_my_site = insert(:site, memberships: [build(:site_membership, user: me, role: "owner")])
|
||||
|
||||
other_user = insert(:user)
|
||||
|
||||
_other_site =
|
||||
insert(:site, memberships: [build(:site_membership, user: other_user, role: "owner")])
|
||||
_my_site = new_site(owner: me)
|
||||
other_user = new_user()
|
||||
_other_site = new_site(owner: other_user)
|
||||
|
||||
conn =
|
||||
post(conn, Routes.settings_path(conn, :api_keys), %{
|
||||
|
@ -342,6 +342,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
user: user
|
||||
} do
|
||||
subscribe_to_enterprise_plan(user, site_limit: 1)
|
||||
team = team_of(user)
|
||||
new_site(owner: user)
|
||||
new_site(owner: user)
|
||||
|
||||
@ -354,7 +355,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == "/example.com/installation?site_created=true&flow="
|
||||
assert Plausible.Billing.Quota.Usage.site_usage(user) == 3
|
||||
assert Plausible.Teams.Billing.site_usage(team) == 3
|
||||
end
|
||||
|
||||
for url <- ["https://Example.com/", "HTTPS://EXAMPLE.COM/", "/Example.com/", "//Example.com/"] do
|
||||
|
@ -1,12 +1,13 @@
|
||||
defmodule PlausibleWeb.UnsubscribeControllerTest do
|
||||
use PlausibleWeb.ConnCase, async: true
|
||||
use Plausible.Repo
|
||||
use Plausible.Teams.Test
|
||||
|
||||
setup {PlausibleWeb.FirstLaunchPlug.Test, :skip}
|
||||
|
||||
describe "GET /sites/:domain/weekly-report/unsubscribe" do
|
||||
test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do
|
||||
site = insert(:site)
|
||||
site = new_site()
|
||||
insert(:weekly_report, site: site, recipients: ["recipient@email.com"])
|
||||
|
||||
conn =
|
||||
@ -35,7 +36,7 @@ defmodule PlausibleWeb.UnsubscribeControllerTest do
|
||||
|
||||
describe "GET /sites/:domain/monthly-report/unsubscribe" do
|
||||
test "removes a recipient from the weekly report without them having to log in", %{conn: conn} do
|
||||
site = insert(:site)
|
||||
site = new_site()
|
||||
insert(:monthly_report, site: site, recipients: ["recipient@email.com"])
|
||||
|
||||
conn =
|
||||
|
@ -281,7 +281,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
||||
user: user
|
||||
} do
|
||||
for _ <- 1..9, do: new_site(owner: user)
|
||||
assert 10 = Plausible.Teams.Adapter.Read.Billing.site_usage(user)
|
||||
assert user |> team_of() |> Plausible.Teams.Billing.site_usage() == 10
|
||||
|
||||
another_user = new_user()
|
||||
pending_ownership_site = new_site(owner: another_user)
|
||||
@ -611,11 +611,13 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
team = team_of(user)
|
||||
|
||||
for _ <- 1..49 do
|
||||
new_site(owner: user)
|
||||
end
|
||||
|
||||
assert 50 = Plausible.Teams.Adapter.Read.Billing.quota_usage(user).sites
|
||||
assert 50 = Plausible.Teams.Billing.quota_usage(team).sites
|
||||
|
||||
another_user = new_user()
|
||||
pending_ownership_site = new_site(owner: another_user)
|
||||
|
@ -8,13 +8,6 @@ Application.ensure_all_started(:double)
|
||||
|
||||
FunWithFlags.enable(:channels)
|
||||
FunWithFlags.enable(:scroll_depth)
|
||||
# Temporary flag to test `read_team_schemas` flag on all tests.
|
||||
if System.get_env("TEST_READ_TEAM_SCHEMAS") == "1" do
|
||||
IO.puts("READS TEAM SCHEMAS")
|
||||
FunWithFlags.enable(:read_team_schemas)
|
||||
else
|
||||
FunWithFlags.disable(:read_team_schemas)
|
||||
end
|
||||
|
||||
Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual)
|
||||
|
||||
|
@ -29,7 +29,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -37,17 +37,17 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
status: :active
|
||||
)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: "wont-exist-should-crash",
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
"wont-exist-should-crash",
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
inserted_at: DateTime.shift(DateTime.utc_now(), day: -2),
|
||||
status: :deleted
|
||||
)
|
||||
@ -62,7 +62,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
test "sends more than one email", %{user: user} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -74,11 +74,11 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
insert(:site, members: [user2])
|
||||
|
||||
for u <- [user, user2] do
|
||||
insert(:subscription,
|
||||
user: u,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
next_bill_date: Timex.shift(Timex.today(), days: +5),
|
||||
subscribe_to_plan(
|
||||
u,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
next_bill_date: Date.shift(Date.utc_today(), day: +5),
|
||||
status: :active
|
||||
)
|
||||
end
|
||||
@ -129,7 +129,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 9_000},
|
||||
@ -154,7 +154,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 10_999},
|
||||
@ -180,7 +180,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -188,10 +188,10 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
|
||||
@ -211,7 +211,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -219,10 +219,10 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
|
||||
@ -240,7 +240,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000_000},
|
||||
@ -248,9 +248,9 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
@ -267,7 +267,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
%{grace_period: existing_grace_period} = Plausible.Users.start_grace_period(user)
|
||||
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -292,7 +292,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -300,9 +300,9 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
@ -318,7 +318,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
test "clears grace period when plan is applicable again", %{user: user} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -326,10 +326,10 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
|
||||
@ -337,7 +337,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?()
|
||||
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -352,7 +352,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
@tag :teams
|
||||
test "syncs clearing grace period with teams", %{user: user} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -360,9 +360,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
subscribe_to_plan(user, @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1),
|
||||
status: unquote(status)
|
||||
)
|
||||
@ -374,7 +372,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
assert Plausible.Auth.GracePeriod.active?(team)
|
||||
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -395,7 +393,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
Plausible.Users.start_manual_lock_grace_period(user)
|
||||
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
|
||||
@ -423,7 +421,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
|
||||
@ -453,7 +451,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 1},
|
||||
@ -483,7 +481,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
test "manual lock grace period is synced with teams", %{user: user} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
|
||||
@ -510,7 +508,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
test "starts grace period when plan is outgrown", %{user: user} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 1_100_000},
|
||||
@ -538,7 +536,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -546,10 +544,10 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1)
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: Date.shift(Date.utc_today(), day: -1)
|
||||
)
|
||||
|
||||
CheckUsage.perform(nil, usage_stub)
|
||||
@ -564,7 +562,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -588,7 +586,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
user: user
|
||||
} do
|
||||
usage_stub =
|
||||
Plausible.Billing.Quota.Usage
|
||||
Plausible.Teams.Billing
|
||||
|> stub(:monthly_pageview_usage, fn _user ->
|
||||
%{
|
||||
penultimate_cycle: %{date_range: @date_range, total: 11_000},
|
||||
@ -596,9 +594,9 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
}
|
||||
end)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: @paddle_id_10k,
|
||||
subscribe_to_plan(
|
||||
user,
|
||||
@paddle_id_10k,
|
||||
last_bill_date: ~D[2021-06-29]
|
||||
)
|
||||
|
||||
|
@ -1,17 +1,16 @@
|
||||
defmodule Plausible.Workers.LockSitesTest do
|
||||
use Plausible.DataCase, async: true
|
||||
use Plausible.Teams.Test
|
||||
require Plausible.Billing.Subscription.Status
|
||||
alias Plausible.Workers.LockSites
|
||||
alias Plausible.Billing.Subscription
|
||||
|
||||
test "does not lock enterprise site on grace period" do
|
||||
user =
|
||||
:user
|
||||
|> build()
|
||||
|> Plausible.Auth.GracePeriod.start_manual_lock_changeset()
|
||||
|> Plausible.Repo.insert!()
|
||||
new_user()
|
||||
|> Plausible.Users.start_manual_lock_grace_period()
|
||||
|
||||
site = insert(:site, members: [user])
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -19,8 +18,8 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "does not lock trial user's site" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: 1))
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 1))
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -28,8 +27,8 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "locks site for user whose trial has expired" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -37,9 +36,8 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "does not lock active subsriber's sites" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user() |> subscribe_to_growth_plan(status: Subscription.Status.active())
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -47,9 +45,8 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "does not lock user who is past due" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.past_due(), user: user)
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user() |> subscribe_to_growth_plan(status: Subscription.Status.past_due())
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -57,9 +54,8 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "does not lock user who cancelled subscription but it hasn't expired yet" do
|
||||
user = insert(:user)
|
||||
insert(:subscription, status: Subscription.Status.deleted(), user: user)
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user() |> subscribe_to_growth_plan(status: Subscription.Status.deleted())
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -67,15 +63,14 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "locks user who cancelled subscription and the cancelled subscription has expired" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
user =
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Date.utc_today() |> Date.shift(day: -1)
|
||||
)
|
||||
|
||||
insert(:subscription,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||
user: user
|
||||
)
|
||||
|
||||
site = insert(:site, members: [user])
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
@ -83,18 +78,16 @@ defmodule Plausible.Workers.LockSitesTest do
|
||||
end
|
||||
|
||||
test "does not lock if user has an old cancelled subscription and a new active subscription" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
user =
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
|> subscribe_to_growth_plan(
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Date.utc_today() |> Date.shift(day: -1),
|
||||
inserted_at: NaiveDateTime.utc_now() |> NaiveDateTime.shift(day: -1)
|
||||
)
|
||||
|> subscribe_to_growth_plan(status: Subscription.Status.deleted())
|
||||
|
||||
insert(:subscription,
|
||||
status: Subscription.Status.deleted(),
|
||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||
user: user,
|
||||
inserted_at: Timex.now() |> Timex.shift(days: -1)
|
||||
)
|
||||
|
||||
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||
|
||||
site = insert(:site, members: [user])
|
||||
site = new_site(owner: user)
|
||||
|
||||
LockSites.perform(nil)
|
||||
|
||||
|
@ -6,10 +6,10 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
alias Plausible.Workers.SendTrialNotifications
|
||||
|
||||
test "does not send a notification if user didn't create a site" do
|
||||
insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7))
|
||||
insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 1))
|
||||
insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 0))
|
||||
insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: -1))
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 7))
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 1))
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 0))
|
||||
new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
|
||||
perform_job(SendTrialNotifications, %{})
|
||||
|
||||
@ -17,8 +17,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not send a notification if user does not have a trial" do
|
||||
user = insert(:user, trial_expiry_date: nil)
|
||||
insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: nil)
|
||||
new_site(members: user)
|
||||
|
||||
perform_job(SendTrialNotifications, %{})
|
||||
|
||||
@ -26,8 +26,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not send a notification if user created a site but there are no pageviews" do
|
||||
user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7))
|
||||
insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 7))
|
||||
new_site(owner: user)
|
||||
|
||||
perform_job(SendTrialNotifications, %{})
|
||||
|
||||
@ -35,14 +35,9 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not send a notification if user is a collaborator on sites but not an owner" do
|
||||
user = insert(:user, trial_expiry_date: Timex.now())
|
||||
|
||||
site =
|
||||
insert(:site,
|
||||
memberships: [
|
||||
build(:site_membership, user: user, role: :admin)
|
||||
]
|
||||
)
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
site = new_site()
|
||||
add_guest(site, user: user, role: :editor)
|
||||
|
||||
populate_stats(site, [build(:pageview)])
|
||||
|
||||
@ -53,8 +48,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
|
||||
describe "with site and pageviews" do
|
||||
test "sends a reminder 7 days before trial ends (16 days after user signed up)" do
|
||||
user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7))
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 7))
|
||||
site = new_site(owner: user)
|
||||
populate_stats(site, [build(:pageview)])
|
||||
|
||||
perform_job(SendTrialNotifications, %{})
|
||||
@ -63,8 +58,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
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])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 1))
|
||||
site = new_site(owner: user)
|
||||
usage = %{total: 3, custom_events: 0}
|
||||
|
||||
populate_stats(site, [
|
||||
@ -79,8 +74,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
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])
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
site = new_site(owner: user)
|
||||
usage = %{total: 3, custom_events: 0}
|
||||
|
||||
populate_stats(site, [
|
||||
@ -95,7 +90,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not include custom event note if user has not used custom events" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today())
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
usage = %{total: 9_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -105,7 +100,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "includes custom event note if user has used custom events" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today())
|
||||
user = new_user(trial_expiry_date: Date.utc_today())
|
||||
usage = %{total: 9_100, custom_events: 100}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -115,8 +110,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "sends a trial over email the day after the trial ends" do
|
||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: -1))
|
||||
site = new_site(owner: user)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview),
|
||||
@ -130,8 +125,8 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not send a notification if user has a subscription" do
|
||||
user = insert(:user, trial_expiry_date: Timex.now() |> Timex.shift(days: 7))
|
||||
site = insert(:site, members: [user])
|
||||
user = new_user(trial_expiry_date: Date.utc_today() |> Date.shift(day: 7))
|
||||
site = new_site(owner: user)
|
||||
|
||||
populate_stats(site, [
|
||||
build(:pageview),
|
||||
@ -139,7 +134,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
build(:pageview)
|
||||
])
|
||||
|
||||
insert(:subscription, user: user)
|
||||
subscribe_to_growth_plan(user)
|
||||
|
||||
perform_job(SendTrialNotifications, %{})
|
||||
|
||||
@ -149,7 +144,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
|
||||
describe "Suggested plans" do
|
||||
test "suggests 10k/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 9_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -157,7 +152,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 100k/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 90_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -165,7 +160,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 200k/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 180_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -173,7 +168,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 500k/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 450_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -181,7 +176,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 1m/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 900_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -189,7 +184,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 2m/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 1_800_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -197,7 +192,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 5m/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 4_500_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -205,7 +200,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "suggests 10m/mo plan" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 9_000_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
@ -213,7 +208,7 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
||||
end
|
||||
|
||||
test "does not suggest a plan above that" do
|
||||
user = insert(:user)
|
||||
user = new_user()
|
||||
usage = %{total: 20_000_000, custom_events: 0}
|
||||
|
||||
email = PlausibleWeb.Email.trial_upgrade_email(user, "today", usage)
|
||||
|
Loading…
Reference in New Issue
Block a user