Skip to content

Commit

Permalink
Switch on teams schema in choose plan view behind FF (#4838)
Browse files Browse the repository at this point in the history
* Switch on teams schema in choose plan view behind FF

* Proxy via Read adapter where applicable for billing context

* Proxy remaining plan-related functions

* Switch enterprise_configured?/1 tests to use the adapter

* Format

* Update SiteLocker tests

* Actually use `has_active_subscription?/1` billing adapter

---------

Co-authored-by: Adam Rutkowski <[email protected]>
  • Loading branch information
zoldar and aerosol authored Nov 20, 2024
1 parent 7b49dd3 commit 380dc00
Show file tree
Hide file tree
Showing 18 changed files with 253 additions and 231 deletions.
18 changes: 9 additions & 9 deletions lib/plausible/billing/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ defmodule Plausible.Billing do
alias Plausible.Billing.{Subscription, Plans, Quota}
alias Plausible.Auth.User

@spec active_subscription_for(integer()) :: Subscription.t() | nil
def active_subscription_for(user_id) do
user_id |> active_subscription_query() |> Repo.one()
@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?(integer()) :: boolean()
def has_active_subscription?(user_id) do
user_id |> active_subscription_query() |> Repo.exists?()
@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
Expand Down Expand Up @@ -41,7 +41,7 @@ defmodule Plausible.Billing do
end

def change_plan(user, new_plan_id) do
subscription = active_subscription_for(user.id)
subscription = active_subscription_for(user)
plan = Plans.find(new_plan_id)

limit_checking_opts =
Expand Down Expand Up @@ -283,9 +283,9 @@ defmodule Plausible.Billing do
"subscription_cancelled__#{user.id}"
end

defp active_subscription_query(user_id) do
defp active_subscription_query(user) do
from(s in Subscription,
where: s.user_id == ^user_id and s.status == ^Subscription.Status.active(),
where: s.user_id == ^user.id and s.status == ^Subscription.Status.active(),
order_by: [desc: s.inserted_at],
limit: 1
)
Expand Down
38 changes: 17 additions & 21 deletions lib/plausible/billing/plans.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,20 @@ defmodule Plausible.Billing.Plans do
@business_tier_launch ~N[2023-11-08 12:00:00]
def business_tier_launch, do: @business_tier_launch

@spec growth_plans_for(User.t()) :: [Plan.t()]
@spec growth_plans_for(Subscription.t()) :: [Plan.t()]
@doc """
Returns a list of growth plans available for the user to choose.
Returns a list of growth plans available for the subscription to choose.
As new versions of plans are introduced, users who were on old plans can
As new versions of plans are introduced, subscriptions which were on old plans can
still choose from old plans.
"""
def growth_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
def growth_plans_for(subscription) do
owned_plan = get_regular_plan(subscription)

cond do
Application.get_env(:plausible, :environment) in ["dev", "staging"] -> @sandbox_plans
is_nil(owned_plan) -> @plans_v4
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
subscription && Subscriptions.expired?(subscription) -> @plans_v4
owned_plan.kind == :business -> @plans_v4
owned_plan.generation == 1 -> @plans_v1 |> drop_high_plans(owned_plan)
owned_plan.generation == 2 -> @plans_v2 |> drop_high_plans(owned_plan)
Expand All @@ -52,21 +51,20 @@ defmodule Plausible.Billing.Plans do
|> Enum.filter(&(&1.kind == :growth))
end

def business_plans_for(%User{} = user) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
def business_plans_for(subscription) do
owned_plan = get_regular_plan(subscription)

cond do
Application.get_env(:plausible, :environment) in ["dev", "staging"] -> @sandbox_plans
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
subscription && Subscriptions.expired?(subscription) -> @plans_v4
owned_plan && owned_plan.generation < 4 -> @plans_v3
true -> @plans_v4
end
|> Enum.filter(&(&1.kind == :business))
end

def available_plans_for(%User{} = user, opts \\ []) do
plans = growth_plans_for(user) ++ business_plans_for(user)
def available_plans_for(subscription, opts \\ []) do
plans = growth_plans_for(subscription) ++ business_plans_for(subscription)

plans =
if Keyword.get(opts, :with_prices) do
Expand Down Expand Up @@ -220,18 +218,16 @@ defmodule Plausible.Billing.Plans do
def suggest(user, usage_during_cycle) do
cond do
usage_during_cycle > @enterprise_level_usage -> :enterprise
Plausible.Auth.enterprise_configured?(user) -> :enterprise
true -> suggest_by_usage(user, usage_during_cycle)
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(user) -> :enterprise
true -> Plausible.Teams.Adapter.Read.Billing.suggest_by_usage(user, usage_during_cycle)
end
end

defp suggest_by_usage(user, usage_during_cycle) do
user = Plausible.Users.with_subscription(user)

def suggest_by_usage(subscription, usage_during_cycle) do
available_plans =
if business_tier?(user.subscription),
do: business_plans_for(user),
else: growth_plans_for(user)
if business_tier?(subscription),
do: business_plans_for(subscription),
else: growth_plans_for(subscription)

Enum.find(available_plans, &(usage_during_cycle < &1.monthly_pageview_limit))
end
Expand Down
5 changes: 3 additions & 2 deletions lib/plausible/billing/site_locker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ 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.Billing.Quota.Usage.monthly_pageview_usage(user)
usage = Plausible.Teams.Adapter.Read.Billing.monthly_pageview_usage(user)
suggested_plan = Plausible.Billing.Plans.suggest(user, usage.last_cycle.total)

PlausibleWeb.Email.dashboard_locked(user, usage, suggested_plan)
user
|> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan)
|> Plausible.Mailer.send()
end
end
21 changes: 21 additions & 0 deletions lib/plausible/teams/adapter/read/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ defmodule Plausible.Teams.Adapter.Read.Billing do
"""
use Plausible.Teams.Adapter

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 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 get_subscription(user) do
case user_or_team(user) do
%{subscription: subscription} -> subscription
Expand Down Expand Up @@ -131,4 +147,9 @@ defmodule Plausible.Teams.Adapter.Read.Billing do
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
7 changes: 7 additions & 0 deletions lib/plausible/teams/adapter/read/ownership.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ defmodule Plausible.Teams.Adapter.Read.Ownership do
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 ->
Expand Down
27 changes: 27 additions & 0 deletions lib/plausible/teams/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,28 @@ defmodule Plausible.Teams.Billing do
alias Plausible.Billing.{Plan, Plans, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, Props, StatsAPI}

require Plausible.Billing.Subscription.Status

@team_member_limit_for_trials 3
@limit_sites_since ~D[2021-05-05]
@site_limit_for_trials 10

def enterprise_configured?(nil), do: false

def enterprise_configured?(%Teams.Team{} = team) do
team
|> Ecto.assoc(:enterprise_plan)
|> Repo.exists?()
end

def has_active_subscription?(nil), do: false

def has_active_subscription?(team) do
team
|> active_subscription_query()
|> Repo.exists?()
end

def check_needs_to_upgrade(nil), do: {:needs_to_upgrade, :no_trial}

def check_needs_to_upgrade(team) do
Expand Down Expand Up @@ -350,4 +368,13 @@ defmodule Plausible.Teams.Billing do
end
end
end

defp active_subscription_query(team) do
from(s in Plausible.Billing.Subscription,
where:
s.team_id == ^team.id and s.status == ^Plausible.Billing.Subscription.Status.active(),
order_by: [desc: s.inserted_at],
limit: 1
)
end
end
24 changes: 24 additions & 0 deletions lib/plausible/teams/memberships.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ defmodule Plausible.Teams.Memberships do
alias Plausible.Repo
alias Plausible.Teams

def all_pending_site_transfers(email) do
email
|> pending_site_transfers_query()
|> Repo.all()
|> Enum.map(fn transfer ->
%Plausible.Auth.Invitation{
site_id: transfer.site_id,
email: transfer.email,
invitation_id: transfer.transfer_id,
role: :owner
}
end)
end

def any_pending_site_transfers?(email) do
email
|> pending_site_transfers_query()
|> Repo.exists?()
end

def get(team, user) do
result =
from(tm in Teams.Membership,
Expand Down Expand Up @@ -127,4 +147,8 @@ defmodule Plausible.Teams.Memberships do
membership -> {:ok, membership}
end
end

defp pending_site_transfers_query(email) do
from st in Teams.SiteTransfer, where: st.email == ^email
end
end
2 changes: 1 addition & 1 deletion lib/plausible_web/components/billing/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ defmodule PlausibleWeb.Components.Billing do
</div>
<.styled_link
:if={
not (Plausible.Auth.enterprise_configured?(@user) &&
not (Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@user) &&
Subscriptions.halted?(@subscription))
}
id="#upgrade-or-change-plan-link"
Expand Down
10 changes: 6 additions & 4 deletions lib/plausible_web/controllers/billing_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ defmodule PlausibleWeb.BillingController do
plug PlausibleWeb.RequireAccountPlug

def ping_subscription(%Plug.Conn{} = conn, _params) do
subscribed? = Billing.has_active_subscription?(conn.assigns.current_user.id)
subscribed? =
Plausible.Teams.Adapter.Read.Billing.has_active_subscription?(conn.assigns.current_user)

json(conn, %{is_subscribed: subscribed?})
end

def choose_plan(conn, _params) do
current_user = conn.assigns.current_user

if Plausible.Auth.enterprise_configured?(current_user) do
if Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(current_user) do
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
else
render(conn, "choose_plan.html",
Expand Down Expand Up @@ -135,8 +137,8 @@ defmodule PlausibleWeb.BillingController do
end
end

defp preview_subscription(%{id: user_id}, new_plan_id) do
subscription = Billing.active_subscription_for(user_id)
defp preview_subscription(user, new_plan_id) do
subscription = Billing.active_subscription_for(user)

if subscription do
with {:ok, preview_info} <- Billing.change_plan_preview(subscription, new_plan_id) do
Expand Down
20 changes: 11 additions & 9 deletions lib/plausible_web/live/choose_plan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
require Plausible.Billing.Subscription.Status

alias PlausibleWeb.Components.Billing.{PlanBox, PlanBenefits, Notice, PageviewSlider}
alias Plausible.Site
alias Plausible.Billing.{Plans, Quota}

@contact_link "https://plausible.io/contact"
Expand All @@ -19,7 +18,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
socket
|> assign_new(:pending_ownership_site_ids, fn %{current_user: current_user} ->
current_user.email
|> Site.Memberships.all_pending_ownerships()
|> Plausible.Teams.Adapter.Read.Ownership.all_pending_site_transfers(current_user)
|> Enum.map(& &1.site_id)
end)
|> assign_new(:usage, fn %{
Expand All @@ -31,17 +30,20 @@ defmodule PlausibleWeb.Live.ChoosePlan do
pending_ownership_site_ids: pending_ownership_site_ids
)
end)
|> assign_new(:owned_plan, fn %{current_user: %{subscription: subscription}} ->
|> assign_new(:subscription, fn %{current_user: current_user} ->
Plausible.Teams.Adapter.Read.Billing.get_subscription(current_user)
end)
|> assign_new(:owned_plan, fn %{subscription: subscription} ->
Plans.get_regular_plan(subscription, only_non_expired: true)
end)
|> assign_new(:owned_tier, fn %{owned_plan: owned_plan} ->
if owned_plan, do: Map.get(owned_plan, :kind), else: nil
end)
|> assign_new(:current_interval, fn %{current_user: current_user} ->
current_user_subscription_interval(current_user.subscription)
|> assign_new(:current_interval, fn %{subscription: subscription} ->
current_user_subscription_interval(subscription)
end)
|> assign_new(:available_plans, fn %{current_user: current_user} ->
Plans.available_plans_for(current_user, with_prices: true, customer_ip: remote_ip)
|> assign_new(:available_plans, fn %{subscription: subscription} ->
Plans.available_plans_for(subscription, with_prices: true, customer_ip: remote_ip)
end)
|> assign_new(:recommended_tier, fn %{usage: usage, available_plans: available_plans} ->
highest_growth_plan = List.last(available_plans.growth)
Expand Down Expand Up @@ -102,8 +104,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
class="pb-6"
pending_ownership_count={length(@pending_ownership_site_ids)}
/>
<Notice.subscription_past_due class="pb-6" subscription={@current_user.subscription} />
<Notice.subscription_paused class="pb-6" subscription={@current_user.subscription} />
<Notice.subscription_past_due class="pb-6" subscription={@subscription} />
<Notice.subscription_paused class="pb-6" subscription={@subscription} />
<Notice.upgrade_ineligible :if={not Quota.eligible_for_upgrade?(@usage)} />
<div class="mx-auto max-w-4xl text-center">
<p class="text-4xl font-bold tracking-tight lg:text-5xl">
Expand Down
4 changes: 3 additions & 1 deletion lib/plausible_web/templates/layout/_notice.html.heex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
<div class="flex flex-col gap-y-2">
<Notice.active_grace_period
:if={Plausible.Auth.GracePeriod.active?(@conn.assigns.current_user)}
enterprise?={Plausible.Auth.enterprise_configured?(@conn.assigns.current_user)}
enterprise?={
Plausible.Teams.Adapter.Read.Billing.enterprise_configured?(@conn.assigns.current_user)
}
grace_period_end={grace_period_end(@conn.assigns.current_user)}
/>

Expand Down
18 changes: 13 additions & 5 deletions test/plausible/auth/auth_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,20 @@ defmodule Plausible.AuthTest do
end

test "enterprise_configured?/1 returns whether the user has an enterprise plan" do
user_without_plan = insert(:user)
user_with_plan = insert(:user, enterprise_plan: build(:enterprise_plan))
user_without_plan = new_user()
user_with_plan = new_user() |> subscribe_to_enterprise_plan()

assert Auth.enterprise_configured?(user_with_plan)
refute Auth.enterprise_configured?(user_without_plan)
refute Auth.enterprise_configured?(nil)
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.Adapter.Read.Billing.enterprise_configured?(
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)
end

describe "create_api_key/3" do
Expand Down
Loading

0 comments on commit 380dc00

Please sign in to comment.