Refactor enterprise plan upgrade and change-plan actions (#3397)

* rename enterprise?/1 function

* change link text to Upgrade when subscription deleted

* extract paddle_button and paddle_script components

* create a new upgrade-to-enterprise-plan page

* extract upgrade_link component

* rename function

* link to enterprise plan upgrade page from settings

...if the user has an enterprise plan configured

* fetch enterprise plan price on the new page

* add change_enterprise_plan functionality on the new page

* render existing change_enterprise_plan_contact_us.html

...when subscribed to latest configured enterprise plan

* rename vars and extract resumable? fn

* remove dead billing route

* small test refactor: extract convenience fn

* add tests for...

...restricting paused and past_due subscription access to the new
enterprise plan page.

1. redirect to /settings from the controller action
2. hiding the change-plan link from the user settings

* implement redirect to /settings

* hide the enterprise upgrade/change-plan link

* add tests for a deleted enterprise subscription

* plug in the new controller action and delete dead code

* optimize for dark mode

* fix compile warning

* credo fix

* display N/A instead of crash when price nil

* change subscription.status type to Ecto.Enum

Also, create a new `Subscription.Status` module that exposes macros to
return the used atom values (prevent typos at compiletime).

* fix bug (@conn not available anymore)

* use Routes.billing_path where applicable

* add a status() type

* silence credo

* refactor suggestion from review

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>

* Remove the __using__ macro from Subscription.Status

... instead be explicit about requires and aliases and also order
the use, import, require, and alias clauses according to
https://github.com/christopheradams/elixir_style_guide#module-attribute-ordering

* drop the virtual Enteprise 'price_per_interval' field

* apply review suggestion to make the code more DRY

* use dot syntax to fetch current user in new controller actions

* fix formatting

---------

Co-authored-by: Adrian Gruntkowski <adrian.gruntkowski@gmail.com>
This commit is contained in:
RobertJoonas 2023-10-10 20:35:17 +03:00 committed by GitHub
parent dec193e904
commit 3d2f356ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 907 additions and 464 deletions

View File

@ -8,16 +8,17 @@ defmodule Mix.Tasks.CancelSubscription do
use Mix.Task
use Plausible.Repo
alias Plausible.{Repo, Billing.Subscription}
require Plausible.Billing.Subscription.Status
require Logger
alias Plausible.{Repo, Billing.Subscription}
def run([paddle_subscription_id]) do
Mix.Task.run("app.start")
Repo.get_by!(Subscription, paddle_subscription_id: paddle_subscription_id)
|> Subscription.changeset(%{status: "deleted"})
|> Subscription.changeset(%{status: Subscription.Status.deleted()})
|> Repo.update!()
Logger.info("Successfully set the subscription status to 'deleted'.")
Logger.info("Successfully set the subscription status to #{Subscription.Status.deleted()}")
end
end

View File

@ -118,10 +118,11 @@ defmodule Plausible.Auth do
user_id in Application.get_env(:plausible, :super_admin_user_ids)
end
def enterprise?(nil), do: false
def enterprise_configured?(nil), do: false
def enterprise?(%Plausible.Auth.User{} = user) do
user = Repo.preload(user, :enterprise_plan)
user.enterprise_plan != nil
def enterprise_configured?(%Plausible.Auth.User{} = user) do
user
|> Ecto.assoc(:enterprise_plan)
|> Repo.exists?()
end
end

View File

@ -1,5 +1,7 @@
defmodule Plausible.Auth.UserAdmin do
use Plausible.Repo
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
def custom_index_query(_conn, _schema, query) do
subscripton_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at])
@ -83,7 +85,7 @@ defmodule Plausible.Auth.UserAdmin do
end
defp subscription_plan(user) do
if user.subscription && user.subscription.status == "active" &&
if user.subscription && user.subscription.status == Subscription.Status.active() &&
user.subscription.paddle_subscription_id do
quota = PlausibleWeb.AuthView.subscription_quota(user.subscription)
interval = PlausibleWeb.AuthView.subscription_interval(user.subscription)

View File

@ -1,5 +1,6 @@
defmodule Plausible.Billing do
use Plausible.Repo
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
@spec active_subscription_for(integer()) :: Subscription.t() | nil
@ -92,10 +93,12 @@ defmodule Plausible.Billing do
end
end
defp subscription_is_active?(%Subscription{status: "active"}), do: true
defp subscription_is_active?(%Subscription{status: "past_due"}), do: true
defp subscription_is_active?(%Subscription{status: Subscription.Status.active()}), do: true
defp subscription_is_active?(%Subscription{status: Subscription.Status.past_due()}), do: true
defp subscription_is_active?(%Subscription{status: "deleted"} = subscription) do
defp subscription_is_active?(
%Subscription{status: Subscription.Status.deleted()} = subscription
) do
subscription.next_bill_date && !Timex.before?(subscription.next_bill_date, Timex.today())
end
@ -269,7 +272,7 @@ defmodule Plausible.Billing do
defp active_subscription_query(user_id) do
from(s in Subscription,
where: s.user_id == ^user_id and s.status == "active",
where: s.user_id == ^user_id and s.status == ^Subscription.Status.active(),
order_by: [desc: s.inserted_at],
limit: 1
)

View File

@ -134,6 +134,19 @@ defmodule Plausible.Billing.Plans do
end
end
def latest_enterprise_plan_with_price(user) do
enterprise_plan =
Repo.one!(
from(e in EnterprisePlan,
where: e.user_id == ^user.id,
order_by: [desc: e.inserted_at],
limit: 1
)
)
{enterprise_plan, get_price_for(enterprise_plan)}
end
def subscription_interval(subscription) do
case get_subscription_plan(subscription) do
%EnterprisePlan{billing_interval: interval} ->
@ -185,6 +198,13 @@ defmodule Plausible.Billing.Plans do
end
end
def get_price_for(%EnterprisePlan{paddle_plan_id: product_id}) do
case Plausible.Billing.paddle_api().fetch_prices([product_id]) do
{:ok, prices} -> Map.fetch!(prices, product_id)
{:error, :api_error} -> nil
end
end
defp get_enterprise_plan(%Subscription{} = subscription) do
Repo.get_by(EnterprisePlan,
user_id: subscription.user_id,
@ -217,7 +237,7 @@ defmodule Plausible.Billing.Plans do
def suggest(user, usage_during_cycle) do
cond do
usage_during_cycle > @enterprise_level_usage -> :enterprise
Plausible.Auth.enterprise?(user) -> :enterprise
Plausible.Auth.enterprise_configured?(user) -> :enterprise
true -> suggest_by_usage(user, usage_during_cycle)
end
end

View File

@ -1,38 +1,10 @@
defmodule Plausible.Billing.Subscription do
@moduledoc false
use Ecto.Schema
import Ecto.Changeset
@moduledoc """
The subscription statuses are stored in Paddle. They can only be changed
through Paddle webhooks, which always send the current subscription status
via the payload.
* `active` - All good with the payments. Can access stats.
* `past_due` - The payment has failed, but we're trying to charge the customer
again. Access to stats is still granted. There will be three retries - after
3, 5, and 7 days have passed from the first failure. After a failure on the
final retry, the subscription status will change to `paused`. As soon as the
customer updates their billing details, Paddle will charge them again, and
after a successful payment, the subscription will become `active` again.
* `paused` - we've tried to charge the customer but all the retries have failed.
Stats access restricted. As soon as the customer updates their billing details,
Paddle will charge them again, and after a successful payment, the subscription
will become `active` again.
* `deleted` - The customer has triggered the cancel subscription action. Access
to stats should be granted for the time the customer has already paid for. If
they want to upgrade again, new billing details have to be provided.
Paddle documentation links for reference:
* Subscription statuses -
https://developer.paddle.com/classic/reference/zg9joji1mzu0mdi2-subscription-status-reference
* Payment failures -
https://developer.paddle.com/classic/guides/zg9joji1mzu0mduy-payment-failures
"""
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
@type t() :: %__MODULE__{}
@ -50,14 +22,12 @@ defmodule Plausible.Billing.Subscription do
@optional_fields [:last_bill_date]
@valid_statuses ["active", "past_due", "deleted", "paused"]
schema "subscriptions" do
field :paddle_subscription_id, :string
field :paddle_plan_id, :string
field :update_url, :string
field :cancel_url, :string
field :status, :string
field :status, Ecto.Enum, values: Subscription.Status.valid_statuses()
field :next_bill_amount, :string
field :next_bill_date, :date
field :last_bill_date, :date
@ -72,20 +42,18 @@ defmodule Plausible.Billing.Subscription do
model
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:status, @valid_statuses)
|> unique_constraint(:paddle_subscription_id)
end
def free(attrs \\ %{}) do
%__MODULE__{
paddle_plan_id: "free_10k",
status: "active",
status: Subscription.Status.active(),
next_bill_amount: "0",
currency_code: "EUR"
}
|> cast(attrs, @required_fields)
|> validate_required([:user_id])
|> validate_inclusion(:status, @valid_statuses)
|> unique_constraint(:paddle_subscription_id)
end
end

View File

@ -0,0 +1,45 @@
defmodule Plausible.Billing.Subscription.Status do
@moduledoc """
The subscription statuses are stored in Paddle. They can only be changed
through Paddle webhooks, which always send the current subscription status
via the payload.
* `active` - All good with the payments. Can access stats.
* `past_due` - The payment has failed, but we're trying to charge the customer
again. Access to stats is still granted. There will be three retries - after
3, 5, and 7 days have passed from the first failure. After a failure on the
final retry, the subscription status will change to `paused`. As soon as the
customer updates their billing details, Paddle will charge them again, and
after a successful payment, the subscription will become `active` again.
* `paused` - we've tried to charge the customer but all the retries have failed.
Stats access restricted. As soon as the customer updates their billing details,
Paddle will charge them again, and after a successful payment, the subscription
will become `active` again.
* `deleted` - The customer has triggered the cancel subscription action. Access
to stats should be granted for the time the customer has already paid for. If
they want to upgrade again, new billing details have to be provided.
Paddle documentation links for reference:
* Subscription statuses -
https://developer.paddle.com/classic/reference/zg9joji1mzu0mdi2-subscription-status-reference
* Payment failures -
https://developer.paddle.com/classic/guides/zg9joji1mzu0mduy-payment-failures
"""
@statuses [:active, :past_due, :paused, :deleted]
@type status() :: unquote(Enum.reduce(@statuses, &{:|, [], [&1, &2]}))
for status <- @statuses do
defmacro unquote(status)(), do: unquote(status)
end
def valid_statuses() do
@statuses
end
end

View File

@ -1,7 +1,8 @@
defmodule Plausible.Billing.Subscriptions do
@moduledoc false
alias Plausible.Billing.{Subscription}
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
@spec expired?(Subscription.t()) :: boolean()
@doc """
@ -14,9 +15,19 @@ defmodule Plausible.Billing.Subscriptions do
def expired?(%Subscription{paddle_plan_id: "free_10k"}), do: false
def expired?(%Subscription{status: status, next_bill_date: next_bill_date}) do
cancelled? = status == "deleted"
cancelled? = status == Subscription.Status.deleted()
expired? = Timex.compare(next_bill_date, Timex.today()) < 0
cancelled? && expired?
end
def resumable?(nil), do: false
def resumable?(%Subscription{status: status}) do
status in [
Subscription.Status.active(),
Subscription.Status.past_due(),
Subscription.Status.paused()
]
end
end

View File

@ -3,6 +3,7 @@ defmodule PlausibleWeb.Components.Billing do
use Phoenix.Component
import PlausibleWeb.Components.Generic
require Plausible.Billing.Subscription.Status
alias PlausibleWeb.Router.Helpers, as: Routes
alias Plausible.Billing.Subscription
@ -50,6 +51,7 @@ defmodule PlausibleWeb.Components.Billing do
def monthly_quota_box(%{business_tier: true} = assigns) do
~H"""
<div
id="monthly-quota-box"
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
style="width: 11.75rem;"
>
@ -57,8 +59,13 @@ defmodule PlausibleWeb.Components.Billing do
<div class="py-2 text-xl font-medium dark:text-gray-100">
<%= PlausibleWeb.AuthView.subscription_quota(@subscription, format: :long) %>
</div>
<.styled_link href={Routes.billing_path(@conn, :choose_plan)} class="text-sm font-medium">
<%= upgrade_link_text(@subscription) %>
<.styled_link
:if={show_upgrade_or_change_plan_link?(@user, @subscription)}
id="#upgrade-or-change-plan-link"
href={upgrade_link_href(@user)}
class="text-sm font-medium"
>
<%= change_plan_or_upgrade_text(@subscription) %>
</.styled_link>
</div>
"""
@ -77,15 +84,15 @@ defmodule PlausibleWeb.Components.Billing do
</div>
<.styled_link
:if={@subscription.status == "active"}
href={Routes.billing_path(@conn, :change_plan_form)}
:if={@subscription.status == Subscription.Status.active()}
href={Routes.billing_path(PlausibleWeb.Endpoint, :change_plan_form)}
class="text-sm font-medium"
>
Change plan
</.styled_link>
<span
:if={@subscription.status == "past_due"}
:if={@subscription.status == Subscription.Status.past_due()}
class="text-sm text-gray-600 dark:text-gray-400 font-medium"
tooltip="Please update your billing details before changing plans"
>
@ -93,7 +100,10 @@ defmodule PlausibleWeb.Components.Billing do
</span>
<% else %>
<div class="py-2 text-xl font-medium dark:text-gray-100">Free trial</div>
<.styled_link href={Routes.billing_path(@conn, :upgrade)} class="text-sm font-medium">
<.styled_link
href={Routes.billing_path(PlausibleWeb.Endpoint, :upgrade)}
class="text-sm font-medium"
>
Upgrade
</.styled_link>
<% end %>
@ -101,7 +111,9 @@ defmodule PlausibleWeb.Components.Billing do
"""
end
def subscription_past_due_notice(%{subscription: %Subscription{status: "past_due"}} = assigns) do
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">
@ -142,7 +154,9 @@ defmodule PlausibleWeb.Components.Billing do
def subscription_past_due_notice(assigns), do: ~H""
def subscription_paused_notice(%{subscription: %Subscription{status: "paused"}} = assigns) do
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">
@ -183,12 +197,111 @@ defmodule PlausibleWeb.Components.Billing do
def subscription_paused_notice(assigns), do: ~H""
def format_price(%Money{} = money) do
def present_enterprise_plan(assigns) do
~H"""
<ul class="w-full py-4">
<li>
Up to <b><%= present_limit(@plan, :monthly_pageview_limit) %></b> monthly pageviews
</li>
<li>
Up to <b><%= present_limit(@plan, :site_limit) %></b> sites
</li>
<li>
Up to <b><%= present_limit(@plan, :hourly_api_request_limit) %></b> hourly api requests
</li>
</ul>
"""
end
defp present_limit(enterprise_plan, key) do
enterprise_plan
|> Map.fetch!(key)
|> PlausibleWeb.StatsView.large_number_format()
end
@spec format_price(Money.t(), Keyword.t()) :: String.t()
def format_price(money, opts \\ []) do
opts =
opts
|> Keyword.put_new(:format, :short)
|> Keyword.put_new(:fractional_digits, 2)
money
|> Money.to_string!(format: :short, fractional_digits: 2)
|> Money.to_string!(opts)
|> String.replace(".00", "")
end
defp upgrade_link_text(nil), do: "Upgrade"
defp upgrade_link_text(_subscription), do: "Change plan"
def paddle_button(assigns) do
~H"""
<button
id={@id}
onclick={"Paddle.Checkout.open(#{Jason.encode!(%{product: @paddle_product_id, email: @user.email, disableLogout: true, passthrough: @user.id, success: Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), theme: "none"})})"}
class="w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white bg-indigo-600 hover:bg-indigo-500"
>
<%= render_slot(@inner_block) %>
</button>
"""
end
def paddle_script(assigns) do
~H"""
<script type="text/javascript" src="https://cdn.paddle.com/paddle/paddle.js">
</script>
<script :if={Application.get_env(:plausible, :environment) == "dev"}>
Paddle.Environment.set('sandbox')
</script>
<script>
Paddle.Setup({vendor: <%= Application.get_env(:plausible, :paddle) |> Keyword.fetch!(:vendor_id) %> })
</script>
"""
end
def upgrade_link(%{business_tier: true} = assigns) do
~H"""
<.link
id="upgrade-link-2"
href={upgrade_link_href(@user)}
class="inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
>
Upgrade
</.link>
"""
end
def upgrade_link(assigns) do
~H"""
<.link
href={Routes.billing_path(PlausibleWeb.Endpoint, :upgrade)}
class="inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
>
Upgrade
</.link>
"""
end
defp upgrade_link_href(user) do
action =
if Plausible.Auth.enterprise_configured?(user),
do: :upgrade_to_enterprise_plan,
else: :choose_plan
Routes.billing_path(PlausibleWeb.Endpoint, action)
end
defp change_plan_or_upgrade_text(nil), do: "Upgrade"
defp change_plan_or_upgrade_text(%Subscription{status: Subscription.Status.deleted()}),
do: "Upgrade"
defp change_plan_or_upgrade_text(_subscription), do: "Change plan"
defp show_upgrade_or_change_plan_link?(user, subscription) do
is_enterprise? = Plausible.Auth.enterprise_configured?(user)
subscription_halted? =
subscription &&
subscription.status in [Subscription.Status.past_due(), Subscription.Status.paused()]
!(is_enterprise? && subscription_halted?)
end
end

View File

@ -42,6 +42,7 @@ defmodule PlausibleWeb.Components.Generic do
"""
end
attr :id, :any, default: nil
attr :href, :string, required: true
attr :new_tab, :boolean
attr :class, :string, default: ""
@ -53,6 +54,7 @@ defmodule PlausibleWeb.Components.Generic do
~H"""
<.link
id={@id}
class={[
"inline-flex items-center gap-x-0.5 text-indigo-600 hover:text-indigo-700 dark:text-indigo-500 dark:hover:text-indigo-600",
@class

View File

@ -1,8 +1,10 @@
defmodule PlausibleWeb.BillingController do
use PlausibleWeb, :controller
use Plausible.Repo
alias Plausible.Billing
require Logger
require Plausible.Billing.Subscription.Status
alias Plausible.Billing
alias Plausible.Billing.{Plans, Subscription}
plug PlausibleWeb.RequireAccountPlug
@ -12,18 +14,14 @@ defmodule PlausibleWeb.BillingController do
end
def upgrade(conn, _params) do
user =
conn.assigns[:current_user]
|> Repo.preload(:enterprise_plan)
user = conn.assigns[:current_user]
cond do
user.subscription && user.subscription.status == "active" ->
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
Plausible.Auth.enterprise_configured?(user) ->
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
user.enterprise_plan ->
redirect(conn,
to: Routes.billing_path(conn, :upgrade_enterprise_plan, user.enterprise_plan.id)
)
user.subscription && user.subscription.status == Subscription.Status.active() ->
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
true ->
render(conn, "upgrade.html",
@ -36,7 +34,7 @@ defmodule PlausibleWeb.BillingController do
end
def choose_plan(conn, _params) do
user = conn.assigns[:current_user]
user = conn.assigns.current_user
if FunWithFlags.enabled?(:business_tier, for: user) do
render(conn, "choose_plan.html",
@ -50,54 +48,66 @@ defmodule PlausibleWeb.BillingController do
end
end
def upgrade_enterprise_plan(conn, %{"plan_id" => plan_id}) do
user = conn.assigns[:current_user]
subscription = user.subscription
plan = Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: user.id, id: plan_id)
def upgrade_to_enterprise_plan(conn, _params) do
user = Plausible.Users.with_subscription(conn.assigns.current_user)
cond do
plan && subscription && plan.paddle_plan_id == subscription.paddle_plan_id ->
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
if FunWithFlags.enabled?(:business_tier, for: user) do
{latest_enterprise_plan, price} = Plans.latest_enterprise_plan_with_price(user)
plan ->
render(conn, "upgrade_to_plan.html",
skip_plausible_tracking: true,
user: user,
plan: plan,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
subscription_resumable? = Plausible.Billing.Subscriptions.resumable?(user.subscription)
true ->
render_error(conn, 404)
subscribed_to_latest? =
subscription_resumable? &&
user.subscription.paddle_plan_id == latest_enterprise_plan.paddle_plan_id
cond do
user.subscription &&
user.subscription.status in [
Subscription.Status.past_due(),
Subscription.Status.paused()
] ->
redirect(conn, to: Routes.auth_path(conn, :user_settings))
subscribed_to_latest? ->
render(conn, "change_enterprise_plan_contact_us.html",
skip_plausible_tracking: true,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
true ->
render(conn, "upgrade_to_enterprise_plan.html",
user: user,
latest_enterprise_plan: latest_enterprise_plan,
price: price,
subscription_resumable: subscription_resumable?,
contact_link: "https://plausible.io/contact",
skip_plausible_tracking: true,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
else
render_error(conn, 404)
end
end
def upgrade_enterprise_plan(conn, _params) do
# DEPRECATED - For some time we need to ensure that the existing
# links sent out to customers will lead the user to the right place
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
end
def upgrade_success(conn, _params) do
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
end
def change_plan_form(conn, _params) do
user =
conn.assigns[:current_user]
|> Repo.preload(:enterprise_plan)
user = conn.assigns[:current_user]
subscription = Billing.active_subscription_for(user.id)
cond do
subscription && user.enterprise_plan &&
subscription.paddle_plan_id !== user.enterprise_plan.paddle_plan_id ->
redirect(conn,
to: Routes.billing_path(conn, :change_enterprise_plan, user.enterprise_plan.id)
)
subscription && user.enterprise_plan &&
subscription.paddle_plan_id == user.enterprise_plan.paddle_plan_id ->
render(conn, "change_enterprise_plan_contact_us.html",
skip_plausible_tracking: true,
user: user,
plan: user.enterprise_plan,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
Plausible.Auth.enterprise_configured?(user) ->
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
subscription ->
render(conn, "change_plan.html",
@ -111,26 +121,10 @@ defmodule PlausibleWeb.BillingController do
end
end
def change_enterprise_plan(conn, %{"plan_id" => plan_id}) do
user = conn.assigns[:current_user]
new_plan = Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: user.id, id: plan_id)
cond do
is_nil(user.subscription) ->
redirect(conn, to: "/billing/upgrade")
is_nil(new_plan) || new_plan.paddle_plan_id == user.subscription.paddle_plan_id ->
render_error(conn, 404)
true ->
render(conn, "change_enterprise_plan.html",
skip_plausible_tracking: true,
user: user,
plan: new_plan,
layout: {PlausibleWeb.LayoutView, "focus.html"}
)
end
def change_enterprise_plan(conn, _params) do
# DEPRECATED - For some time we need to ensure that the existing
# links sent out to customers will lead the user to the right place
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
end
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
@ -144,7 +138,7 @@ defmodule PlausibleWeb.BillingController do
)
else
_ ->
redirect(conn, to: "/billing/upgrade")
redirect(conn, to: Routes.billing_path(conn, :upgrade))
end
end

View File

@ -4,12 +4,15 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
use Phoenix.LiveView
use Phoenix.HTML
alias Plausible.Users
alias Plausible.Billing.{Plans, Plan, Quota}
alias PlausibleWeb.Router.Helpers, as: Routes
import PlausibleWeb.Components.Billing
require Plausible.Billing.Subscription.Status
alias Plausible.Users
alias Plausible.Billing.{Plans, Plan, Quota, Subscription}
alias PlausibleWeb.Router.Helpers, as: Routes
@contact_link "https://plausible.io/contact"
@billing_faq_link "https://plausible.io/docs/billing"
@ -255,7 +258,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<%= cond do %>
<% !@available -> %>
<.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" />
<% @owned_plan && @user.subscription && @user.subscription.status in ["active", "past_due", "paused"] -> %>
<% @owned_plan && Plausible.Billing.Subscriptions.resumable?(@user.subscription) -> %>
<.render_change_plan_link
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
text={
@ -270,9 +273,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do
/>
<% true -> %>
<.paddle_button
id={"#{@kind}-checkout"}
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
{assigns}
/>
>
Upgrade
</.paddle_button>
<% end %>
</div>
<ul
@ -325,7 +331,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
<.change_plan_link
plan_already_owned={@text == "Currently on this plan"}
billing_details_expired={
@user.subscription && @user.subscription.status in ["past_due", "paused"]
@user.subscription &&
@user.subscription.status in [Subscription.Status.past_due(), Subscription.Status.paused()]
}
{assigns}
/>
@ -336,7 +343,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
~H"""
<.link
id={"#{@kind}-checkout"}
href={"/billing/change-plan/preview/" <> @paddle_product_id}
href={Routes.billing_path(PlausibleWeb.Endpoint, :change_plan_preview, @paddle_product_id)}
class={[
"w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white",
!(@plan_already_owned || @billing_details_expired) && "bg-indigo-600 hover:bg-indigo-500",
@ -355,18 +362,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
end
defp paddle_button(assigns) do
~H"""
<button
id={"#{@kind}-checkout"}
onclick={"Paddle.Checkout.open(#{Jason.encode!(%{product: @paddle_product_id, email: @user.email, disableLogout: true, passthrough: @user.id, success: Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success), theme: "none"})})"}
class="w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white bg-indigo-600 hover:bg-indigo-500"
>
Upgrade
</button>
"""
end
defp contact_button(assigns) do
~H"""
<.link
@ -519,19 +514,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
"""
end
defp paddle_script(assigns) do
~H"""
<script type="text/javascript" src="https://cdn.paddle.com/paddle/paddle.js">
</script>
<script :if={Application.get_env(:plausible, :environment) == "dev"}>
Paddle.Environment.set('sandbox')
</script>
<script>
Paddle.Setup({vendor: <%= Application.get_env(:plausible, :paddle) |> Keyword.fetch!(:vendor_id) %> })
</script>
"""
end
defp slider_styles(assigns) do
~H"""
<style>

View File

@ -201,7 +201,7 @@ defmodule PlausibleWeb.Router do
post "/billing/change-plan/:new_plan_id", BillingController, :change_plan
get "/billing/upgrade", BillingController, :upgrade
get "/billing/choose-plan", BillingController, :choose_plan
get "/billing/upgrade/:plan_id", BillingController, :upgrade_to_plan
get "/billing/upgrade-to-enterprise-plan", BillingController, :upgrade_to_enterprise_plan
get "/billing/upgrade/enterprise/:plan_id", BillingController, :upgrade_enterprise_plan
get "/billing/change-plan/enterprise/:plan_id", BillingController, :change_enterprise_plan
get "/billing/upgrade-success", BillingController, :upgrade_success

View File

@ -26,7 +26,9 @@
<div class="my-4 border-b border-gray-400"></div>
<div
:if={@subscription && @subscription.status == "deleted"}
: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">
@ -61,8 +63,8 @@
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
<PlausibleWeb.Components.Billing.monthly_quota_box
user={@user}
subscription={@subscription}
conn={@conn}
business_tier={FunWithFlags.enabled?(:business_tier, for: @user)}
/>
<div
@ -70,7 +72,7 @@
style="width: 11.75rem;"
>
<h4 class="font-black dark:text-gray-100">Next bill amount</h4>
<%= if @subscription && @subscription.status in ["active", "past_due"] do %>
<%= if @subscription && @subscription.status in [Plausible.Billing.Subscription.Status.active(), Plausible.Billing.Subscription.Status.past_due()] do %>
<div class="py-2 text-xl font-medium dark:text-gray-100">
<%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %>
</div>
@ -90,7 +92,7 @@
>
<h4 class="font-black dark:text-gray-100">Next bill date</h4>
<%= if @subscription && @subscription.next_bill_date && @subscription.status in ["active", "past_due"] do %>
<%= if @subscription && @subscription.next_bill_date && @subscription.status in [Plausible.Billing.Subscription.Status.active(), Plausible.Billing.Subscription.Status.past_due()] do %>
<div class="py-2 text-xl font-medium dark:text-gray-100">
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
</div>
@ -140,7 +142,7 @@
</article>
<%= cond do %>
<% @subscription && @subscription.status in ["active", "past_due", "paused"] && @subscription.cancel_url -> %>
<% Plausible.Billing.Subscriptions.resumable?(@subscription) && @subscription.cancel_url -> %>
<div class="mt-8">
<%= link("Cancel my subscription",
to: @subscription.cancel_url,
@ -150,15 +152,10 @@
</div>
<% true -> %>
<div class="mt-8">
<%= link("Upgrade",
to:
if(FunWithFlags.enabled?(:business_tier, for: @user),
do: PlausibleWeb.Router.Helpers.billing_path(PlausibleWeb.Endpoint, :choose_plan),
else: "/billing/upgrade"
),
class:
"inline-block px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
) %>
<PlausibleWeb.Components.Billing.upgrade_link
user={@user}
business_tier={FunWithFlags.enabled?(:business_tier, for: @user)}
/>
</div>
<% end %>
</div>
@ -344,7 +341,7 @@
Deleting your account removes all sites and stats you've collected
</p>
<%= if @subscription && @subscription.status == "active" do %>
<%= if @subscription && @subscription.status == Plausible.Billing.Subscription.Status.active() do %>
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">
Delete my account
</span>

View File

@ -1,68 +0,0 @@
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fetch-jsonp/1.1.3/fetch-jsonp.min.js"></script>
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black dark:text-gray-100">Change subscription plan</h1>
</div>
<script>
plan = function() {
return {
localizedPlan: null,
price() {
var currency = {
'USD': '$',
'EUR': '',
'GBP': '£'
}[this.localizedPlan.currency]
return currency + this.localizedPlan.price.net
},
fetchPlan() {
fetchJsonp('<%= Plausible.Billing.PaddleApi.checkout_domain() %>/api/2.0/prices?product_ids=<%= @plan.paddle_plan_id %>')
.then(res => res.json())
.then((data) => {
this.localizedPlan = data.response.products[0]
})
}
}
}
</script>
<div class="w-full max-w-lg px-4 mx-auto mt-4">
<div x-init="fetchPlan()" x-data="window.plan()" class="flex-1 p-8 mt-8 bg-white rounded shadow-md dark:bg-gray-800">
<div x-show="!localizedPlan" class="mx-auto my-40 loading sm"><div></div></div>
<template x-if="localizedPlan">
<div>
<div class="w-full pb-4 dark:text-gray-100">
<span>We've prepared your account for an upgrade to custom limits outside the listed plans:</span>
</div>
<ul class="w-full py-4 dark:text-gray-100">
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.monthly_pageview_limit) %></b> monthly pageviews</li>
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.site_limit) %></b> sites</li>
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.hourly_api_request_limit) %></b> hourly api requests</li>
</ul>
<ul class="w-full py-4 dark:text-gray-100">
<span>The plan is priced at</span>
<template x-if="localizedPlan"><b x-text="price()"></b> </template>
<span>per <%= if @plan.billing_interval == :yearly, do: "year", else: "month" %>. On the next page, our payment provider will calculate the prorated amount that your card will be charged if you decide to upgrade now.</span>
</ul>
<div class="mt-6 text-left">
<span class="inline-flex w-full rounded-md shadow-sm">
<%= link(to: Routes.billing_path(@conn, :change_plan_preview, @plan.paddle_plan_id), class: "inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent leading-5 rounded-md hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150 ") do %>
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2"><path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path><path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path></svg>
Preview changes
<% end %>
</span>
</div>
</div>
</template>
</div>
</div>
<div class="mt-8 text-center dark:text-gray-100">
Questions? <%= link("Contact us", to: "https://plausible.io/contact", class: "text-indigo-500") %>
</div>
<%= render("_paddle_script.html") %>

View File

@ -66,12 +66,12 @@
<div class="flex items-center justify-between mt-10">
<span class="flex rounded-md shadow-sm">
<a href="/billing/change-plan" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 dark:active:text-gray-200 active:bg-gray-50 transition ease-in-out duration-150">
<a href="<%= Routes.billing_path(@conn, :change_plan_form) %>" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-500 text-sm leading-5 font-medium rounded-md text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 hover:text-gray-500 dark:hover:text-gray-200 focus:outline-none focus:border-blue-300 focus:ring active:text-gray-800 dark:active:text-gray-200 active:bg-gray-50 transition ease-in-out duration-150">
Back
</a>
</span>
<span class="flex space-betwee rounded-md shadow-sm">
<%= button("Confirm plan change", to: "/billing/change-plan/#{@preview_info["plan_id"]}", method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %>
<%= button("Confirm plan change", to: Routes.billing_path(@conn, :change_plan, @preview_info["plan_id"]), method: :post, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm leading-5 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150") %>
</span>
</div>
</div>

View File

@ -100,7 +100,7 @@
<div class="text-sm font-medium dark:text-gray-100">Due today: <template x-if="localizedPlans"><b x-text="selectedPlanPrice()"></b></template></div>
<div class="mb-4 text-xs font-medium dark:text-gray-100">+ VAT if applicable</div>
<span class="inline-flex rounded-md shadow-sm">
<button type="button" data-theme="none" :data-product="selectedPlanProductId()" data-email="<%= @conn.assigns[:current_user].email %>" data-disable-logout="true" data-passthrough="<%= @conn.assigns[:current_user].id %>" data-success="/billing/upgrade-success" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent paddle_button leading-5 rounded-md hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150">
<button type="button" data-theme="none" :data-product="selectedPlanProductId()" data-email="<%= @conn.assigns[:current_user].email %>" data-disable-logout="true" data-passthrough="<%= @conn.assigns[:current_user].id %>" data-success="<%= Routes.billing_path(@conn, :upgrade_success) %>" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent paddle_button leading-5 rounded-md hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150">
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
Pay securely via Paddle
</button>

View File

@ -0,0 +1,91 @@
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black text-black dark:text-gray-100">
<%= if @subscription_resumable,
do: "Change subscription plan",
else: "Upgrade to Enterprise" %>
</h1>
</div>
<div class="w-full max-w-lg px-4 mx-auto mt-4 text-gray-900 dark:text-gray-100">
<div class="flex-1 p-8 mt-8 rounded bg-white shadow-md dark:bg-gray-800 dark:shadow-none">
<div class="w-full pb-4">
<span>
<%= if @subscription_resumable,
do:
"We've prepared your account for an upgrade to custom limits outside the listed plans:",
else:
"We've prepared a custom enterprise plan for your account with the following limits:" %>
</span>
</div>
<PlausibleWeb.Components.Billing.present_enterprise_plan plan={@latest_enterprise_plan} />
<ul class="w-full py-4">
<span>
The plan is priced at
<b>
<%= case @price do
%Money{} = money ->
PlausibleWeb.Components.Billing.format_price(money, format: :standard)
nil ->
"N/A"
end %>
</b>
</span>
<span>
per <%= if @latest_enterprise_plan.billing_interval == :yearly,
do: "year",
else: "month" %> + VAT if applicable. <%= if @subscription_resumable,
do:
"On the next page, our payment provider will calculate the prorated amount that your card will be charged if you decide to upgrade now.",
else: "Click the button below to upgrade." %>
</span>
</ul>
<div class="w-max">
<%= if @subscription_resumable do %>
<span class="inline-flex w-full rounded-md shadow-sm">
<.link
id="preview-changes"
href={
Routes.billing_path(
@conn,
:change_plan_preview,
@latest_enterprise_plan.paddle_plan_id
)
}
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent leading-5 rounded-md hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:ring active:bg-indigo-700 transition ease-in-out duration-150"
>
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
<path
fill-rule="evenodd"
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
clip-rule="evenodd"
>
</path>
</svg>
Preview changes
</.link>
</span>
<% else %>
<PlausibleWeb.Components.Billing.paddle_button
id="paddle-button"
paddle_product_id={@latest_enterprise_plan.paddle_plan_id}
user={@user}
>
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2">
<path
fill-rule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clip-rule="evenodd"
>
</path>
</svg>
Pay securely via Paddle
</PlausibleWeb.Components.Billing.paddle_button>
<% end %>
</div>
</div>
</div>
<div class="mt-8 text-center text-gray-900 dark:text-gray-100">
Questions? <a class="text-indigo-600" href={@contact_link}>Contact us</a>
</div>
<PlausibleWeb.Components.Billing.paddle_script />

View File

@ -1,67 +0,0 @@
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fetch-jsonp/1.1.3/fetch-jsonp.min.js"></script>
<script>
plan = function() {
return {
localizedPlan: null,
price() {
var currency = {
'USD': '$',
'EUR': '',
'GBP': '£'
}[this.localizedPlan.currency]
return currency + this.localizedPlan.price.net
},
fetchPlan() {
fetchJsonp('<%= Plausible.Billing.PaddleApi.checkout_domain() %>/api/2.0/prices?product_ids=<%= @plan.paddle_plan_id %>')
.then(res => res.json())
.then((data) => {
this.localizedPlan = data.response.products[0]
})
}
}
}
</script>
<div class="mx-auto mt-6 text-center">
<h1 class="text-3xl font-black dark:text-gray-100">Upgrade your free trial</h1>
</div>
<div class="w-full max-w-lg px-4 mx-auto mt-4">
<div x-init="fetchPlan()" x-data="window.plan()" class="flex-1 p-8 mt-8 bg-white rounded shadow-md dark:bg-gray-800">
<div x-show="!localizedPlan" class="mx-auto my-32 loading sm"><div></div></div>
<template x-if="localizedPlan">
<div>
<div class="w-full pb-4 dark:text-gray-100">
<span>We've prepared a custom enterprise plan for your account with the following limits:</span>
</div>
<ul class="w-full py-4 dark:text-gray-100">
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.monthly_pageview_limit) %></b> monthly pageviews</li>
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.site_limit) %></b> sites</li>
<li>Up to <b><%= PlausibleWeb.StatsView.large_number_format(@plan.hourly_api_request_limit) %></b> hourly api requests</li>
</ul>
<ul class="w-full py-4 dark:text-gray-100">
<span>The plan is priced at</span>
<template x-if="localizedPlan"><b x-text="price()"></b> </template>
<span>per <%= if @plan.billing_interval == :yearly, do: "year", else: "month" %> + VAT if applicable. Click the button below to upgrade.
</ul>
</div>
</template>
<div class="mt-6">
<button type="button" data-theme="none" data-product="<%= @plan.paddle_plan_id %>" data-email="<%= @conn.assigns[:current_user].email %>" data-disable-logout="true" data-passthrough="<%= @conn.assigns[:current_user].id %>" data-success="/billing/upgrade-success" class="paddle_button items-center button">
<svg fill="currentColor" viewBox="0 0 20 20" class="inline w-4 h-4 mr-2"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>
Pay securely via Paddle
</button>
</div>
</div>
</div>
<div class="mt-8 text-center dark:text-gray-100">
Questions? <%= link("Contact us", to: "https://plausible.io/contact", class: "text-indigo-500") %>
</div>
<%= render("_paddle_script.html") %>

View File

@ -23,7 +23,7 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
<%= if Plausible.Auth.enterprise?(@conn.assigns[:current_user]) do %>
<%= if Plausible.Auth.enterprise_configured?(@conn.assigns[:current_user]) do %>
You have outgrown your Plausible subscription tier
<% else %>
Please upgrade your account
@ -31,7 +31,7 @@
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
<%= if Plausible.Auth.enterprise?(@conn.assigns[:current_user]) do %>
<%= if Plausible.Auth.enterprise_configured?(@conn.assigns[:current_user]) do %>
In order to keep your stats running, we require you to upgrade
your account to accommodate your new usage levels. Please contact
us to discuss a new custom enterprise plan. <%= link("Contact us →",

View File

@ -1,6 +1,7 @@
defmodule PlausibleWeb.AuthView do
use PlausibleWeb, :view
alias Plausible.Billing.Plans
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.{Plans, Subscription}
def base_domain do
PlausibleWeb.Endpoint.host()
@ -56,15 +57,17 @@ defmodule PlausibleWeb.AuthView do
:lists.reverse(list) ++ acc
end
def present_subscription_status("active"), do: "Active"
def present_subscription_status("past_due"), do: "Past due"
def present_subscription_status("deleted"), do: "Cancelled"
def present_subscription_status("paused"), do: "Paused"
@spec present_subscription_status(Subscription.Status.status()) :: String.t()
def present_subscription_status(Subscription.Status.active()), do: "Active"
def present_subscription_status(Subscription.Status.past_due()), do: "Past due"
def present_subscription_status(Subscription.Status.deleted()), do: "Cancelled"
def present_subscription_status(Subscription.Status.paused()), do: "Paused"
def present_subscription_status(status), do: status
def subscription_colors("active"), do: "bg-green-100 text-green-800"
def subscription_colors("past_due"), do: "bg-yellow-100 text-yellow-800"
def subscription_colors("paused"), do: "bg-red-100 text-red-800"
def subscription_colors("deleted"), do: "bg-red-100 text-red-800"
@spec subscription_colors(Subscription.Status.status()) :: String.t()
def subscription_colors(Subscription.Status.active()), do: "bg-green-100 text-green-800"
def subscription_colors(Subscription.Status.past_due()), do: "bg-yellow-100 text-yellow-800"
def subscription_colors(Subscription.Status.paused()), do: "bg-red-100 text-red-800"
def subscription_colors(Subscription.Status.deleted()), do: "bg-red-100 text-red-800"
def subscription_colors(_), do: ""
end

View File

@ -1,6 +1,8 @@
defmodule Plausible.Workers.CheckUsage do
use Plausible.Repo
use Oban.Worker, queue: :check_usage
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
defmacro yesterday() do
quote do
@ -41,7 +43,7 @@ defmodule Plausible.Workers.CheckUsage do
left_join: ep in Plausible.Billing.EnterprisePlan,
on: ep.user_id == u.id,
where: is_nil(u.grace_period),
where: s.status == "active",
where: s.status == ^Subscription.Status.active(),
where: not is_nil(s.last_bill_date),
# Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01.
where:

View File

@ -1,6 +1,9 @@
defmodule Plausible.Workers.NotifyAnnualRenewal do
use Plausible.Repo
use Oban.Worker, queue: :notify_annual_renewal
require Plausible.Billing.Subscription.Status
alias Money.Subscription
alias Plausible.Billing.Subscription
@yearly_plans Plausible.Billing.Plans.yearly_product_ids()
@ -44,11 +47,11 @@ defmodule Plausible.Workers.NotifyAnnualRenewal do
for user <- users do
case user.subscription.status do
"active" ->
Subscription.Status.active() ->
template = PlausibleWeb.Email.yearly_renewal_notification(user)
Plausible.Mailer.send(template)
"deleted" ->
Subscription.Status.deleted() ->
template = PlausibleWeb.Email.yearly_expiration_notification(user)
Plausible.Mailer.send(template)

View File

@ -41,12 +41,12 @@ defmodule Plausible.AuthTest do
end
end
test "enterprise?/1 returns whether the user has an enterprise plan" do
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))
assert Auth.enterprise?(user_with_plan)
refute Auth.enterprise?(user_without_plan)
refute Auth.enterprise?(nil)
assert Auth.enterprise_configured?(user_with_plan)
refute Auth.enterprise_configured?(user_without_plan)
refute Auth.enterprise_configured?(nil)
end
end

View File

@ -1,7 +1,9 @@
defmodule Plausible.BillingTest do
use Plausible.DataCase
use Bamboo.Test, shared: true
require Plausible.Billing.Subscription.Status
alias Plausible.Billing
alias Plausible.Billing.Subscription
describe "last_two_billing_cycles" do
test "billing on the 1st" do
@ -152,7 +154,12 @@ defmodule Plausible.BillingTest do
test "is false for a user with a cancelled subscription IF the billing cycle isn't completed yet" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
insert(:subscription, user: user, status: "deleted", next_bill_date: Timex.today())
insert(:subscription,
user: user,
status: Subscription.Status.deleted(),
next_bill_date: Timex.today()
)
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
end
@ -162,7 +169,7 @@ defmodule Plausible.BillingTest do
insert(:subscription,
user: user,
status: "deleted",
status: Subscription.Status.deleted(),
next_bill_date: Timex.shift(Timex.today(), days: -1)
)
@ -171,7 +178,8 @@ defmodule Plausible.BillingTest do
test "is true for a deleted subscription if no next_bill_date specified" do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
insert(:subscription, user: user, status: "deleted", next_bill_date: nil)
insert(:subscription, user: user, status: Subscription.Status.deleted(), next_bill_date: nil)
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
end
@ -311,7 +319,7 @@ defmodule Plausible.BillingTest do
test "unlocks sites if subscription is changed from past_due to active" do
user = insert(:user)
subscription = insert(:subscription, user: user, status: "past_due")
subscription = insert(:subscription, user: user, status: Subscription.Status.past_due())
site = insert(:site, locked: true, members: [user])
Billing.subscription_updated(%{
@ -445,7 +453,7 @@ defmodule Plausible.BillingTest do
describe "subscription_cancelled" do
test "sets the status to deleted" do
user = insert(:user)
subscription = insert(:subscription, status: "active", user: user)
subscription = insert(:subscription, status: Subscription.Status.active(), user: user)
Billing.subscription_cancelled(%{
"alert_name" => "subscription_cancelled",
@ -454,7 +462,7 @@ defmodule Plausible.BillingTest do
})
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert subscription.status == "deleted"
assert subscription.status == Subscription.Status.deleted()
end
test "ignores if the subscription cannot be found" do
@ -470,7 +478,7 @@ defmodule Plausible.BillingTest do
test "sends an email to confirm cancellation" do
user = insert(:user)
subscription = insert(:subscription, status: "active", user: user)
subscription = insert(:subscription, status: Subscription.Status.active(), user: user)
Billing.subscription_cancelled(%{
"alert_name" => "subscription_cancelled",
@ -528,8 +536,8 @@ defmodule Plausible.BillingTest do
end
test "active_subscription_for/1 returns active subscription" do
active = insert(:subscription, user: insert(:user), status: "active")
paused = insert(:subscription, user: insert(:user), status: "paused")
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
user_without_subscription = insert(:user)
assert Billing.active_subscription_for(active.user_id).id == active.id
@ -538,8 +546,8 @@ defmodule Plausible.BillingTest do
end
test "has_active_subscription?/1 returns whether the user has an active subscription" do
active = insert(:subscription, user: insert(:user), status: "active")
paused = insert(:subscription, user: insert(:user), status: "paused")
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
user_without_subscription = insert(:user)
assert Billing.has_active_subscription?(active.user_id)

View File

@ -61,6 +61,28 @@ defmodule Plausible.Billing.PlansTest do
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v4_business_plan_id
end)
end
test "latest_enterprise_plan_with_price/1" do
user = insert(:user)
insert(:enterprise_plan, user: user, paddle_plan_id: "123", inserted_at: Timex.now())
insert(:enterprise_plan,
user: user,
paddle_plan_id: "456",
inserted_at: Timex.shift(Timex.now(), hours: -10)
)
insert(:enterprise_plan,
user: user,
paddle_plan_id: "789",
inserted_at: Timex.shift(Timex.now(), minutes: -2)
)
{enterprise_plan, price} = Plans.latest_enterprise_plan_with_price(user)
assert enterprise_plan.paddle_plan_id == "123"
assert price == Money.new(:EUR, "10.0")
end
end
describe "subscription_interval" do

View File

@ -1,7 +1,8 @@
defmodule Plausible.Billing.SiteLockerTest do
use Plausible.DataCase
use Bamboo.Test, shared: true
alias Plausible.Billing.SiteLocker
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.{SiteLocker, Subscription}
describe "update_sites_for/1" do
test "does not lock sites if user is on trial" do
@ -22,7 +23,7 @@ defmodule Plausible.Billing.SiteLockerTest do
test "does not lock if user has an active subscription" do
user = insert(:user)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
site =
insert(:site,
@ -39,7 +40,7 @@ defmodule Plausible.Billing.SiteLockerTest do
test "does not lock user who is past due" do
user = insert(:user)
insert(:subscription, status: "past_due", user: user)
insert(:subscription, status: Subscription.Status.past_due(), user: user)
site =
insert(:site,
@ -55,7 +56,7 @@ defmodule Plausible.Billing.SiteLockerTest do
test "does not lock user who cancelled subscription but it hasn't expired yet" do
user = insert(:user)
insert(:subscription, status: "deleted", user: user)
insert(:subscription, status: Subscription.Status.deleted(), user: user)
site =
insert(:site,
@ -78,7 +79,7 @@ defmodule Plausible.Billing.SiteLockerTest do
}
)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
site =
insert(:site,
@ -96,7 +97,7 @@ defmodule Plausible.Billing.SiteLockerTest do
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
insert(:subscription,
status: "deleted",
status: Subscription.Status.deleted(),
next_bill_date: Timex.today() |> Timex.shift(days: -1),
user: user
)
@ -122,7 +123,7 @@ defmodule Plausible.Billing.SiteLockerTest do
}
)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
site =
insert(:site,
@ -145,7 +146,7 @@ defmodule Plausible.Billing.SiteLockerTest do
}
)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [
@ -171,7 +172,7 @@ defmodule Plausible.Billing.SiteLockerTest do
}
)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
insert(:site,
memberships: [

View File

@ -3,12 +3,19 @@ defmodule PlausibleWeb.AuthControllerTest do
use Bamboo.Test
use Plausible.Repo
import Plausible.Test.Support.HTML
import Mox
require Plausible.Billing.Subscription.Status
alias Plausible.Auth.User
alias Plausible.Billing.Subscription
setup :verify_on_exit!
@v3_plan_id "749355"
@configured_enterprise_plan_paddle_plan_id "123"
describe "GET /register" do
test "shows the register form", %{conn: conn} do
conn = get(conn, "/register")
@ -460,15 +467,10 @@ defmodule PlausibleWeb.AuthControllerTest do
test "shows enterprise plan subscription", %{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: "123", user: user)
insert(:enterprise_plan,
paddle_plan_id: "123",
user: user,
monthly_pageview_limit: 10_000_000,
billing_interval: :yearly
)
configure_enterprise_plan(user)
conn = get(conn, "/settings")
assert html_response(conn, 200) =~ "10M pageviews"
assert html_response(conn, 200) =~ "20M pageviews"
assert html_response(conn, 200) =~ "yearly billing"
end
@ -476,27 +478,139 @@ defmodule PlausibleWeb.AuthControllerTest do
conn: conn,
user: user
} do
insert(:subscription, paddle_plan_id: "123", user: user)
insert(:subscription, paddle_plan_id: @configured_enterprise_plan_paddle_plan_id, user: user)
insert(:enterprise_plan,
paddle_plan_id: "123",
paddle_plan_id: "1234",
user: user,
monthly_pageview_limit: 10_000_000,
billing_interval: :yearly
)
insert(:enterprise_plan,
paddle_plan_id: "1234",
user: user,
monthly_pageview_limit: 20_000_000,
billing_interval: :yearly
)
configure_enterprise_plan(user)
conn = get(conn, "/settings")
assert html_response(conn, 200) =~ "10M pageviews"
assert html_response(conn, 200) =~ "20M pageviews"
assert html_response(conn, 200) =~ "yearly billing"
end
test "links to upgrade to a plan", %{conn: conn} do
doc =
get(conn, "/settings")
|> html_response(200)
upgrade_link_1 = find(doc, "#monthly-quota-box a")
upgrade_link_2 = find(doc, "#upgrade-link-2")
assert text(upgrade_link_1) == "Upgrade"
assert text_of_attr(upgrade_link_1, "href") == Routes.billing_path(conn, :choose_plan)
assert text(upgrade_link_2) == "Upgrade"
assert text_of_attr(upgrade_link_2, "href") == Routes.billing_path(conn, :choose_plan)
end
test "links to change existing plan", %{
conn: conn,
user: user
} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
doc =
get(conn, "/settings")
|> html_response(200)
refute element_exists?(doc, "#upgrade-link-2")
assert doc =~ "Cancel my subscription"
change_plan_link = find(doc, "#monthly-quota-box a")
assert text(change_plan_link) == "Change plan"
assert text_of_attr(change_plan_link, "href") == Routes.billing_path(conn, :choose_plan)
end
test "upgrade_to_enterprise_plan link does not show up when subscription is past_due", %{
conn: conn,
user: user
} do
configure_enterprise_plan(user)
insert(:subscription,
user: user,
status: Subscription.Status.past_due(),
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id
)
doc =
conn
|> get(Routes.auth_path(conn, :user_settings))
|> html_response(200)
refute element_exists?(doc, "#upgrade-or-change-plan-link")
end
test "upgrade_to_enterprise_plan link does not show up when subscription is paused", %{
conn: conn,
user: user
} do
configure_enterprise_plan(user)
insert(:subscription,
user: user,
status: Subscription.Status.paused(),
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id
)
doc =
conn
|> get(Routes.auth_path(conn, :user_settings))
|> html_response(200)
refute element_exists?(doc, "#upgrade-or-change-plan-link")
end
test "links to upgrade to enterprise plan",
%{conn: conn, user: user} do
configure_enterprise_plan(user)
doc =
get(conn, "/settings")
|> html_response(200)
upgrade_link_1 = find(doc, "#monthly-quota-box a")
upgrade_link_2 = find(doc, "#upgrade-link-2")
assert text(upgrade_link_1) == "Upgrade"
assert text_of_attr(upgrade_link_1, "href") ==
Routes.billing_path(conn, :upgrade_to_enterprise_plan)
assert text(upgrade_link_2) == "Upgrade"
assert text_of_attr(upgrade_link_2, "href") ==
Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
test "links to change enterprise plan and cancel subscription",
%{conn: conn, user: user} do
insert(:subscription, paddle_plan_id: @v3_plan_id, user: user)
configure_enterprise_plan(user)
doc =
get(conn, "/settings")
|> html_response(200)
refute element_exists?(doc, "#upgrade-link-2")
assert doc =~ "Cancel my subscription"
change_plan_link = find(doc, "#monthly-quota-box a")
assert text(change_plan_link) == "Change plan"
assert text_of_attr(change_plan_link, "href") ==
Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
test "shows invoices for subscribed user", %{conn: conn, user: user} do
insert(:subscription,
paddle_plan_id: "558018",
@ -613,8 +727,8 @@ defmodule PlausibleWeb.AuthControllerTest do
])
insert(:google_auth, site: site, user: user)
insert(:subscription, user: user, status: "deleted")
insert(:subscription, user: user, status: "active")
insert(:subscription, user: user, status: Subscription.Status.deleted())
insert(:subscription, user: user, status: Subscription.Status.active())
conn = delete(conn, "/me")
assert redirected_to(conn) == "/"
@ -760,4 +874,13 @@ defmodule PlausibleWeb.AuthControllerTest do
end
)
end
defp configure_enterprise_plan(user) do
insert(:enterprise_plan,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
user: user,
monthly_pageview_limit: 20_000_000,
billing_interval: :yearly
)
end
end

View File

@ -1,53 +1,41 @@
defmodule PlausibleWeb.BillingControllerTest do
use PlausibleWeb.ConnCase, async: true
import Plausible.Test.Support.HTML
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
describe "GET /upgrade" do
setup [:create_user, :log_in]
test "shows upgrade page when user does not have a subcription already", %{conn: conn} do
conn = get(conn, "/billing/upgrade")
conn = get(conn, Routes.billing_path(conn, :upgrade))
assert html_response(conn, 200) =~ "Upgrade your free trial"
end
test "redirects user to change plan if they already have a plan", %{conn: conn, user: user} do
insert(:subscription, user: user)
conn = get(conn, "/billing/upgrade")
conn = get(conn, Routes.billing_path(conn, :upgrade))
assert redirected_to(conn) == "/billing/change-plan"
assert redirected_to(conn) == Routes.billing_path(conn, :change_plan_form)
end
test "redirects user to enteprise plan page if they are configured with one", %{
conn: conn,
user: user
} do
plan = insert(:enterprise_plan, user: user)
conn = get(conn, "/billing/upgrade")
assert redirected_to(conn) == "/billing/upgrade/enterprise/#{plan.id}"
insert(:enterprise_plan, user: user)
conn = get(conn, Routes.billing_path(conn, :upgrade))
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
end
describe "GET /upgrade/enterprise/:plan_id" do
describe "GET /upgrade/enterprise/:plan_id (deprecated)" do
setup [:create_user, :log_in]
test "renders enteprise plan upgrade page", %{conn: conn, user: user} do
plan = insert(:enterprise_plan, user: user)
conn = get(conn, "/billing/upgrade/enterprise/#{plan.id}")
assert html_response(conn, 200) =~ "Upgrade your free trial"
assert html_response(conn, 200) =~ "enterprise plan"
end
test "redirects to change-plan page if user is already subscribed to the given enterprise plan",
%{conn: conn, user: user} do
plan = insert(:enterprise_plan, user: user)
insert(:subscription, paddle_plan_id: plan.paddle_plan_id, user: user)
conn = get(conn, "/billing/upgrade/enterprise/#{plan.id}")
assert redirected_to(conn) == "/billing/change-plan"
test "redirects to the new :upgrade_to_enterprise_plan action", %{conn: conn} do
conn = get(conn, Routes.billing_path(conn, :upgrade_enterprise_plan, "123"))
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
end
@ -56,68 +44,31 @@ defmodule PlausibleWeb.BillingControllerTest do
test "shows change plan page if user has subsription", %{conn: conn, user: user} do
insert(:subscription, user: user)
conn = get(conn, "/billing/change-plan")
conn = get(conn, Routes.billing_path(conn, :change_plan_form))
assert html_response(conn, 200) =~ "Change subscription plan"
end
test "redirects to /upgrade if user does not have a subscription", %{conn: conn} do
conn = get(conn, "/billing/change-plan")
conn = get(conn, Routes.billing_path(conn, :change_plan_form))
assert redirected_to(conn) == "/billing/upgrade"
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade)
end
test "redirects to enterprise upgrade page if user is due for an enteprise plan upgrade",
test "redirects to enterprise upgrade page if user has an enterprise plan configured",
%{conn: conn, user: user} do
insert(:subscription, user: user, paddle_plan_id: "standard-plan-id")
enterprise_plan = insert(:enterprise_plan, user: user, paddle_plan_id: "new-custom-id")
conn = get(conn, "/billing/change-plan")
assert redirected_to(conn) == "/billing/change-plan/enterprise/#{enterprise_plan.id}"
end
test "prompts to contact us if user has enterprise plan and existing subscription",
%{conn: conn, user: user} do
insert(:subscription, user: user, paddle_plan_id: "enterprise-plan-id")
insert(:enterprise_plan, user: user, paddle_plan_id: "enterprise-plan-id")
conn = get(conn, "/billing/change-plan")
assert html_response(conn, 200) =~ "please contact us"
insert(:enterprise_plan, user: user, paddle_plan_id: "123")
conn = get(conn, Routes.billing_path(conn, :change_plan_form))
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
end
describe "GET /change-plan/enterprise/:plan_id" do
describe "GET /change-plan/enterprise/:plan_id (deprecated)" do
setup [:create_user, :log_in]
test "shows change plan page if user has subsription and enterprise plan", %{
conn: conn,
user: user
} do
insert(:subscription, user: user)
plan =
insert(:enterprise_plan,
user: user,
monthly_pageview_limit: 1000,
hourly_api_request_limit: 500,
site_limit: 100
)
conn = get(conn, "/billing/change-plan/enterprise/#{plan.id}")
assert html_response(conn, 200) =~ "Change subscription plan"
assert html_response(conn, 200) =~ "Up to <b>1k</b> monthly pageviews"
assert html_response(conn, 200) =~ "Up to <b>500</b> hourly api requests"
assert html_response(conn, 200) =~ "Up to <b>100</b> sites"
end
test "renders 404 is user does not have enterprise plan", %{conn: conn, user: user} do
insert(:subscription, user: user)
conn = get(conn, "/billing/change-plan/enterprise/123")
assert conn.status == 404
test "redirects to the new :upgrade_to_enterprise_plan action", %{conn: conn} do
conn = get(conn, Routes.billing_path(conn, :change_enterprise_plan, "123"))
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
end
end
@ -127,7 +78,7 @@ defmodule PlausibleWeb.BillingControllerTest do
test "calls Paddle API to update subscription", %{conn: conn, user: user} do
insert(:subscription, user: user)
post(conn, "/billing/change-plan/123123")
post(conn, Routes.billing_path(conn, :change_plan, "123123"))
subscription = Plausible.Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
assert subscription.paddle_plan_id == "123123"
@ -140,9 +91,238 @@ defmodule PlausibleWeb.BillingControllerTest do
setup [:create_user, :log_in]
test "shows success page after user subscribes", %{conn: conn} do
conn = get(conn, "/billing/upgrade-success")
conn = get(conn, Routes.billing_path(conn, :upgrade_success))
assert html_response(conn, 200) =~ "Your account is being upgraded"
end
end
@configured_enterprise_plan_paddle_plan_id "123"
describe "GET /upgrade-to-enterprise-plan (no existing subscription)" do
setup [:create_user, :log_in, :configure_enterprise_plan]
test "displays basic page content", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ "Upgrade to Enterprise"
assert doc =~ "prepared a custom enterprise plan for your account with the following limits"
assert doc =~ "Questions?"
assert doc =~ "Contact us"
assert doc =~ "+ VAT if applicable"
assert doc =~ "Click the button below to upgrade"
assert doc =~ "Pay securely via Paddle"
end
test "displays info about the enterprise plan to upgrade to", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ "per year"
end
test "data-product attribute on the checkout link is the paddle_plan_id of the enterprise plan",
%{conn: conn, user: user} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert %{
"disableLogout" => true,
"email" => user.email,
"passthrough" => user.id,
"product" => @configured_enterprise_plan_paddle_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, "#paddle-button"))
end
end
describe "GET /upgrade-to-enterprise-plan (active subscription, new enterprise plan configured)" do
setup [:create_user, :log_in, :subscribe_enterprise, :configure_enterprise_plan]
test "displays basic page content", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ "Change subscription plan"
assert doc =~ "prepared your account for an upgrade to custom limits"
assert doc =~ "+ VAT if applicable"
assert doc =~ "calculate the prorated amount that your card will be charged"
assert doc =~ "Preview changes"
assert doc =~ "Questions?"
assert doc =~ "Contact us"
end
test "displays info about the enterprise plan to upgrade to", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ "per year"
end
test "preview changes links to :change_plan_preview action", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
preview_changes_link = find(doc, "#preview-changes")
assert text(preview_changes_link) == "Preview changes"
assert text_of_attr(preview_changes_link, "href") ==
Routes.billing_path(
PlausibleWeb.Endpoint,
:change_plan_preview,
@configured_enterprise_plan_paddle_plan_id
)
end
end
@enterprise_contact_link "enterprise@plausible.io"
describe "GET /upgrade-to-enterprise-plan (already subscribed to latest enterprise plan)" do
setup [:create_user, :log_in, :configure_enterprise_plan]
setup context do
subscribe_enterprise(context, paddle_plan_id: @configured_enterprise_plan_paddle_plan_id)
end
test "renders contact note", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ "Need to change your limits?"
assert doc =~ "Your account is on an enterprise plan"
assert doc =~ "contact us at #{@enterprise_contact_link}"
end
end
describe "GET /upgrade-to-enterprise-plan (subscription past_due or paused)" do
setup [:create_user, :log_in, :configure_enterprise_plan]
test "redirects to /settings when past_due", %{conn: conn} = context do
subscribe_enterprise(context, status: Subscription.Status.past_due())
conn = get(conn, Routes.billing_path(conn, :upgrade_to_enterprise_plan))
assert redirected_to(conn) == "/settings"
end
test "redirects to /settings when paused", %{conn: conn} = context do
subscribe_enterprise(context, status: Subscription.Status.paused())
conn = get(conn, Routes.billing_path(conn, :upgrade_to_enterprise_plan))
assert redirected_to(conn) == "/settings"
end
end
describe "GET /upgrade-to-enterprise-plan (deleted enterprise subscription)" do
setup [:create_user, :log_in, :configure_enterprise_plan]
setup context do
subscribe_enterprise(context,
paddle_plan_id: @configured_enterprise_plan_paddle_plan_id,
status: Subscription.Status.deleted()
)
context
end
test "displays the same content as for a user without a subscription", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ "Upgrade to Enterprise"
assert doc =~ "prepared a custom enterprise plan for your account with the following limits"
assert doc =~ "Questions?"
assert doc =~ "Contact us"
assert doc =~ "+ VAT if applicable"
assert doc =~ "Click the button below to upgrade"
assert doc =~ "Pay securely via Paddle"
end
test "still allows to subscribe back to the same plan", %{conn: conn} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert doc =~ ~r/Up to\s*<b>\s*50M\s*<\/b>\s*monthly pageviews/
assert doc =~ ~r/Up to\s*<b>\s*20k\s*<\/b>\s*sites/
assert doc =~ ~r/Up to\s*<b>\s*5k\s*<\/b>\s*hourly api requests/
assert doc =~ ~r/The plan is priced at\s*<b>\s*€10\s*<\/b>\s*/
assert doc =~ "per year"
end
test "renders paddle button with the correct checkout params",
%{conn: conn, user: user} do
doc =
conn
|> get(Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|> html_response(200)
assert %{
"disableLogout" => true,
"email" => user.email,
"passthrough" => user.id,
"product" => @configured_enterprise_plan_paddle_plan_id,
"success" => Routes.billing_path(PlausibleWeb.Endpoint, :upgrade_success),
"theme" => "none"
} == get_paddle_checkout_params(find(doc, "#paddle-button"))
end
end
defp configure_enterprise_plan(%{user: user}) do
insert(:enterprise_plan,
user_id: user.id,
paddle_plan_id: "123",
billing_interval: :yearly,
monthly_pageview_limit: 50_000_000,
site_limit: 20_000,
hourly_api_request_limit: 5000,
inserted_at: Timex.now() |> Timex.shift(hours: 1)
)
:ok
end
defp subscribe_enterprise(%{user: user}, opts \\ []) do
opts =
opts
|> Keyword.put(:user, user)
|> Keyword.put_new(:paddle_plan_id, "321")
|> Keyword.put_new(:status, Subscription.Status.active())
insert(:subscription, opts)
{:ok, user: Plausible.Users.with_subscription(user)}
end
defp get_paddle_checkout_params(element) do
with onclick <- text_of_attr(element, "onclick"),
[[_, checkout_params_str]] <- Regex.scan(~r/Paddle\.Checkout\.open\((.*?)\)/, onclick),
{:ok, checkout_params} <- Jason.decode(checkout_params_str) do
checkout_params
end
end
end

View File

@ -6,6 +6,7 @@ defmodule PlausibleWeb.SiteControllerTest do
import ExUnit.CaptureLog
import Mox
setup :verify_on_exit!
describe "GET /sites/new" do

View File

@ -1,8 +1,9 @@
defmodule PlausibleWeb.Live.ChoosePlanTest do
alias Plausible.{Repo, Billing.Subscription}
use PlausibleWeb.ConnCase, async: true
import Phoenix.LiveViewTest
import Plausible.Test.Support.HTML
require Plausible.Billing.Subscription.Status
alias Plausible.{Repo, Billing.Subscription}
@v1_10k_yearly_plan_id "572810"
@v4_growth_200k_yearly_plan_id "change-me-749347"
@ -259,7 +260,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
growth_checkout_button = find(doc, @growth_checkout_button)
assert text_of_attr(growth_checkout_button, "href") =~
"/billing/change-plan/preview/#{@v4_growth_200k_yearly_plan_id}"
Routes.billing_path(conn, :change_plan_preview, @v4_growth_200k_yearly_plan_id)
element(lv, @slider_input) |> render_change(%{slider: 6})
doc = element(lv, @monthly_interval_button) |> render_click()
@ -267,7 +268,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
business_checkout_button = find(doc, @business_checkout_button)
assert text_of_attr(business_checkout_button, "href") =~
"/billing/change-plan/preview/#{@v4_business_5m_monthly_plan_id}"
Routes.billing_path(conn, :change_plan_preview, @v4_business_5m_monthly_plan_id)
end
end
@ -469,7 +470,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
defp create_past_due_subscription(%{user: user}) do
create_subscription_for(user,
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
status: "past_due",
status: Subscription.Status.past_due(),
update_url: "https://update.billing.details"
)
end
@ -477,7 +478,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
defp create_paused_subscription(%{user: user}) do
create_subscription_for(user,
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
status: "paused",
status: Subscription.Status.paused(),
update_url: "https://update.billing.details"
)
end
@ -485,7 +486,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
defp create_cancelled_subscription(%{user: user}) do
create_subscription_for(user,
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
status: "deleted"
status: Subscription.Status.deleted()
)
end
@ -503,7 +504,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
defp get_liveview(conn) do
conn = assign(conn, :live_module, PlausibleWeb.Live.ChoosePlan)
{:ok, _lv, _doc} = live(conn, "/billing/choose-plan")
{:ok, _lv, _doc} = live(conn, Routes.billing_path(conn, :choose_plan))
end
defp get_paddle_checkout_params(element) do

View File

@ -1,5 +1,7 @@
defmodule Plausible.Factory do
use ExMachina.Ecto, repo: Plausible.Repo
require Plausible.Billing.Subscription.Status
alias Plausible.Billing.Subscription
def user_factory(attrs) do
pw = Map.get(attrs, :password, "password")
@ -88,7 +90,7 @@ defmodule Plausible.Factory do
paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"),
cancel_url: "cancel.com",
update_url: "cancel.com",
status: "active",
status: Subscription.Status.active(),
next_bill_amount: "6.00",
next_bill_date: Timex.today(),
last_bill_date: Timex.today(),

View File

@ -1,6 +1,8 @@
defmodule Plausible.Workers.LockSitesTest do
use Plausible.DataCase, async: true
require Plausible.Billing.Subscription.Status
alias Plausible.Workers.LockSites
alias Plausible.Billing.Subscription
test "does not lock enterprise site on grace period" do
user =
@ -36,7 +38,7 @@ defmodule Plausible.Workers.LockSitesTest do
test "does not lock active subsriber's sites" do
user = insert(:user)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
site = insert(:site, members: [user])
LockSites.perform(nil)
@ -46,7 +48,7 @@ defmodule Plausible.Workers.LockSitesTest do
test "does not lock user who is past due" do
user = insert(:user)
insert(:subscription, status: "past_due", user: user)
insert(:subscription, status: Subscription.Status.past_due(), user: user)
site = insert(:site, members: [user])
LockSites.perform(nil)
@ -56,7 +58,7 @@ defmodule Plausible.Workers.LockSitesTest do
test "does not lock user who cancelled subscription but it hasn't expired yet" do
user = insert(:user)
insert(:subscription, status: "deleted", user: user)
insert(:subscription, status: Subscription.Status.deleted(), user: user)
site = insert(:site, members: [user])
LockSites.perform(nil)
@ -68,7 +70,7 @@ defmodule Plausible.Workers.LockSitesTest do
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
insert(:subscription,
status: "deleted",
status: Subscription.Status.deleted(),
next_bill_date: Timex.today() |> Timex.shift(days: -1),
user: user
)
@ -84,13 +86,13 @@ defmodule Plausible.Workers.LockSitesTest do
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
insert(:subscription,
status: "deleted",
status: Subscription.Status.deleted(),
next_bill_date: Timex.today() |> Timex.shift(days: -1),
user: user,
inserted_at: Timex.now() |> Timex.shift(days: -1)
)
insert(:subscription, status: "active", user: user)
insert(:subscription, status: Subscription.Status.active(), user: user)
site = insert(:site, members: [user])

View File

@ -1,8 +1,9 @@
defmodule Plausible.Workers.NotifyAnnualRenewalTest do
use Plausible.DataCase, async: true
use Bamboo.Test
require Plausible.Billing.Subscription.Status
alias Plausible.Workers.NotifyAnnualRenewal
alias Plausible.Billing.Subscription
setup [:create_user, :create_site]
@monthly_plan "558018"
@ -185,7 +186,7 @@ defmodule Plausible.Workers.NotifyAnnualRenewalTest do
user: user,
paddle_plan_id: @yearly_plan,
next_bill_date: Timex.shift(Timex.today(), days: 7),
status: "deleted"
status: Subscription.Status.deleted()
)
NotifyAnnualRenewal.perform(nil)

View File

@ -2,7 +2,6 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
use Plausible.DataCase
use Bamboo.Test
use Oban.Testing, repo: Plausible.Repo
alias Plausible.Workers.SendTrialNotifications
test "does not send a notification if user didn't create a site" do