Switch on team schema in site settings controller actions and LVs (#4834)

* Populate `current_team` to site's team and make site and subscription preloads consistent

* Accept only full `User` struct in `Users.get_for_user(!)`

* Make all uses of `Sites.get_for_user(!)` switch on team schema

* Remove redundant preloads for funnel/props settings

* Use adapter transitions in subscription settings

* Use team's schema subscription when listing invoices

* Fix typespec

* Turn owned site IDs into a specific query

* Add clauses for when FF is on but no team has been created

* Fix formatting

---------

Co-authored-by: Adam Rutkowski <hq@mtod.org>
This commit is contained in:
Adrian Gruntkowski 2024-11-19 10:49:37 +01:00 committed by GitHub
parent 9af498833e
commit 4ff2a66548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 401 additions and 261 deletions

View File

@ -206,7 +206,7 @@ defmodule PlausibleWeb.Api.ExternalSitesController do
end
defp get_site(user, site_id, roles) do
case Sites.get_for_user(user.id, site_id, roles) do
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, site_id, roles) do
nil -> {:error, :site_not_found}
site -> {:ok, site}
end

View File

@ -6,7 +6,7 @@ defmodule PlausibleWeb.Live.FunnelSettings do
use Plausible.Funnel
alias Plausible.{Sites, Goals, Funnels}
alias Plausible.{Goals, Funnels}
def mount(
_params,
@ -16,7 +16,11 @@ defmodule PlausibleWeb.Live.FunnelSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:all_funnels, fn %{site: %{id: ^site_id} = site} ->
Funnels.list(site)
@ -102,7 +106,11 @@ defmodule PlausibleWeb.Live.FunnelSettings do
def handle_event("delete-funnel", %{"funnel-id" => id}, socket) do
site =
Sites.get_for_user!(socket.assigns.current_user, socket.assigns.domain, [:owner, :admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(
socket.assigns.current_user,
socket.assigns.domain,
[:owner, :admin]
)
id = String.to_integer(id)
:ok = Funnels.delete(site, id)

View File

@ -9,11 +9,15 @@ defmodule PlausibleWeb.Live.FunnelSettings.Form do
use Plausible.Funnel
import PlausibleWeb.Live.Components.Form
alias Plausible.{Sites, Goals, Funnels}
alias Plausible.{Goals, Funnels}
def mount(_params, %{"domain" => domain} = session, socket) do
site =
Sites.get_for_user!(socket.assigns.current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin
])
# We'll have the options trimmed to only the data we care about, to keep
# it minimal at the socket assigns, yet, we want to retain specific %Goal{}

View File

@ -83,7 +83,7 @@ defmodule Plausible.Billing.PaddleApi do
end
end
@spec get_invoices(Plausible.Billing.Subscription.t()) ::
@spec get_invoices(Plausible.Billing.Subscription.t() | nil) ::
{:ok, list()}
| {:error, :request_failed}
| {:error, :no_invoices}

View File

@ -58,7 +58,7 @@ defmodule Plausible.Billing.Quota.Limits do
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
@spec monthly_pageview_limit(User.t() | Subscription.t()) ::
@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)

View File

@ -55,7 +55,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
get_for_user!(user.id, site.domain)
Plausible.Teams.Adapter.Read.Sites.get_for_user!(user, site.domain)
user
|> Site.UserPreference.changeset(site, %{option => value})
@ -91,7 +91,7 @@ defmodule Plausible.Sites do
end)
|> Ecto.Multi.run(:clear_changed_from, fn
_repo, %{site_changeset: %{changes: %{domain: domain}}} ->
case get_for_user(user.id, domain, [:owner]) do
case Plausible.Teams.Adapter.Read.Sites.get_for_user(user, domain, [:owner]) do
%Site{domain_changed_from: ^domain} = site ->
site
|> Ecto.Changeset.change()
@ -204,46 +204,6 @@ defmodule Plausible.Sites do
base <> domain <> "?auth=" <> link.slug
end
@spec get_for_user!(Auth.User.t() | pos_integer(), String.t(), [
:super_admin | :owner | :admin | :viewer
]) ::
Site.t()
def get_for_user!(user, domain, roles \\ [:owner, :admin, :viewer])
def get_for_user!(%Auth.User{id: user_id}, domain, roles) do
get_for_user!(user_id, domain, roles)
end
def get_for_user!(user_id, domain, roles) do
if :super_admin in roles and Auth.is_super_admin?(user_id) do
get_by_domain!(domain)
else
user_id
|> get_for_user_q(domain, List.delete(roles, :super_admin))
|> Repo.one!()
end
end
@spec get_for_user(Auth.User.t() | pos_integer(), String.t(), [
:super_admin | :owner | :admin | :viewer
]) ::
Site.t() | nil
def get_for_user(user, domain, roles \\ [:owner, :admin, :viewer])
def get_for_user(%Auth.User{id: user_id}, domain, roles) do
get_for_user(user_id, domain, roles)
end
def get_for_user(user_id, domain, roles) do
if :super_admin in roles and Auth.is_super_admin?(user_id) do
get_by_domain(domain)
else
user_id
|> get_for_user_q(domain, List.delete(roles, :super_admin))
|> Repo.one()
end
end
def update_installation_meta!(site, meta) do
site
|> Ecto.Changeset.change()
@ -251,17 +211,6 @@ defmodule Plausible.Sites do
|> Repo.update!()
end
defp get_for_user_q(user_id, domain, roles) do
from(s in Site,
join: sm in 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
def has_goals?(site) do
Repo.exists?(
from(g in Plausible.Goal,

View File

@ -9,8 +9,9 @@ defmodule Plausible.Teams do
alias Plausible.Repo
use Plausible
@spec on_trial?(Teams.Team.t()) :: boolean()
@spec on_trial?(Teams.Team.t() | nil) :: boolean()
on_ee do
def on_trial?(nil), do: false
def on_trial?(%Teams.Team{trial_expiry_date: nil}), do: false
def on_trial?(team) do
@ -38,6 +39,19 @@ defmodule Plausible.Teams do
Repo.preload(team, :sites).sites
end
def owned_sites_ids(nil) do
[]
end
def owned_sites_ids(team) do
Repo.all(
from s in Plausible.Site,
where: s.team_id == ^team.id,
select: s.id,
order_by: [desc: s.id]
)
end
@doc """
Create (when necessary) and load team relation for provided site.
@ -110,6 +124,19 @@ defmodule Plausible.Teams do
end
end
def last_subscription_join_query() do
from(subscription in last_subscription_query(),
where: subscription.team_id == parent_as(:team).id
)
end
def last_subscription_query() do
from(subscription in Plausible.Billing.Subscription,
order_by: [desc: subscription.inserted_at, desc: subscription.id],
limit: 1
)
end
defp create_my_team(user) do
team =
"My Team"
@ -135,11 +162,4 @@ defmodule Plausible.Teams do
{:error, :exists_already}
end
end
defp last_subscription_query() do
from(subscription in Plausible.Billing.Subscription,
order_by: [desc: subscription.inserted_at, desc: subscription.id],
limit: 1
)
end
end

View File

@ -11,6 +11,13 @@ defmodule Plausible.Teams.Adapter do
end
end
def user_or_team(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)
@ -22,8 +29,11 @@ defmodule Plausible.Teams.Adapter do
{:error, _} -> nil
end
team = Plausible.Teams.with_subscription(team)
team_fn.(team)
else
user = Plausible.Users.with_subscription(user)
user_fn.(user)
end
end

View File

@ -4,6 +4,42 @@ defmodule Plausible.Teams.Adapter.Read.Billing do
"""
use Plausible.Teams.Adapter
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,

View File

@ -3,11 +3,13 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
Transition adapter for new schema reads
"""
use Plausible.Teams.Adapter
import Ecto.Query
alias Plausible.Repo
alias Plausible.Site
use Plausible.Teams.Adapter
alias Plausible.Teams
def list(user, pagination_params, opts \\ []) do
switch(
@ -182,6 +184,73 @@ defmodule Plausible.Teams.Adapter.Read.Sites do
end
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
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,
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}%"))

View File

@ -85,6 +85,10 @@ defmodule Plausible.Teams.Billing do
|> length()
end
defp get_site_limit_from_plan(nil) do
@site_limit_for_trials
end
defp get_site_limit_from_plan(team) do
team =
Teams.with_subscription(team)
@ -96,6 +100,10 @@ defmodule Plausible.Teams.Billing do
end
end
def team_member_limit(nil) do
@team_member_limit_for_trials
end
def team_member_limit(team) do
team = Teams.with_subscription(team)
@ -110,7 +118,7 @@ defmodule Plausible.Teams.Billing do
team = Teams.with_subscription(team)
with_features? = Keyword.get(opts, :with_features, false)
pending_site_ids = Keyword.get(opts, :pending_ownership_site_ids, [])
team_site_ids = team |> Teams.owned_sites() |> Enum.map(& &1.id)
team_site_ids = Teams.owned_sites_ids(team)
all_site_ids = pending_site_ids ++ team_site_ids
monthly_pageviews = monthly_pageview_usage(team, all_site_ids)
@ -129,6 +137,50 @@ defmodule Plausible.Teams.Billing do
end
end
@monthly_pageview_limit_for_free_10k 10_000
@monthly_pageview_limit_for_trials :unlimited
def monthly_pageview_limit(nil) do
@monthly_pageview_limit_for_trials
end
def monthly_pageview_limit(%Teams.Team{} = team) do
team = Teams.with_subscription(team)
monthly_pageview_limit(team.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 monthly_pageview_usage(team, site_ids \\ nil)
def monthly_pageview_usage(team, nil) do
monthly_pageview_usage(team, Teams.owned_sites_ids(team))
end
def monthly_pageview_usage(nil, _site_ids) do
%{last_30_days: usage_cycle(nil, :last_30_days, [])}
end
def monthly_pageview_usage(team, site_ids) do
team = Teams.with_subscription(team)
active_subscription? = Subscriptions.active?(team.subscription)

View File

@ -191,7 +191,7 @@ defmodule Plausible.Users do
user
end
defp last_subscription_query() do
def last_subscription_query() do
from(subscription in Subscription,
order_by: [desc: subscription.inserted_at],
limit: 1

View File

@ -12,7 +12,7 @@ defmodule PlausibleWeb.AdminController do
usage = Quota.Usage.usage(user, with_features: true)
limits = %{
monthly_pageviews: Quota.Limits.monthly_pageview_limit(user),
monthly_pageviews: Quota.Limits.monthly_pageview_limit(user.subscription),
sites: Quota.Limits.site_limit(user),
team_members: Quota.Limits.team_member_limit(user)
}

View File

@ -5,8 +5,6 @@ defmodule PlausibleWeb.SettingsController do
alias Plausible.Auth
alias PlausibleWeb.UserAuth
alias Plausible.Billing.Quota
require Logger
def index(conn, _params) do
@ -23,22 +21,25 @@ defmodule PlausibleWeb.SettingsController do
def subscription(conn, _params) do
current_user = conn.assigns.current_user
subscription = Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
render(conn, :subscription,
layout: {PlausibleWeb.LayoutView, :settings},
subscription: current_user.subscription,
pageview_limit: Quota.Limits.monthly_pageview_limit(current_user),
pageview_usage: Quota.Usage.monthly_pageview_usage(current_user),
site_usage: Quota.Usage.site_usage(current_user),
site_limit: Quota.Limits.site_limit(current_user),
team_member_limit: Quota.Limits.team_member_limit(current_user),
team_member_usage: Quota.Usage.team_member_usage(current_user)
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)
)
end
def invoices(conn, _params) do
current_user = conn.assigns.current_user
invoices = Plausible.Billing.paddle_api().get_invoices(current_user.subscription)
subscription =
Plausible.Teams.Adapter.Read.Billing.get_subscription(conn.assigns.current_user)
invoices = Plausible.Billing.paddle_api().get_invoices(subscription)
render(conn, :invoices, layout: {PlausibleWeb.LayoutView, :settings}, invoices: invoices)
end

View File

@ -13,7 +13,6 @@ defmodule PlausibleWeb.Site.MembershipController do
use PlausibleWeb, :controller
use Plausible.Repo
use Plausible
alias Plausible.Sites
alias Plausible.Site.{Membership, Memberships}
@only_owner_is_allowed_to [:transfer_ownership_form, :transfer_ownership]
@ -26,8 +25,8 @@ defmodule PlausibleWeb.Site.MembershipController do
def invite_member_form(conn, _params) do
site =
conn.assigns.current_user.id
|> Sites.get_for_user!(conn.assigns.site.domain)
conn.assigns.current_user
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.site.domain)
|> Plausible.Repo.preload(:owner)
limit = Plausible.Billing.Quota.Limits.team_member_limit(site.owner)
@ -45,10 +44,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def invite_member(conn, %{"email" => email, "role" => role}) do
site_domain = conn.assigns[:site].domain
site_domain = conn.assigns.site.domain
site =
Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
Plausible.Teams.Adapter.Read.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
@ -94,8 +93,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def transfer_ownership_form(conn, _params) do
site_domain = conn.assigns[:site].domain
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
site_domain = conn.assigns.site.domain
site =
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
render(
conn,
@ -106,8 +107,10 @@ defmodule PlausibleWeb.Site.MembershipController do
end
def transfer_ownership(conn, %{"email" => email}) do
site_domain = conn.assigns[:site].domain
site = Sites.get_for_user!(conn.assigns[:current_user].id, site_domain)
site_domain = conn.assigns.site.domain
site =
Plausible.Teams.Adapter.Read.Sites.get_for_user!(conn.assigns.current_user, site_domain)
case Memberships.create_invitation(site, conn.assigns.current_user, email, :owner) do
{:ok, _invitation} ->

View File

@ -152,13 +152,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_goals(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_goals.html",
site: site,
dogfood_page_path: "/:dashboard/settings/goals",
connect_live_socket: true,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
@ -166,13 +161,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_funnels(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_funnels.html",
site: site,
dogfood_page_path: "/:dashboard/settings/funnels",
connect_live_socket: true,
layout: {PlausibleWeb.LayoutView, "site_settings.html"}
@ -180,13 +170,8 @@ defmodule PlausibleWeb.SiteController do
end
def settings_props(conn, _params) do
site = Repo.preload(conn.assigns[:site], [:owner])
owner = Plausible.Users.with_subscription(site.owner)
site = Map.put(site, :owner, owner)
conn
|> render("settings_props.html",
site: site,
dogfood_page_path: "/:dashboard/settings/properties",
layout: {PlausibleWeb.LayoutView, "site_settings.html"},
connect_live_socket: true

View File

@ -4,7 +4,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
"""
use PlausibleWeb, :live_view
alias Plausible.{Sites, Goals}
alias Plausible.Goals
alias PlausibleWeb.Live.Components.Modal
def mount(
@ -16,7 +16,7 @@ defmodule PlausibleWeb.Live.GoalSettings do
socket
|> assign_new(:site, fn %{current_user: current_user} ->
current_user
|> Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|> Plausible.Teams.Adapter.Read.Sites.get_for_user!(domain, [:owner, :admin, :super_admin])
|> Plausible.Imported.load_import_data()
end)
|> assign_new(:all_goals, fn %{site: site} ->

View File

@ -8,7 +8,6 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
alias Plausible.Imported
alias Plausible.Imported.SiteImport
alias Plausible.Sites
require Plausible.Imported.SiteImport
@ -16,7 +15,11 @@ defmodule PlausibleWeb.Live.ImportsExportsSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:site_imports, fn %{site: site} ->
site

View File

@ -32,7 +32,7 @@ defmodule PlausibleWeb.Live.Installation do
socket
) do
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin,

View File

@ -5,14 +5,17 @@ defmodule PlausibleWeb.Live.Plugins.API.Settings do
use PlausibleWeb, :live_view
alias Plausible.Sites
alias Plausible.Plugins.API.Tokens
def mount(_params, %{"domain" => domain} = session, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:displayed_tokens, fn %{site: site} ->
Tokens.list(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
use PlausibleWeb, live_view: :no_sentry_context
import PlausibleWeb.Live.Components.Form
alias Plausible.Sites
alias Plausible.Plugins.API.{Token, Tokens}
def mount(
@ -20,7 +19,11 @@ defmodule PlausibleWeb.Live.Plugins.API.TokenForm do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
token = Token.generate()

View File

@ -11,7 +11,11 @@ defmodule PlausibleWeb.Live.PropsSettings do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Plausible.Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:all_props, fn %{site: site} ->
site.allowed_event_props || []

View File

@ -18,7 +18,11 @@ defmodule PlausibleWeb.Live.PropsSettings.Form do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Plausible.Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:form, fn %{site: site} ->
new_form(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Shields.Countries do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(
_params,
@ -15,7 +14,11 @@ defmodule PlausibleWeb.Live.Shields.Countries do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:country_rules_count, fn %{site: site} ->
Shields.count_country_rules(site)

View File

@ -5,13 +5,16 @@ defmodule PlausibleWeb.Live.Shields.Hostnames do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(_params, %{"domain" => domain}, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:hostname_rules_count, fn %{site: site} ->
Shields.count_hostname_rules(site)

View File

@ -5,7 +5,6 @@ defmodule PlausibleWeb.Live.Shields.IPAddresses do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(
_params,
@ -18,7 +17,11 @@ defmodule PlausibleWeb.Live.Shields.IPAddresses do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:ip_rules_count, fn %{site: site} ->
Shields.count_ip_rules(site)

View File

@ -5,13 +5,16 @@ defmodule PlausibleWeb.Live.Shields.Pages do
use PlausibleWeb, :live_view
alias Plausible.Shields
alias Plausible.Sites
def mount(_params, %{"domain" => domain}, socket) do
socket =
socket
|> assign_new(:site, fn %{current_user: current_user} ->
Sites.get_for_user!(current_user, domain, [:owner, :admin, :super_admin])
Plausible.Teams.Adapter.Read.Sites.get_for_user!(current_user, domain, [
:owner,
:admin,
:super_admin
])
end)
|> assign_new(:page_rules_count, fn %{site: site} ->
Shields.count_page_rules(site)

View File

@ -18,7 +18,7 @@ defmodule PlausibleWeb.Live.Verification do
socket
) do
site =
Plausible.Sites.get_for_user!(socket.assigns.current_user, domain, [
Plausible.Teams.Adapter.Read.Sites.get_for_user!(socket.assigns.current_user, domain, [
:owner,
:admin,
:super_admin,

View File

@ -107,9 +107,21 @@ defmodule PlausibleWeb.Plugs.AuthorizeSiteAccess do
Sentry.Context.set_extra_context(%{site_id: site.id, domain: site.domain})
Plausible.OpenTelemetry.add_site_attributes(site)
site = Plausible.Imported.load_import_data(site)
site =
site
|> Plausible.Imported.load_import_data()
|> Repo.preload(
team: [subscription: Plausible.Teams.last_subscription_query()],
owner: [subscription: Plausible.Users.last_subscription_query()]
)
merge_assigns(conn, site: site, current_user_role: role)
conn = merge_assigns(conn, site: site, current_user_role: role)
if not is_nil(current_user) and role not in [:public, nil] do
assign(conn, :current_team, site.team)
else
conn
end
else
error_not_found(conn)
end

View File

@ -123,7 +123,8 @@ defmodule PlausibleWeb.UserAuth do
defp get_session_by_token(token) do
now = NaiveDateTime.utc_now(:second)
last_subscription_query = Plausible.Users.last_subscription_join_query()
last_user_subscription_query = Plausible.Users.last_subscription_join_query()
last_team_subscription_query = Plausible.Teams.last_subscription_join_query()
token_query =
from(us in Auth.UserSession,
@ -132,10 +133,13 @@ defmodule PlausibleWeb.UserAuth do
left_join: tm in assoc(u, :team_memberships),
on: tm.role != :guest,
left_join: t in assoc(tm, :team),
left_lateral_join: s in subquery(last_subscription_query),
as: :team,
left_lateral_join: ts in subquery(last_team_subscription_query),
on: true,
left_lateral_join: s in subquery(last_user_subscription_query),
on: true,
where: us.token == ^token and us.timeout_at > ^now,
preload: [user: {u, subscription: s, team_memberships: {tm, team: t}}]
preload: [user: {u, subscription: s, team_memberships: {tm, team: {t, subscription: ts}}}]
)
case Repo.one(token_query) do

View File

@ -150,7 +150,7 @@ defmodule Plausible.Workers.CheckUsage do
defp check_pageview_usage_two_cycles(subscriber, usage_mod) do
usage = usage_mod.monthly_pageview_usage(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber)
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
{:over_limit, usage}
@ -161,7 +161,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)
limit = Quota.Limits.monthly_pageview_limit(subscriber.subscription)
if :last_cycle in Quota.exceeded_cycles(usage, limit) do
{:over_limit, usage}

View File

@ -157,16 +157,20 @@ defmodule Plausible.SitesTest do
describe "get_for_user/2" do
@tag :ee_only
test "get site for super_admin" do
user1 = insert(:user)
user2 = insert(:user)
user1 = new_user()
user2 = new_user()
patch_env(:super_admin_user_ids, [user2.id])
%{id: site_id, domain: domain} = insert(:site, members: [user1])
assert %{id: ^site_id} = Sites.get_for_user(user1.id, domain)
assert %{id: ^site_id} = Sites.get_for_user(user1.id, domain, [:owner])
%{id: site_id, domain: domain} = new_site(owner: user1)
assert %{id: ^site_id} = Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain)
assert is_nil(Sites.get_for_user(user2.id, domain))
assert %{id: ^site_id} = Sites.get_for_user(user2.id, domain, [:super_admin])
assert %{id: ^site_id} =
Plausible.Teams.Adapter.Read.Sites.get_for_user(user1, domain, [:owner])
assert is_nil(Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain))
assert %{id: ^site_id} =
Plausible.Teams.Adapter.Read.Sites.get_for_user(user2, domain, [:super_admin])
end
end
@ -487,8 +491,8 @@ defmodule Plausible.SitesTest do
describe "set_option/4" do
test "allows setting option multiple times" do
user = insert(:user)
site = insert(:site, members: [user])
user = new_user()
site = new_site(owner: user)
assert prefs =
%{pinned_at: %NaiveDateTime{}} =
@ -538,8 +542,8 @@ defmodule Plausible.SitesTest do
describe "toggle_pin/2" do
test "allows pinning and unpinning site" do
user = insert(:user)
site = insert(:site, members: [user])
user = new_user()
site = new_site(owner: user)
site = %{site | pinned_at: nil}
assert {:ok, prefs} = Sites.toggle_pin(user, site)
@ -567,10 +571,10 @@ defmodule Plausible.SitesTest do
end
test "returns error when pins limit hit" do
user = insert(:user)
user = new_user()
for _ <- 1..9 do
site = insert(:site, members: [user])
site = new_site(owner: user)
assert {:ok, _} = Sites.toggle_pin(user, site)
end

View File

@ -232,15 +232,11 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
test "returns 404 when api key owner does not have permissions to create a shared link", %{
conn: conn,
site: site,
user: user
} do
Repo.update_all(
from(sm in Plausible.Site.Membership,
where: sm.site_id == ^site.id and sm.user_id == ^user.id
),
set: [role: :viewer]
)
site = new_site()
add_guest(site, user: user, role: :viewer)
conn =
put(conn, "/api/v1/sites/shared-links", %{
@ -383,15 +379,11 @@ defmodule PlausibleWeb.Api.ExternalSitesControllerTest do
test "returns 404 when api key owner does not have permissions to create a goal", %{
conn: conn,
site: site,
user: user
} do
Repo.update_all(
from(sm in Plausible.Site.Membership,
where: sm.site_id == ^site.id and sm.user_id == ^user.id
),
set: [role: :viewer]
)
site = new_site()
add_guest(site, user: user, role: :viewer)
conn =
put(conn, "/api/v1/sites/goals", %{

View File

@ -2,6 +2,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
use PlausibleWeb.ConnCase, async: true
use Bamboo.Test
use Plausible.Repo
use Plausible.Teams.Test
import Mox
import Plausible.Test.Support.HTML
@ -23,7 +24,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "558018", user: user)
subscribe_to_plan(user, "558018")
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "10k pageviews"
assert html_response(conn, 200) =~ "monthly billing"
@ -31,7 +32,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows yearly subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "590752", user: user)
subscribe_to_plan(user, "590752")
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "100k pageviews"
assert html_response(conn, 200) =~ "yearly billing"
@ -39,7 +40,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows free subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "free_10k", user: user)
subscribe_to_plan(user, "free_10k")
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "10k pageviews"
assert html_response(conn, 200) =~ "N/A billing"
@ -47,8 +48,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows enterprise plan subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "123", user: user)
subscribe_to_plan(user, "123")
configure_enterprise_plan(user)
conn = get(conn, Routes.settings_path(conn, :subscription))
@ -61,20 +61,15 @@ defmodule PlausibleWeb.SettingsControllerTest do
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user
)
insert(:enterprise_plan,
paddle_plan_id: "1234",
user: user,
monthly_pageview_limit: 10_000_000,
billing_interval: :yearly
)
configure_enterprise_plan(user)
subscribe_to_enterprise_plan(user,
paddle_plan_id: "1234",
monthly_pageview_limit: 10_000_000,
billing_interval: :yearly,
subscription?: false
)
conn = get(conn, Routes.settings_path(conn, :subscription))
assert html_response(conn, 200) =~ "20M pageviews"
assert html_response(conn, 200) =~ "yearly billing"
@ -102,7 +97,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
conn: conn,
user: user
} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
subscribe_to_plan(user, @v3_plan_id)
doc =
conn
@ -161,7 +156,12 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "renders two links to '/billing/choose-plan' with the text 'Upgrade' for a configured enterprise plan",
%{conn: conn, user: user} do
configure_enterprise_plan(user)
subscribe_to_enterprise_plan(user,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
monthly_pageview_limit: 20_000_000,
billing_interval: :yearly,
subscription?: false
)
doc =
conn
@ -282,7 +282,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
conn: conn,
user: user
} do
site = insert(:site, members: [user])
site = new_site(owner: user)
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
@ -293,12 +293,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
last_bill_date: last_bill_date
)
subscribe_to_plan(user, @v4_plan_id, last_bill_date: last_bill_date, status: :deleted)
html =
conn
@ -358,14 +353,11 @@ defmodule PlausibleWeb.SettingsControllerTest do
assert element_exists?(doc, "#total_pageviews_penultimate_cycle")
end
# for an active subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
subscribe_to_plan(user, @v4_plan_id,
status: :active,
last_bill_date: Timex.shift(Timex.now(), months: -6)
)
).subscription
get(conn, Routes.settings_path(conn, :subscription))
|> html_response(200)
@ -398,7 +390,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "penultimate cycle is disabled if there's no usage", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -5)),
@ -407,11 +399,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
last_bill_date = Timex.shift(Timex.today(), days: -10)
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: last_bill_date
)
subscribe_to_plan(user, @v4_plan_id, last_bill_date: last_bill_date)
html =
conn
@ -429,11 +417,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
last_bill_date: Timex.shift(Timex.today(), days: -1)
)
subscribe_to_plan(user, @v4_plan_id, last_bill_date: Timex.shift(Timex.today(), days: -1))
html =
conn
@ -450,7 +434,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
conn: conn,
user: user
} do
site = insert(:site, members: [user])
site = new_site(owner: user)
populate_stats(site, [
build(:event, name: "pageview", timestamp: Timex.shift(Timex.now(), days: -1)),
@ -474,15 +458,12 @@ defmodule PlausibleWeb.SettingsControllerTest do
|> html_response(200)
|> assert_usage.()
# for an expired subscription
subscription =
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
subscribe_to_plan(user, @v4_plan_id,
status: :deleted,
last_bill_date: ~D[2022-01-01],
next_bill_date: ~D[2022-02-01]
)
).subscription
conn
|> get(Routes.settings_path(conn, :subscription))
@ -514,8 +495,8 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "renders sites usage and limit", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
insert(:site, members: [user])
subscribe_to_plan(user, @v3_plan_id)
new_site(owner: user)
site_usage_row_text =
conn
@ -541,7 +522,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "renders team member usage without limit if it's unlimited", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
subscribe_to_plan(user, @v3_plan_id)
team_member_usage_row_text =
conn
@ -570,11 +551,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows invoices for subscribed user", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",
paddle_subscription_id: "redundant",
user: user
)
subscribe_to_plan(user, "558018")
html =
conn
@ -589,11 +566,7 @@ defmodule PlausibleWeb.SettingsControllerTest do
@tag :ee_only
test "shows message on failed invoice request'", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",
paddle_subscription_id: "invalid_subscription_id",
user: user
)
subscribe_to_plan(user, "558018", paddle_subscription_id: "invalid_subscription_id")
html =
conn
@ -1125,9 +1098,8 @@ defmodule PlausibleWeb.SettingsControllerTest do
end
defp configure_enterprise_plan(user) do
insert(:enterprise_plan,
subscribe_to_enterprise_plan(user,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user,
monthly_pageview_limit: 20_000_000,
billing_interval: :yearly
)

View File

@ -13,7 +13,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
describe "GET /sites/:domain/memberships/invite" do
test "shows invite form", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
html =
conn
@ -27,11 +27,11 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
@tag :ee_only
test "display a notice when is over limit", %{conn: conn, user: user} do
memberships = [
build(:site_membership, user: user, role: :owner) | build_list(5, :site_membership)
]
site = new_site(owner: user)
site = insert(:site, memberships: memberships)
for _ <- 1..5 do
add_guest(site, role: :viewer)
end
html =
conn
@ -44,7 +44,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
describe "POST /sites/:domain/memberships/invite" do
test "creates invitation", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
conn =
post(conn, "/sites/#{site.domain}/memberships/invite", %{
@ -60,11 +60,11 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
@tag :ee_only
test "fails to create invitation when is over limit", %{conn: conn, user: user} do
memberships = [
build(:site_membership, user: user, role: :owner) | build_list(5, :site_membership)
]
site = new_site(owner: user)
site = insert(:site, memberships: memberships)
for _ <- 1..5 do
add_guest(site, role: :viewer)
end
conn =
post(conn, "/sites/#{site.domain}/memberships/invite", %{
@ -110,7 +110,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
end
test "sends invitation email for new user", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
post(conn, "/sites/#{site.domain}/memberships/invite", %{
email: "john.doe@example.com",
@ -125,7 +125,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
test "sends invitation email for existing user", %{conn: conn, user: user} do
existing_user = insert(:user)
site = insert(:site, members: [user])
site = new_site(owner: user)
post(conn, "/sites/#{site.domain}/memberships/invite", %{
email: existing_user.email,
@ -139,14 +139,10 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
end
test "renders form with error if the invitee is already a member", %{conn: conn, user: user} do
second_member = insert(:user)
site = new_site(owner: user)
memberships = [
build(:site_membership, user: user, role: :owner),
build(:site_membership, user: second_member)
]
site = insert(:site, memberships: memberships)
second_member = new_user()
add_guest(site, user: second_member, role: :viewer)
conn =
post(conn, "/sites/#{site.domain}/memberships/invite", %{
@ -162,7 +158,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
conn: conn,
user: user
} do
site = insert(:site, members: [user])
site = new_site(owner: user)
_req1 =
post(conn, "/sites/#{site.domain}/memberships/invite", %{
@ -202,7 +198,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
describe "GET /sites/:domain/transfer-ownership" do
test "shows ownership transfer form", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
conn = get(conn, "/sites/#{site.domain}/transfer-ownership")
@ -212,7 +208,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
describe "POST /sites/:domain/transfer-ownership" do
test "creates invitation with :owner role", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
conn =
post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"})
@ -224,7 +220,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
end
test "sends ownership transfer email for new user", %{conn: conn, user: user} do
site = insert(:site, members: [user])
site = new_site(owner: user)
post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: "john.doe@example.com"})
@ -236,7 +232,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
test "sends invitation email for existing user", %{conn: conn, user: user} do
existing_user = insert(:user)
site = insert(:site, members: [user])
site = new_site(owner: user)
post(conn, "/sites/#{site.domain}/transfer-ownership", %{email: existing_user.email})
@ -262,7 +258,7 @@ defmodule PlausibleWeb.Site.MembershipControllerTest do
test "fails to transfer ownership to invited user with proper error message", ctx do
%{conn: conn, user: user} = ctx
site = insert(:site, members: [user])
site = new_site(owner: user)
invited = "john.doe@example.com"
# invite a user but don't join

View File

@ -444,13 +444,7 @@ defmodule PlausibleWeb.SiteControllerTest do
conn: conn,
user: user
} do
:site
|> insert(
domain: "example.com",
memberships: [
build(:site_membership, user: user, role: :owner)
]
)
new_site(domain: "example.com", owner: user)
|> Plausible.Site.Domain.change("new.example.com")
conn =

View File

@ -149,11 +149,11 @@ defmodule Plausible.Teams.Test do
user
end
def subscribe_to_plan(user, paddle_plan_id) do
def subscribe_to_plan(user, paddle_plan_id, attrs \\ []) do
{:ok, team} = Teams.get_or_create(user)
insert(:subscription, user: user, team: team, paddle_plan_id: paddle_plan_id)
user
attrs = Keyword.merge([user: user, team: team, paddle_plan_id: paddle_plan_id], attrs)
subscription = insert(:subscription, attrs)
%{user | subscription: subscription}
end
def subscribe_to_enterprise_plan(user, attrs \\ []) do