Limit grandfathering to *active* subscribers and trials (#3524)

* refactor asserting plan generation in plans_test.exs

* stop grandfathering old expired trials

For users who registered before the business tiers release, we want to
offer a chance to subscribe to a grandfathered plan. However, if they
let their trial expire and don't subscribe in the next 10 days, they'll
lose that opportunity.

* stop grandfathering expired subscriptions

* remove default title and icon from Generic.notice

* fix bug with dismissable notice

classList is null when dismissable_id is not given

* alias Plausible.Auth.User

* Refactor Generic.notice component

Make it easy to apply different colors

* move subscription_cancelled_notice across the app

And remove from user settings > subscription box. Also, include a note
about losing grandfathered status when letting the subscription expire.

* allow full width in Generic.notice

* use Generic.notice for subscription_past_due_notice

* use Generic.notice for subscription_paused_notice

* prevent two notices clashing into each other with gap-y-2

* define attrs for phx components

* optimize for light mode

* make subscription cancelled notice dismissable

but if it's dismiss, show it in the place where it was before in the
account settings > subscription box

* make function private

* replace function doc with regular comment to avoid compile warning

* use array for classnames

Co-authored-by: Vinicius Brasil <vini@hey.com>

* fix typos in function doc

---------

Co-authored-by: Vinicius Brasil <vini@hey.com>
This commit is contained in:
RobertJoonas 2023-11-16 15:40:50 +00:00 committed by GitHub
parent 13055aafc0
commit d66322e12d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 389 additions and 161 deletions

View File

@ -281,6 +281,10 @@ defmodule Plausible.Billing do
def paddle_api(), do: Application.fetch_env!(:plausible, :paddle_api)
def cancelled_subscription_notice_dismiss_id(%Plausible.Auth.User{} = user) do
"subscription_cancelled__#{user.id}"
end
defp active_subscription_query(user_id) do
from(s in Subscription,
where: s.user_id == ^user_id and s.status == ^Subscription.Status.active(),

View File

@ -38,16 +38,17 @@ defmodule Plausible.Billing.Plans do
still choose from old plans.
"""
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
def growth_plans_for(%User{} = user) do
def growth_plans_for(%User{} = user, now \\ Timex.now()) do
user = Plausible.Users.with_subscription(user)
v4_available = FunWithFlags.enabled?(:business_tier, for: user)
owned_plan = get_regular_plan(user.subscription)
cond do
Application.get_env(:plausible, :environment) == "dev" -> @sandbox_plans
is_nil(owned_plan) && Timex.before?(user.inserted_at, @business_tier_launch) -> @plans_v3
is_nil(owned_plan) && grandfathered_trial?(user.trial_expiry_date, now) -> @plans_v3
is_nil(owned_plan) && v4_available -> @plans_v4
is_nil(owned_plan) -> @plans_v3
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
owned_plan.kind == :business -> @plans_v4
owned_plan.generation == 1 -> @plans_v1
owned_plan.generation == 2 -> @plans_v2
@ -57,19 +58,34 @@ defmodule Plausible.Billing.Plans do
|> Enum.filter(&(&1.kind == :growth))
end
def business_plans_for(%User{} = user) do
def business_plans_for(%User{} = user, now \\ Timex.now()) do
user = Plausible.Users.with_subscription(user)
owned_plan = get_regular_plan(user.subscription)
cond do
Application.get_env(:plausible, :environment) == "dev" -> @sandbox_plans
is_nil(owned_plan) && Timex.before?(user.inserted_at, @business_tier_launch) -> @plans_v3
is_nil(owned_plan) && grandfathered_trial?(user.trial_expiry_date, now) -> @plans_v3
user.subscription && Subscriptions.expired?(user.subscription) -> @plans_v4
owned_plan && owned_plan.generation < 4 -> @plans_v3
true -> @plans_v4
end
|> Enum.filter(&(&1.kind == :business))
end
# Takes a Date struct argument representing the trial end date of a user.
# If the `trial_expiry` is `nil`, it means that the user has not started
# their trial yet (i.e. invited user), and this function returns false.
defp grandfathered_trial?(nil, _now), do: false
defp grandfathered_trial?(trial_expiry, now) do
trial_start = Timex.shift(trial_expiry, days: -30)
joined_before_business_tiers = Timex.before?(trial_start, @business_tier_launch)
trial_active_or_expired_less_than_10d_ago = Timex.diff(now, trial_expiry, :days) <= 10
joined_before_business_tiers && trial_active_or_expired_less_than_10d_ago
end
def available_plans_for(%User{} = user, opts \\ []) do
plans = growth_plans_for(user) ++ business_plans_for(user)

View File

@ -4,13 +4,14 @@ defmodule PlausibleWeb.Components.Billing do
use Phoenix.Component
import PlausibleWeb.Components.Generic
require Plausible.Billing.Subscription.Status
alias Plausible.Auth.User
alias Plausible.Billing.Feature.{RevenueGoals, Funnels}
alias Plausible.Billing.Feature.{Props, StatsAPI}
alias PlausibleWeb.Router.Helpers, as: Routes
alias Plausible.Billing.{Subscription, Plans, Plan, Subscriptions}
attr(:billable_user, Plausible.Auth.User, required: true)
attr(:current_user, Plausible.Auth.User, required: true)
attr(:billable_user, User, required: true)
attr(:current_user, User, required: true)
attr(:feature_mod, :atom, required: true, values: Plausible.Billing.Feature.list())
attr(:grandfathered?, :boolean, default: false)
attr(:size, :atom, default: :sm)
@ -33,7 +34,7 @@ defmodule PlausibleWeb.Components.Billing do
not has_access? ->
~H"""
<.notice class="rounded-t-md rounded-b-none" size={@size} {@rest}>
<.notice class="rounded-t-md rounded-b-none" size={@size} {@rest} title="Notice">
<%= account_label(@current_user, @billable_user) %> does not have access to <%= assigns.feature_mod.display_name() %>. To get access to this feature,
<.upgrade_call_to_action current_user={@current_user} billable_user={@billable_user} />.
</.notice>
@ -56,8 +57,8 @@ defmodule PlausibleWeb.Components.Billing do
end
end
attr(:billable_user, Plausible.Auth.User, required: true)
attr(:current_user, Plausible.Auth.User, required: true)
attr(:billable_user, User, required: true)
attr(:current_user, User, required: true)
attr(:limit, :integer, required: true)
attr(:resource, :string, required: true)
attr(:rest, :global)
@ -65,7 +66,7 @@ defmodule PlausibleWeb.Components.Billing do
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
def limit_exceeded_notice(assigns) do
~H"""
<.notice {@rest}>
<.notice {@rest} title="Notice">
<%= account_label(@current_user, @billable_user) %> is limited to <%= @limit %> <%= @resource %>. To increase this limit,
<.upgrade_call_to_action current_user={@current_user} billable_user={@billable_user} />.
</.notice>
@ -215,86 +216,120 @@ defmodule PlausibleWeb.Components.Billing do
"""
end
attr(:user, :map, required: true)
attr(:dismissable, :boolean, default: true)
@doc """
Given a user with a cancelled subscription, this component renders a cancelled
subscription notice. If the given user does not have a subscription or it has a
different status, this function returns an empty template.
It also takes a dismissable argument which renders the notice dismissable (with
the help of JavaScript and localStorage). We show a dismissable notice about a
cancelled subscription across the app, but when the user dismisses it, we will
start displaying it in the account settings > subscription section instead.
So it's either shown across the app, or only on the /settings page. Depending
on whether the localStorage flag to dismiss it has been set or not.
"""
def subscription_cancelled_notice(assigns)
def subscription_cancelled_notice(
%{
dismissable: true,
user: %User{subscription: %Subscription{status: Subscription.Status.deleted()}}
} =
assigns
) do
~H"""
<aside id="global-subscription-cancelled-notice" class="container">
<PlausibleWeb.Components.Generic.notice
dismissable_id={Plausible.Billing.cancelled_subscription_notice_dismiss_id(@user)}
title="Subscription cancelled"
theme={:red}
class="shadow-md dark:shadow-none"
>
<.subscription_cancelled_notice_body user={@user} />
</PlausibleWeb.Components.Generic.notice>
</aside>
"""
end
def subscription_cancelled_notice(
%{
dismissable: false,
user: %User{subscription: %Subscription{status: Subscription.Status.deleted()}}
} =
assigns
) do
assigns = assign(assigns, :container_id, "local-subscription-cancelled-notice")
~H"""
<aside id={@container_id} class="hidden">
<PlausibleWeb.Components.Generic.notice
title="Subscription cancelled"
theme={:red}
class="shadow-md dark:shadow-none"
>
<.subscription_cancelled_notice_body user={@user} />
</PlausibleWeb.Components.Generic.notice>
</aside>
<script
data-localstorage-key={"notice_dismissed__#{Plausible.Billing.cancelled_subscription_notice_dismiss_id(assigns.user)}"}
data-container-id={@container_id}
>
const dataset = document.currentScript.dataset
if (localStorage[dataset.localstorageKey]) {
document.getElementById(dataset.containerId).classList.remove('hidden')
}
</script>
"""
end
def subscription_cancelled_notice(assigns), do: ~H""
attr(:class, :string, default: "")
attr(:subscription, :any, default: nil)
def subscription_past_due_notice(
%{subscription: %Subscription{status: Subscription.Status.past_due()}} = assigns
) do
~H"""
<aside class={@class}>
<div class="shadow-md dark:shadow-none rounded-lg bg-yellow-100 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg
class="w-5 h-5 mt-0.5 text-yellow-800"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-yellow-700">
There was a problem with your latest payment. Please update your payment information to keep using Plausible.
</p>
<.link
href={@subscription.update_url}
class="whitespace-nowrap font-medium text-yellow-700 hover:text-yellow-600"
>
Update billing info <span aria-hidden="true"> &rarr;</span>
</.link>
</div>
</div>
</div>
<PlausibleWeb.Components.Generic.notice
title="Payment failed"
class="shadow-md dark:shadow-none"
>
There was a problem with your latest payment. Please update your payment information to keep using Plausible.<.link
href={@subscription.update_url}
class="whitespace-nowrap font-semibold"
> Update billing info <span aria-hidden="true"> &rarr;</span></.link>
</PlausibleWeb.Components.Generic.notice>
</aside>
"""
end
def subscription_past_due_notice(assigns), do: ~H""
attr(:class, :string, default: "")
attr(:subscription, :any, default: nil)
def subscription_paused_notice(
%{subscription: %Subscription{status: Subscription.Status.paused()}} = assigns
) do
~H"""
<aside class={@class}>
<div class="shadow-md dark:shadow-none rounded-lg bg-red-100 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg
class="w-5 h-5 mt-0.5 text-yellow-800"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-red-700">
Your subscription is paused due to failed payments. Please provide valid payment details to keep using Plausible.
</p>
<.link
href={@subscription.update_url}
class="whitespace-nowrap font-medium text-red-700 hover:text-red-600"
>
Update billing info <span aria-hidden="true"> &rarr;</span>
</.link>
</div>
</div>
</div>
<PlausibleWeb.Components.Generic.notice
title="Subscription paused"
theme={:red}
class="shadow-md dark:shadow-none"
>
Your subscription is paused due to failed payments. Please provide valid payment details to keep using Plausible.<.link
href={@subscription.update_url}
class="whitespace-nowrap font-semibold"
> Update billing info <span aria-hidden="true"> &rarr;</span></.link>
</PlausibleWeb.Components.Generic.notice>
</aside>
"""
end
@ -322,10 +357,11 @@ defmodule PlausibleWeb.Components.Billing do
~H"""
<div
:if={FunWithFlags.enabled?(:premium_features_private_preview) && @features_to_lose != []}
class="container mt-2"
class="container"
>
<.notice
class="shadow-md dark:shadow-none"
title="Notice"
dismissable_id={"premium_features_private_preview_end__#{@user.id}"}
>
Business plans are now live! The private preview of <%= PlausibleWeb.TextHelpers.pretty_join(
@ -432,6 +468,47 @@ defmodule PlausibleWeb.Components.Billing do
"""
end
defp subscription_cancelled_notice_body(assigns) do
if Plausible.Billing.Subscriptions.expired?(assigns.user.subscription) do
~H"""
<.link class="underline inline-block" href={Plausible.Billing.upgrade_route_for(@user)}>
Upgrade your subscription
</.link>
<p>to get access to your stats again.</p>
"""
else
~H"""
<p>
You have access to your stats until <span class="font-semibold inline"><%= Timex.format!(@user.subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %></span>.
<.link class="underline inline-block" href={Plausible.Billing.upgrade_route_for(@user)}>
Upgrade your subscription
</.link>
to make sure you don't lose access.
</p>
<.lose_grandfathering_warning user={@user} />
"""
end
end
defp lose_grandfathering_warning(%{user: %{subscription: subscription} = user} = assigns) do
business_tiers_available? = FunWithFlags.enabled?(:business_tier, for: user)
plan = Plans.get_regular_plan(subscription, only_non_expired: true)
loses_grandfathering = business_tiers_available? && plan && plan.generation < 4
assigns = assign(assigns, :loses_grandfathering, loses_grandfathering)
~H"""
<p :if={@loses_grandfathering} class="mt-2">
Please also note that by letting your subscription expire, you lose access to our grandfathered terms. If you want to subscribe again after that, your account will be offered the <.link
href="https://plausible.io/#pricing"
target="_blank"
rel="noopener noreferrer"
class="underline"
>latest pricing</.link>.
</p>
"""
end
defp change_plan_or_upgrade_text(nil), do: "Upgrade"
defp change_plan_or_upgrade_text(%Subscription{status: Subscription.Status.deleted()}),

View File

@ -4,6 +4,21 @@ defmodule PlausibleWeb.Components.Generic do
"""
use Phoenix.Component
@notice_themes %{
yellow: %{
bg: "bg-yellow-50 dark:bg-yellow-100",
icon: "text-yellow-400",
title_text: "text-yellow-800 dark:text-yellow-900",
body_text: "text-yellow-700 dark:text-yellow-800"
},
red: %{
bg: "bg-red-100",
icon: "text-red-700",
title_text: "text-red-800 dark:text-red-900",
body_text: "text-red-700 dark:text-red-800"
}
}
attr(:type, :string, default: "button")
attr(:class, :string, default: "")
attr(:disabled, :boolean, default: false)
@ -58,28 +73,31 @@ defmodule PlausibleWeb.Components.Generic do
"""
end
attr(:title, :string, default: "Notice")
attr(:title, :any, default: nil)
attr(:size, :atom, default: :sm)
attr(:theme, :atom, default: :yellow)
attr(:dismissable_id, :any, default: nil)
attr(:class, :string, default: "")
attr(:rest, :global)
slot(:inner_block)
def notice(assigns) do
assigns = assign(assigns, :theme, Map.fetch!(@notice_themes, assigns.theme))
~H"""
<div id={@dismissable_id} class={@dismissable_id && "hidden"}>
<div class={"rounded-md bg-yellow-50 dark:bg-yellow-100 p-4 relative #{@class}"} {@rest}>
<div class={["rounded-md p-4 relative", @theme.bg, @class]} {@rest}>
<button
:if={@dismissable_id}
class="absolute right-0 top-0 m-2 text-yellow-800 dark:text-yellow-900"
class={"absolute right-0 top-0 m-2 #{@theme.title_text}"}
onclick={"localStorage['notice_dismissed__#{@dismissable_id}'] = 'true'; document.getElementById('#{@dismissable_id}').classList.add('hidden')"}
>
<Heroicons.x_mark class="h-4 w-4 hover:stroke-2" />
</button>
<div class="flex">
<div :if={@size !== :xs} class="flex-shrink-0">
<div :if={@title} class="flex-shrink-0">
<svg
class="h-5 w-5 text-yellow-400"
class={"h-5 w-5 #{@theme.icon}"}
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
@ -91,14 +109,11 @@ defmodule PlausibleWeb.Components.Generic do
/>
</svg>
</div>
<div class="ml-3">
<h3
:if={@size !== :xs}
class={"text-#{@size} font-medium text-yellow-800 dark:text-yellow-900 mb-2"}
>
<div class={["w-full", @title && "ml-3"]}>
<h3 :if={@title} class={"text-#{@size} font-medium #{@theme.title_text} mb-2"}>
<%= @title %>
</h3>
<div class={"text-#{@size} text-yellow-700 dark:text-yellow-800"}>
<div class={"text-#{@size} #{@theme.body_text}"}>
<p>
<%= render_slot(@inner_block) %>
</p>
@ -107,7 +122,7 @@ defmodule PlausibleWeb.Components.Generic do
</div>
</div>
</div>
<script data-key={@dismissable_id}>
<script :if={@dismissable_id} data-key={@dismissable_id}>
const dismissId = document.currentScript.dataset.key
const localStorageKey = `notice_dismissed__${dismissId}`

View File

@ -25,41 +25,10 @@
<div class="my-4 border-b border-gray-400"></div>
<div
:if={
@subscription && @subscription.status == Plausible.Billing.Subscription.Status.deleted()
}
class="p-2 bg-red-100 rounded-lg sm:p-3"
>
<div class="flex flex-wrap items-center justify-between">
<div class="flex items-center flex-1 w-0">
<svg
class="w-6 h-6 text-red-800"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 9V11M12 15H12.01M5.07183 19H18.9282C20.4678 19 21.4301 17.3333 20.6603 16L13.7321 4C12.9623 2.66667 11.0378 2.66667 10.268 4L3.33978 16C2.56998 17.3333 3.53223 19 5.07183 19Z"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<p class="ml-3 font-medium text-red-800">
<%= if @subscription.next_bill_date && Timex.compare(@subscription.next_bill_date, Timex.today()) >= 0 do %>
Your subscription is cancelled but you have access to your stats until <%= Timex.format!(
@subscription.next_bill_date,
"{Mshort} {D}, {YYYY}"
) %>. Upgrade below to make sure you don't lose access.
<% else %>
Your subscription is cancelled. Upgrade below to get access to your stats again.
<% end %>
</p>
</div>
</div>
</div>
<PlausibleWeb.Components.Billing.subscription_cancelled_notice
user={@user}
dismissable={false}
/>
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
<PlausibleWeb.Components.Billing.monthly_quota_box

View File

@ -96,15 +96,19 @@
<% end %>
<%= if @conn.assigns[:current_user] do %>
<.subscription_past_due_notice
subscription={@conn.assigns.current_user.subscription}
class="container"
/>
<div class="flex flex-col gap-y-2">
<.subscription_cancelled_notice user={@conn.assigns.current_user} />
<.subscription_paused_notice
subscription={@conn.assigns.current_user.subscription}
class="container"
/>
<.subscription_past_due_notice
subscription={@conn.assigns.current_user.subscription}
class="container"
/>
<.private_preview_end_notice user={@conn.assigns.current_user} />
<.subscription_paused_notice
subscription={@conn.assigns.current_user.subscription}
class="container"
/>
<.private_preview_end_notice user={@conn.assigns.current_user} />
</div>
<% end %>

View File

@ -5,56 +5,92 @@ defmodule Plausible.Billing.PlansTest do
@legacy_plan_id "558746"
@v1_plan_id "558018"
@v2_plan_id "654177"
@v3_plan_id "749342"
@v4_plan_id "857097"
@v3_business_plan_id "857481"
@v4_business_plan_id "857105"
describe "getting subscription plans for user" do
test "growth_plans_for/1 returns v1 plans for a user on a legacy plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|> Plans.growth_plans_for()
|> assert_generation(1)
end
test "growth_plans_for/1 returns v1 plans for users who are already on v1 pricing" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v1_plan_id
insert(:user, subscription: build(:subscription, paddle_plan_id: @v1_plan_id))
|> Plans.growth_plans_for()
|> assert_generation(1)
end
test "growth_plans_for/1 returns v2 plans for users who are already on v2 pricing" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v2_plan_id
insert(:user, subscription: build(:subscription, paddle_plan_id: @v2_plan_id))
|> Plans.growth_plans_for()
|> assert_generation(2)
end
test "growth_plans_for/1 shows v3 pricing for users who signed up before the business tier" do
user = insert(:user, inserted_at: ~U[2023-10-01T00:00:00Z])
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v3_plan_id
test "growth_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
insert(:user, trial_expiry_date: nil)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
insert(:user, trial_expiry_date: ~D[2023-12-24])
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 returns v3 plans for pre business tier trials only if their trial is active or expired less than 10 days ago" do
trial_start = ~D[2023-10-27]
trial_expiry = Timex.shift(trial_start, days: 30)
expiry_datetime = Timex.to_datetime(trial_expiry)
user = insert(:user, trial_expiry_date: trial_expiry)
now1 = Timex.shift(expiry_datetime, days: -1)
now2 = Timex.shift(expiry_datetime, days: 10)
now3 = Timex.shift(expiry_datetime, days: 11)
Plans.growth_plans_for(user, now1) |> assert_generation(3)
Plans.growth_plans_for(user, now2) |> assert_generation(3)
Plans.growth_plans_for(user, now3) |> assert_generation(4)
end
test "growth_plans_for/1 returns v4 plans for expired legacy subscriptions" do
subscription =
build(:subscription,
paddle_plan_id: @v1_plan_id,
status: :deleted,
next_bill_date: ~D[2023-11-10]
)
insert(:user, subscription: subscription)
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 shows v4 plans for everyone else" do
user = insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "growth_plans_for/1 does not return business plans" do
user = insert(:user)
Plans.growth_plans_for(user)
insert(:user)
|> Plans.growth_plans_for()
|> Enum.each(fn plan ->
assert plan.kind != :business
end)
end
test "growth_plans_for/1 returns the latest generation of growth plans for a user with a business subscription" do
user =
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
assert List.first(Plans.growth_plans_for(user)).monthly_product_id == @v4_plan_id
insert(:user, subscription: build(:subscription, paddle_plan_id: @v3_business_plan_id))
|> Plans.growth_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v3 business plans for a user on a legacy plan" do
user = insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
assert List.first(Plans.business_plans_for(user)).monthly_product_id == @v3_business_plan_id
insert(:user, subscription: build(:subscription, paddle_plan_id: @legacy_plan_id))
|> Plans.business_plans_for()
|> assert_generation(3)
end
test "business_plans_for/1 returns v3 business plans for a v2 subscriber" do
@ -63,12 +99,48 @@ defmodule Plausible.Billing.PlansTest do
business_plans = Plans.business_plans_for(user)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert List.first(business_plans).monthly_product_id == @v3_business_plan_id
assert_generation(business_plans, 3)
end
test "business_plans_for/1 returns v3 business plans for users who signed up before the business tier release" do
user = insert(:user, inserted_at: ~U[2023-10-01T00:00:00Z])
assert List.first(Plans.business_plans_for(user)).monthly_product_id == @v3_business_plan_id
test "business_plans_for/1 returns v4 plans for invited users with trial_expiry = nil" do
insert(:user, trial_expiry_date: nil)
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v4 plans for users whose trial started after the business tiers release" do
insert(:user, trial_expiry_date: ~D[2023-12-24])
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v3 plans for pre business tier trials only if their trial is active or expired less than 10 days ago" do
trial_start = ~D[2023-10-27]
trial_expiry = Timex.shift(trial_start, days: 30)
expiry_datetime = Timex.to_datetime(trial_expiry)
user = insert(:user, trial_expiry_date: trial_expiry)
now1 = Timex.shift(expiry_datetime, days: -1)
now2 = Timex.shift(expiry_datetime, days: 10)
now3 = Timex.shift(expiry_datetime, days: 11)
Plans.business_plans_for(user, now1) |> assert_generation(3)
Plans.business_plans_for(user, now2) |> assert_generation(3)
Plans.business_plans_for(user, now3) |> assert_generation(4)
end
test "business_plans_for/1 returns v4 plans for expired legacy subscriptions" do
subscription =
build(:subscription,
paddle_plan_id: @v2_plan_id,
status: :deleted,
next_bill_date: ~D[2023-11-10]
)
insert(:user, subscription: subscription)
|> Plans.business_plans_for()
|> assert_generation(4)
end
test "business_plans_for/1 returns v4 business plans for everyone else" do
@ -76,7 +148,7 @@ defmodule Plausible.Billing.PlansTest do
business_plans = Plans.business_plans_for(user)
assert Enum.all?(business_plans, &(&1.kind == :business))
assert List.first(business_plans).monthly_product_id == @v4_business_plan_id
assert_generation(business_plans, 4)
end
test "available_plans returns all plans for user with prices when asked for" do
@ -275,4 +347,8 @@ defmodule Plausible.Billing.PlansTest do
assert Plans.suggest_tier(user) == :business
end
end
defp assert_generation(plans_list, generation) do
assert List.first(plans_list).generation == generation
end
end

View File

@ -16,6 +16,7 @@ defmodule PlausibleWeb.AuthControllerTest do
setup :verify_on_exit!
@v3_plan_id "749355"
@v4_plan_id "857097"
@configured_enterprise_plan_paddle_plan_id "123"
describe "GET /register" do
@ -627,6 +628,67 @@ defmodule PlausibleWeb.AuthControllerTest do
Routes.billing_path(conn, :choose_plan)
end
test "renders cancelled subscription notice", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
next_bill_date: ~D[2023-01-01]
)
notice_text =
get(conn, "/settings")
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "Upgrade your subscription to get access to your stats again"
end
test "renders cancelled subscription notice with some subscription days still left", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v4_plan_id,
user: user,
status: :deleted,
next_bill_date: Timex.shift(Timex.today(), days: 10)
)
notice_text =
get(conn, "/settings")
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "You have access to your stats until"
assert notice_text =~ "Upgrade your subscription to make sure you don't lose access"
end
test "renders cancelled subscription notice with a warning about losing grandfathering", %{
conn: conn,
user: user
} do
insert(:subscription,
paddle_plan_id: @v3_plan_id,
user: user,
status: :deleted,
next_bill_date: Timex.shift(Timex.today(), days: 10)
)
notice_text =
get(conn, "/settings")
|> html_response(200)
|> text_of_element("#global-subscription-cancelled-notice")
assert notice_text =~ "Subscription cancelled"
assert notice_text =~ "You have access to your stats until"
assert notice_text =~
"by letting your subscription expire, you lose access to our grandfathered terms"
end
test "shows invoices for subscribed user", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",

View File

@ -34,7 +34,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
describe "for a legacy trial (user registered before business tiers release)" do
setup %{conn: conn} do
user = insert(:user, inserted_at: ~N[2023-10-25 12:00:00])
user = insert(:user, trial_expiry_date: ~D[2023-11-24])
{:ok, conn: conn} = log_in(%{conn: conn, user: user})
{:ok, conn: conn, user: user}
end

View File

@ -35,7 +35,12 @@ defmodule Plausible.TestUtils do
end
def create_user(_) do
{:ok, user: Factory.insert(:user, inserted_at: ~U[2024-01-01T00:00:00Z])}
{:ok,
user:
Factory.insert(:user,
inserted_at: ~U[2024-01-01T00:00:00Z],
trial_expiry_date: ~D[2024-02-01]
)}
end
def create_site(%{user: user}) do