Bugfix: available features for expired trials, no subscriptions (#3740)

* Reorganize how subscriptions/trials are evaluated

* Bugfix: expired trial+no subscriptions should not have access to extra features

* Make self-hosted users always on trial

* Seed secondary user with password

* Format

* Fix docs

* Fix small_test run

* Run the test only on full_build

* More tweaks to small builds

* Allow [Goals] for expired trials with no subscription
This commit is contained in:
hq1 2024-02-01 08:15:04 +01:00 committed by GitHub
parent 85c9771d11
commit 9fb4ea0c3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 126 additions and 90 deletions

View File

@ -118,7 +118,7 @@ defmodule Plausible.Auth.UserAdmin do
user.subscription ->
PlausibleWeb.AuthView.present_subscription_status(user.subscription.status)
Plausible.Billing.on_trial?(user) ->
Plausible.Users.on_trial?(user) ->
"On trial"
true ->

View File

@ -2,6 +2,7 @@ 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.Auth.User
@ -94,42 +95,28 @@ defmodule Plausible.Billing do
| :no_upgrade_needed
def check_needs_to_upgrade(user) do
user = Plausible.Users.with_subscription(user)
trial_is_over = user.trial_expiry_date && Timex.before?(user.trial_expiry_date, Timex.today())
subscription_active = subscription_is_active?(user.subscription)
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
!user.trial_expiry_date && !subscription_active -> {:needs_to_upgrade, :no_trial}
trial_is_over && !subscription_active -> {:needs_to_upgrade, :no_active_subscription}
Plausible.Auth.GracePeriod.expired?(user) -> {:needs_to_upgrade, :grace_period_ended}
true -> :no_upgrade_needed
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
def subscription_is_active?(%Subscription{status: Subscription.Status.active()}), do: true
def subscription_is_active?(%Subscription{status: Subscription.Status.past_due()}), do: true
def subscription_is_active?(%Subscription{status: Subscription.Status.deleted()} = subscription) do
subscription.next_bill_date && !Timex.before?(subscription.next_bill_date, Timex.today())
end
def subscription_is_active?(%Subscription{}), do: false
def subscription_is_active?(nil), do: false
on_full_build do
def on_trial?(%User{trial_expiry_date: nil}), do: false
def on_trial?(user) do
user = Plausible.Users.with_subscription(user)
!subscription_is_active?(user.subscription) && trial_days_left(user) >= 0
end
else
def on_trial?(_), do: false
end
def trial_days_left(user) do
Timex.diff(user.trial_expiry_date, Timex.today(), :days)
end
defp handle_subscription_created(params) do
params =
if present?(params["passthrough"]) do

View File

@ -194,7 +194,7 @@ defmodule Plausible.Billing.Feature.StatsAPI do
@doc """
Checks whether the user has access to Stats API or not.
Before the the business tier, users who had not yet started their trial had
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

View File

@ -5,9 +5,10 @@ defmodule Plausible.Billing.Quota do
use Plausible
import Ecto.Query
alias Plausible.Users
alias Plausible.Auth.User
alias Plausible.Site
alias Plausible.Billing.{Plan, Plans, Subscription, EnterprisePlan, Feature}
alias Plausible.Billing.{Plan, Plans, Subscription, Subscriptions, EnterprisePlan, Feature}
alias Plausible.Billing.Feature.{Goals, RevenueGoals, Funnels, Props, StatsAPI}
@type limit() :: :site_limit | :pageview_limit | :team_member_limit
@ -59,7 +60,7 @@ defmodule Plausible.Billing.Quota do
end
defp get_site_limit_from_plan(user) do
user = Plausible.Users.with_subscription(user)
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{site_limit: site_limit} -> site_limit
@ -70,7 +71,7 @@ defmodule Plausible.Billing.Quota do
@spec team_member_limit(User.t()) :: non_neg_integer()
def team_member_limit(user) do
user = Plausible.Users.with_subscription(user)
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%{team_member_limit: limit} -> limit
@ -102,7 +103,7 @@ defmodule Plausible.Billing.Quota do
in a background job instead (see `check_usage.ex`).
"""
def ensure_can_add_new_site(user) do
user = Plausible.Users.with_subscription(user)
user = Users.with_subscription(user)
case Plans.get_subscription_plan(user.subscription) do
%EnterprisePlan{} ->
@ -122,7 +123,7 @@ defmodule Plausible.Billing.Quota do
@spec monthly_pageview_limit(User.t() | Subscription.t()) ::
non_neg_integer() | :unlimited
def monthly_pageview_limit(%User{} = user) do
user = Plausible.Users.with_subscription(user)
user = Users.with_subscription(user)
monthly_pageview_limit(user.subscription)
end
@ -180,7 +181,7 @@ defmodule Plausible.Billing.Quota do
end
def monthly_pageview_usage(user, site_ids) do
active_subscription? = Plausible.Billing.subscription_is_active?(user.subscription)
active_subscription? = Subscriptions.active?(user.subscription)
if active_subscription? && user.subscription.last_bill_date do
[:current_cycle, :last_cycle, :penultimate_cycle]
@ -217,7 +218,7 @@ defmodule Plausible.Billing.Quota do
end
def usage_cycle(user, cycle, owned_site_ids, today) do
user = Plausible.Users.with_subscription(user)
user = Users.with_subscription(user)
last_bill_date = user.subscription.last_bill_date
normalized_last_bill_date =
@ -435,13 +436,24 @@ defmodule Plausible.Billing.Quota do
ability to use all features during their trial.
"""
def allowed_features_for(user) do
user = Plausible.Users.with_subscription(user)
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 -> Feature.list()
%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

View File

@ -4,6 +4,17 @@ defmodule Plausible.Billing.Subscriptions do
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
def active?(%Subscription{status: Subscription.Status.active()}), do: true
def active?(%Subscription{status: Subscription.Status.past_due()}), do: true
def active?(%Subscription{status: Subscription.Status.deleted()} = subscription) do
not is_nil(subscription.next_bill_date) and
not Date.before?(subscription.next_bill_date, Date.utc_today())
end
def active?(%Subscription{}), do: false
def active?(nil), do: false
@spec expired?(Subscription.t()) :: boolean()
@doc """
Returns whether the given subscription is expired. That means that the

View File

@ -20,7 +20,7 @@ defmodule Plausible.Site.GateKeeper do
a Site by domain using `Plausible.Cache` interface.
The module defines two policies outside the regular bucket inspection:
* when the the site is not found in cache: #{@policy_for_non_existing_sites}
* when the site is not found in cache: #{@policy_for_non_existing_sites}
* when the underlying rate limiting mechanism returns
an internal error: :allow
"""

View File

@ -72,7 +72,7 @@ defmodule Plausible.Site.Memberships.Invitations do
new_owner = Plausible.Users.with_subscription(new_owner)
plan = Plausible.Billing.Plans.get_subscription_plan(new_owner.subscription)
active_subscription? = Plausible.Billing.subscription_is_active?(new_owner.subscription)
active_subscription? = Plausible.Billing.Subscriptions.active?(new_owner.subscription)
if active_subscription? && plan != :free_10k do
usage_after_transfer = %{

View File

@ -11,6 +11,23 @@ defmodule Plausible.Users do
alias Plausible.Billing.Subscription
alias Plausible.Repo
@spec on_trial?(Auth.User.t()) :: boolean()
on_full_build do
def on_trial?(%Auth.User{trial_expiry_date: nil}), do: false
def on_trial?(user) do
user = with_subscription(user)
not Plausible.Billing.Subscriptions.active?(user.subscription) && trial_days_left(user) >= 0
end
else
def on_trial?(_), do: true
end
@spec trial_days_left(Auth.User.t()) :: integer()
def trial_days_left(user) do
Timex.diff(user.trial_expiry_date, Timex.today(), :days)
end
@spec update_accept_traffic_until(Auth.User.t()) :: Auth.User.t()
def update_accept_traffic_until(user) do
user
@ -24,7 +41,7 @@ defmodule Plausible.Users do
user = with_subscription(user)
cond do
Plausible.Billing.on_trial?(user) ->
Plausible.Users.on_trial?(user) ->
Timex.shift(user.trial_expiry_date,
days: Auth.User.trial_accept_traffic_until_offset_days()
)
@ -86,7 +103,7 @@ defmodule Plausible.Users do
end
defp last_subscription_query(user_id) do
from(subscription in Plausible.Billing.Subscription,
from(subscription in Subscription,
where: subscription.user_id == ^user_id,
order_by: [desc: subscription.inserted_at],
limit: 1

View File

@ -277,7 +277,7 @@ defmodule PlausibleWeb.Components.Billing.Notice do
plan =
Plans.get_regular_plan(billable_user.subscription, only_non_expired: true)
trial? = Plausible.Billing.on_trial?(assigns.billable_user)
trial? = Plausible.Users.on_trial?(assigns.billable_user)
growth? = plan && plan.kind == :growth
cond do

View File

@ -1,4 +1,4 @@
<%= if Plausible.Billing.on_trial?(@user) do %>
<%= if Plausible.Users.on_trial?(@user) do %>
You signed up for a free 30-day trial of Plausible, a simple and privacy-friendly website analytics tool.
<br /><br />
<% end %>

View File

@ -4,7 +4,7 @@ Do check out your <%= link("easy to use, fast-loading and privacy-friendly dashb
<br /><br />
Something looks off? Take a look at our <%= link("installation troubleshooting guide", to: "https://plausible.io/docs/troubleshoot-integration") %>.
<br /><br />
<%= if Plausible.Billing.on_trial?(@user) do %>
<%= if Plausible.Users.on_trial?(@user) do %>
You're on a 30-day free trial with no obligations so do take your time to explore Plausible. Here's how to get <%= link("the most out of your Plausible experience", to: "https://plausible.io/docs/your-plausible-experience") %>.
<br /><br />
<% end %>

View File

@ -25,7 +25,7 @@
<% @conn.assigns[:current_user] -> %>
<ul class="flex items-center w-full sm:w-auto">
<li
:if={Plausible.Billing.on_trial?(@conn.assigns[:current_user])}
:if={full_build?() and Plausible.Users.on_trial?(@conn.assigns[:current_user])}
class="hidden mr-6 sm:block"
>
<%= link(trial_notificaton(@conn.assigns[:current_user]),

View File

@ -59,7 +59,7 @@ defmodule PlausibleWeb.LayoutView do
end
def trial_notificaton(user) do
case Plausible.Billing.trial_days_left(user) do
case Plausible.Users.trial_days_left(user) do
days when days > 1 ->
"#{days} trial days left"

View File

@ -43,7 +43,7 @@ site =
memberships: [
Plausible.Factory.build(:site_membership, user: user, role: :owner),
Plausible.Factory.build(:site_membership,
user: Plausible.Factory.build(:user, name: "Arnold Wallaby"),
user: Plausible.Factory.build(:user, name: "Arnold Wallaby", password: "plausible"),
role: :viewer
)
]

View File

@ -5,6 +5,41 @@ defmodule Plausible.UsersTest do
alias Plausible.Auth.User
alias Plausible.Repo
describe "trial_days_left" do
test "is 30 days for new signup" do
user = insert(:user)
assert Users.trial_days_left(user) == 30
end
test "is based on trial_expiry_date" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 1))
assert Users.trial_days_left(user) == 1
end
end
describe "on_trial?" do
@describetag :full_build_only
test "is true with >= 0 trial days left" do
user = insert(:user)
assert Users.on_trial?(user)
end
test "is false with < 0 trial days left" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: -1))
refute Users.on_trial?(user)
end
test "is false if user has subscription" do
user = insert(:user, subscription: build(:subscription))
refute Users.on_trial?(user)
end
end
describe "update_accept_traffic_until" do
@describetag :full_build_only
test "update" do

View File

@ -5,41 +5,6 @@ defmodule Plausible.BillingTest do
alias Plausible.Billing
alias Plausible.Billing.Subscription
describe "trial_days_left" do
test "is 30 days for new signup" do
user = insert(:user)
assert Billing.trial_days_left(user) == 30
end
test "is based on trial_expiry_date" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: 1))
assert Billing.trial_days_left(user) == 1
end
end
describe "on_trial?" do
@describetag :full_build_only
test "is true with >= 0 trial days left" do
user = insert(:user)
assert Billing.on_trial?(user)
end
test "is false with < 0 trial days left" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.now(), days: -1))
refute Billing.on_trial?(user)
end
test "is false if user has subscription" do
user = insert(:user, subscription: build(:subscription))
refute Billing.on_trial?(user)
end
end
describe "check_needs_to_upgrade" do
test "is false for a trial user" do
user = insert(:user)

View File

@ -499,6 +499,13 @@ defmodule Plausible.Billing.QuotaTest do
end
describe "allowed_features_for/1" do
on_full_build 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.allowed_features_for(user)
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))

View File

@ -104,6 +104,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
assert rendered =~ "/billing/choose-plan"
end
@tag :full_build_only
test "limit_exceeded/1 when billable user is on an enterprise plan displays support email" do
me =
insert(:user,
@ -123,6 +124,7 @@ defmodule PlausibleWeb.Components.Billing.NoticeTest do
assert rendered =~ "please contact hello@plausible.io to upgrade your subscription"
end
@tag :full_build_only
test "limit_exceeded/1 when billable user is on a business plan displays support email" do
me = insert(:user, subscription: build(:business_subscription))