mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 18:07:33 +03:00
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:
parent
85c9771d11
commit
9fb4ea0c3e
@ -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 ->
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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 = %{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 %>
|
||||
|
@ -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 %>
|
||||
|
@ -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]),
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user