Switch sites creation to read teams schemas (#4823)

* Expose site limit, usage, ensure_can_add_new_site via Adapter

* Print to stdout if TEST_READ_TEAM_SCHEMAS is enabled

* Add factory wrappers for remaining subscription types

* Ensure consistent ordering when fetching latest subscription

* Switch creating new site to read team schemas

* Dedup code based on read team schemas switching

* Switch to transitional factory where necessary

* Update yet another test requiring transitional factory
This commit is contained in:
hq1 2024-11-14 12:03:10 +01:00 committed by GitHub
parent 9b6961ce9b
commit 0d6bec1bbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 174 additions and 105 deletions

View File

@ -6,7 +6,6 @@ defmodule Plausible.Sites do
import Ecto.Query
alias Plausible.Auth
alias Plausible.Billing.Quota
alias Plausible.Repo
alias Plausible.Site
alias Plausible.Site.SharedLink
@ -84,7 +83,7 @@ defmodule Plausible.Sites do
end
def create(user, params) do
with :ok <- Quota.ensure_can_add_new_site(user) 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 ->

View File

@ -138,7 +138,7 @@ defmodule Plausible.Teams do
defp last_subscription_query() do
from(subscription in Plausible.Billing.Subscription,
order_by: [desc: subscription.inserted_at],
order_by: [desc: subscription.inserted_at, desc: subscription.id],
limit: 1
)
end

View File

@ -0,0 +1,38 @@
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 team_or_user(user) do
switch(
user,
team_fn: &Function.identity/1,
user_fn: &Function.identity/1
)
end
def switch(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_fn.(team)
else
user_fn.(user)
end
end
end

View File

@ -2,19 +2,36 @@ defmodule Plausible.Teams.Adapter.Read.Billing do
@moduledoc """
Transition adapter for new schema reads
"""
alias Plausible.Teams
use Plausible.Teams.Adapter
def check_needs_to_upgrade(user) do
if Teams.read_team_schemas?(user) do
team =
case Teams.get_by_owner(user) do
{:ok, team} -> team
{:error, _} -> nil
end
switch(
user,
team_fn: &Teams.Billing.check_needs_to_upgrade/1,
user_fn: &Plausible.Billing.check_needs_to_upgrade/1
)
end
Teams.Billing.check_needs_to_upgrade(team)
else
Plausible.Billing.check_needs_to_upgrade(user)
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
end

View File

@ -3,59 +3,47 @@ defmodule Plausible.Teams.Adapter.Read.Ownership do
Transition adapter for new schema reads
"""
use Plausible
use Plausible.Teams.Adapter
alias Plausible.Site
alias Plausible.Auth
alias Plausible.Teams
alias Plausible.Site.Memberships.Invitations
def ensure_can_take_ownership(site, user) do
if Teams.read_team_schemas?(user) do
team =
case Teams.get_by_owner(user) do
{:ok, team} -> team
{:error, _} -> nil
end
Teams.Invitations.ensure_can_take_ownership(site, team)
else
Invitations.ensure_can_take_ownership(site, user)
end
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
if Teams.read_team_schemas?(user) do
Teams.Users.has_sites?(user, include_pending?: true)
else
Site.Memberships.any_or_pending?(user)
end
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
if Teams.read_team_schemas?(user) do
Teams.Users.owns_sites?(user, include_pending?: true)
else
Enum.any?(sites.entries, fn site ->
length(site.invitations) > 0 && List.first(site.invitations).role == :owner
end) ||
Auth.user_owns_sites?(user)
end
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
user_or_team =
if Teams.read_team_schemas?(new_owner) do
case Teams.get_by_owner(new_owner) do
{:ok, team} -> team
{:error, _} -> nil
end
else
new_owner
end
team_or_user = team_or_user(new_owner)
missing_features =
Plausible.Billing.Quota.Usage.features_usage(nil, [site.id])
|> Enum.filter(&(&1.check_availability(user_or_team) != :ok))
|> Enum.filter(&(&1.check_availability(team_or_user) != :ok))
if missing_features == [] do
:ok

View File

@ -5,30 +5,29 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
import Ecto.Query
alias Plausible.Auth
alias Plausible.Repo
alias Plausible.Site
alias Plausible.Teams
use Plausible.Teams.Adapter
def list(user, pagination_params, opts \\ []) do
if Plausible.Teams.read_team_schemas?(user) do
Plausible.Teams.Sites.list(user, pagination_params, opts)
else
old_list(user, pagination_params, opts)
end
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
if Plausible.Teams.read_team_schemas?(user) do
Plausible.Teams.Sites.list_with_invitations(user, pagination_params, opts)
else
old_list_with_invitations(user, pagination_params, opts)
end
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
@type list_opt() :: {:filter_by_domain, String.t()}
@spec old_list(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t()
def old_list(user, pagination_params, opts \\ []) do
defp old_list(user, pagination_params, opts) do
domain_filter = Keyword.get(opts, :filter_by_domain)
from(s in Site,
@ -60,8 +59,7 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
|> Repo.paginate(pagination_params)
end
@spec old_list_with_invitations(Auth.User.t(), map(), [list_opt()]) :: Scrivener.Page.t()
def old_list_with_invitations(user, pagination_params, opts \\ []) do
defp old_list_with_invitations(user, pagination_params, opts) do
domain_filter = Keyword.get(opts, :filter_by_domain)
result =

View File

@ -42,6 +42,10 @@ defmodule Plausible.Teams.Billing do
end
end
def ensure_can_add_new_site(nil) do
:ok
end
def ensure_can_add_new_site(team) do
team = Teams.with_subscription(team)
@ -61,6 +65,10 @@ defmodule Plausible.Teams.Billing do
end
end
def site_limit(nil) do
@site_limit_for_trials
end
def site_limit(team) do
if Timex.before?(team.inserted_at, @limit_sites_since) do
:unlimited
@ -69,6 +77,8 @@ defmodule Plausible.Teams.Billing do
end
end
def site_usage(nil), do: 0
def site_usage(team) do
team
|> Teams.owned_sites()
@ -76,7 +86,8 @@ defmodule Plausible.Teams.Billing do
end
defp get_site_limit_from_plan(team) do
team = Teams.with_subscription(team)
team =
Teams.with_subscription(team)
case Plans.get_subscription_plan(team.subscription) do
%{site_limit: site_limit} -> site_limit

View File

@ -4,7 +4,6 @@ defmodule PlausibleWeb.SiteController do
use Plausible
alias Plausible.Sites
alias Plausible.Billing.Quota
plug(PlausibleWeb.RequireAccountPlug)
@ -19,8 +18,9 @@ defmodule PlausibleWeb.SiteController do
render(conn, "new.html",
changeset: Plausible.Site.changeset(%Plausible.Site{}),
site_limit: Quota.Limits.site_limit(current_user),
site_limit_exceeded?: Quota.ensure_can_add_new_site(current_user) != :ok,
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,
form_submit_url: "/sites?flow=#{flow}",
flow: flow
)
@ -28,7 +28,7 @@ defmodule PlausibleWeb.SiteController do
def create_site(conn, %{"site" => site_params}) do
user = conn.assigns[:current_user]
first_site? = Quota.Usage.site_usage(user) == 0
first_site? = Plausible.Teams.Adapter.Read.Billing.site_usage(user) == 0
flow = conn.params["flow"]
case Sites.create(user, site_params) do
@ -60,7 +60,7 @@ defmodule PlausibleWeb.SiteController do
render(conn, "new.html",
changeset: changeset,
first_site?: first_site?,
site_limit: Quota.Limits.site_limit(user),
site_limit: Plausible.Teams.Adapter.Read.Billing.site_limit(user),
site_limit_exceeded?: false,
flow: flow,
form_submit_url: "/sites?flow=#{flow}"

View File

@ -4,6 +4,8 @@ defmodule Plausible.Billing.QuotaTest do
alias Plausible.Billing.{Quota, Plans}
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI}
use Plausible.Teams.Test
on_ee do
alias Plausible.Billing.Feature.Funnels
alias Plausible.Billing.Feature.RevenueGoals
@ -22,56 +24,44 @@ defmodule Plausible.Billing.QuotaTest do
@describetag :ee_only
test "returns 50 when user is on an old plan" do
user_on_v1 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
user_on_v2 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
user_on_v3 = insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_plan_id))
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)
assert 50 == Quota.Limits.site_limit(user_on_v1)
assert 50 == Quota.Limits.site_limit(user_on_v2)
assert 50 == Quota.Limits.site_limit(user_on_v3)
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)
end
test "returns 50 when user is on free_10k plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: "free_10k"))
assert 50 == Quota.Limits.site_limit(user)
user = new_user() |> subscribe_to_plan("free_10k")
assert 50 == Plausible.Teams.Adapter.Read.Billing.site_limit(user)
end
test "returns the configured site limit for enterprise plan" do
user = insert(:user)
enterprise_plan = insert(:enterprise_plan, user_id: user.id, site_limit: 500)
insert(:subscription, user_id: user.id, paddle_plan_id: enterprise_plan.paddle_plan_id)
assert enterprise_plan.site_limit == Quota.Limits.site_limit(user)
user = new_user() |> subscribe_to_enterprise_plan(site_limit: 500)
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 500
end
test "returns 10 when user in on trial" do
user =
insert(:user,
trial_expiry_date: Timex.shift(Timex.now(), days: 7)
)
assert 10 == Quota.Limits.site_limit(user)
user = new_user(trial_expiry_date: Date.shift(Date.utc_today(), day: 7))
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 10
end
test "returns the subscription limit for enterprise users who have not paid yet" do
user =
insert(:user,
enterprise_plan: build(:enterprise_plan, paddle_plan_id: "123321"),
subscription: build(:subscription, paddle_plan_id: @v1_plan_id)
)
new_user()
|> subscribe_to_plan(@v1_plan_id)
|> subscribe_to_enterprise_plan(paddle_plan_id: "123321", subscription?: false)
assert 50 == Quota.Limits.site_limit(user)
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 50
end
test "returns 10 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
)
new_user() |> subscribe_to_enterprise_plan(paddle_plan_id: "123321", subscription?: false)
assert 10 == Quota.Limits.site_limit(user)
assert Plausible.Teams.Adapter.Read.Billing.site_limit(user) == 10
end
end

View File

@ -2,6 +2,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
use Plausible
use PlausibleWeb.ConnCase, async: false
use Plausible.Repo
use Plausible.Teams.Test
on_ee do
setup :create_user
@ -77,7 +78,7 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
end
test "does not allow creating more sites than the limit", %{conn: conn, user: user} do
insert_list(50, :site, members: [user])
for _ <- 1..10, do: new_site(owner: user)
conn =
post(conn, "/api/v1/sites", %{

View File

@ -284,7 +284,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn: conn,
user: user
} do
insert(:site, members: [user])
new_site(owner: user)
post(conn, "/sites", %{
"site" => %{
@ -301,7 +301,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn: conn,
user: user
} do
insert_list(10, :site, members: [user])
for _ <- 1..10, do: new_site(owner: user)
conn =
post(conn, "/sites", %{

View File

@ -139,6 +139,32 @@ defmodule Plausible.Teams.Test do
{:ok, team} = Teams.get_or_create(user)
insert(:growth_subscription, user: user, team: team)
user
end
def subscribe_to_plan(user, paddle_plan_id) do
{:ok, team} = Teams.get_or_create(user)
insert(:subscription, user: user, team: team, paddle_plan_id: paddle_plan_id)
user
end
def subscribe_to_enterprise_plan(user, attrs) do
{:ok, team} = Teams.get_or_create(user)
{subscription?, attrs} = Keyword.pop(attrs, :subscription?, true)
enterprise_plan = insert(:enterprise_plan, Keyword.merge([user: user, team: team], attrs))
if subscription? do
insert(:subscription,
team: team,
user: user,
paddle_plan_id: enterprise_plan.paddle_plan_id
)
end
user
end
def assert_team_exists(user, team_id \\ nil) do

View File

@ -9,6 +9,7 @@ Application.ensure_all_started(:double)
FunWithFlags.enable(:channels)
# 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)