mirror of
https://github.com/plausible/analytics.git
synced 2024-11-26 11:44:03 +03:00
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:
parent
dec193e904
commit
3d2f356ba7
@ -8,16 +8,17 @@ defmodule Mix.Tasks.CancelSubscription do
|
|||||||
|
|
||||||
use Mix.Task
|
use Mix.Task
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
alias Plausible.{Repo, Billing.Subscription}
|
require Plausible.Billing.Subscription.Status
|
||||||
require Logger
|
require Logger
|
||||||
|
alias Plausible.{Repo, Billing.Subscription}
|
||||||
|
|
||||||
def run([paddle_subscription_id]) do
|
def run([paddle_subscription_id]) do
|
||||||
Mix.Task.run("app.start")
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
Repo.get_by!(Subscription, paddle_subscription_id: paddle_subscription_id)
|
Repo.get_by!(Subscription, paddle_subscription_id: paddle_subscription_id)
|
||||||
|> Subscription.changeset(%{status: "deleted"})
|
|> Subscription.changeset(%{status: Subscription.Status.deleted()})
|
||||||
|> Repo.update!()
|
|> Repo.update!()
|
||||||
|
|
||||||
Logger.info("Successfully set the subscription status to 'deleted'.")
|
Logger.info("Successfully set the subscription status to #{Subscription.Status.deleted()}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -118,10 +118,11 @@ defmodule Plausible.Auth do
|
|||||||
user_id in Application.get_env(:plausible, :super_admin_user_ids)
|
user_id in Application.get_env(:plausible, :super_admin_user_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enterprise?(nil), do: false
|
def enterprise_configured?(nil), do: false
|
||||||
|
|
||||||
def enterprise?(%Plausible.Auth.User{} = user) do
|
def enterprise_configured?(%Plausible.Auth.User{} = user) do
|
||||||
user = Repo.preload(user, :enterprise_plan)
|
user
|
||||||
user.enterprise_plan != nil
|
|> Ecto.assoc(:enterprise_plan)
|
||||||
|
|> Repo.exists?()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
defmodule Plausible.Auth.UserAdmin do
|
defmodule Plausible.Auth.UserAdmin do
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
def custom_index_query(_conn, _schema, query) do
|
def custom_index_query(_conn, _schema, query) do
|
||||||
subscripton_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at])
|
subscripton_q = from(s in Plausible.Billing.Subscription, order_by: [desc: s.inserted_at])
|
||||||
@ -83,7 +85,7 @@ defmodule Plausible.Auth.UserAdmin do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp subscription_plan(user) do
|
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
|
user.subscription.paddle_subscription_id do
|
||||||
quota = PlausibleWeb.AuthView.subscription_quota(user.subscription)
|
quota = PlausibleWeb.AuthView.subscription_quota(user.subscription)
|
||||||
interval = PlausibleWeb.AuthView.subscription_interval(user.subscription)
|
interval = PlausibleWeb.AuthView.subscription_interval(user.subscription)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
defmodule Plausible.Billing do
|
defmodule Plausible.Billing do
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
alias Plausible.Billing.Subscription
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
@spec active_subscription_for(integer()) :: Subscription.t() | nil
|
@spec active_subscription_for(integer()) :: Subscription.t() | nil
|
||||||
@ -92,10 +93,12 @@ defmodule Plausible.Billing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp subscription_is_active?(%Subscription{status: "active"}), do: true
|
defp subscription_is_active?(%Subscription{status: Subscription.Status.active()}), do: true
|
||||||
defp subscription_is_active?(%Subscription{status: "past_due"}), 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())
|
subscription.next_bill_date && !Timex.before?(subscription.next_bill_date, Timex.today())
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -269,7 +272,7 @@ defmodule Plausible.Billing do
|
|||||||
|
|
||||||
defp active_subscription_query(user_id) do
|
defp active_subscription_query(user_id) do
|
||||||
from(s in Subscription,
|
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],
|
order_by: [desc: s.inserted_at],
|
||||||
limit: 1
|
limit: 1
|
||||||
)
|
)
|
||||||
|
@ -134,6 +134,19 @@ defmodule Plausible.Billing.Plans do
|
|||||||
end
|
end
|
||||||
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
|
def subscription_interval(subscription) do
|
||||||
case get_subscription_plan(subscription) do
|
case get_subscription_plan(subscription) do
|
||||||
%EnterprisePlan{billing_interval: interval} ->
|
%EnterprisePlan{billing_interval: interval} ->
|
||||||
@ -185,6 +198,13 @@ defmodule Plausible.Billing.Plans do
|
|||||||
end
|
end
|
||||||
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
|
defp get_enterprise_plan(%Subscription{} = subscription) do
|
||||||
Repo.get_by(EnterprisePlan,
|
Repo.get_by(EnterprisePlan,
|
||||||
user_id: subscription.user_id,
|
user_id: subscription.user_id,
|
||||||
@ -217,7 +237,7 @@ defmodule Plausible.Billing.Plans do
|
|||||||
def suggest(user, usage_during_cycle) do
|
def suggest(user, usage_during_cycle) do
|
||||||
cond do
|
cond do
|
||||||
usage_during_cycle > @enterprise_level_usage -> :enterprise
|
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)
|
true -> suggest_by_usage(user, usage_during_cycle)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,38 +1,10 @@
|
|||||||
defmodule Plausible.Billing.Subscription do
|
defmodule Plausible.Billing.Subscription do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
@moduledoc """
|
alias Plausible.Billing.Subscription
|
||||||
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
|
|
||||||
"""
|
|
||||||
|
|
||||||
@type t() :: %__MODULE__{}
|
@type t() :: %__MODULE__{}
|
||||||
|
|
||||||
@ -50,14 +22,12 @@ defmodule Plausible.Billing.Subscription do
|
|||||||
|
|
||||||
@optional_fields [:last_bill_date]
|
@optional_fields [:last_bill_date]
|
||||||
|
|
||||||
@valid_statuses ["active", "past_due", "deleted", "paused"]
|
|
||||||
|
|
||||||
schema "subscriptions" do
|
schema "subscriptions" do
|
||||||
field :paddle_subscription_id, :string
|
field :paddle_subscription_id, :string
|
||||||
field :paddle_plan_id, :string
|
field :paddle_plan_id, :string
|
||||||
field :update_url, :string
|
field :update_url, :string
|
||||||
field :cancel_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_amount, :string
|
||||||
field :next_bill_date, :date
|
field :next_bill_date, :date
|
||||||
field :last_bill_date, :date
|
field :last_bill_date, :date
|
||||||
@ -72,20 +42,18 @@ defmodule Plausible.Billing.Subscription do
|
|||||||
model
|
model
|
||||||
|> cast(attrs, @required_fields ++ @optional_fields)
|
|> cast(attrs, @required_fields ++ @optional_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> validate_inclusion(:status, @valid_statuses)
|
|
||||||
|> unique_constraint(:paddle_subscription_id)
|
|> unique_constraint(:paddle_subscription_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def free(attrs \\ %{}) do
|
def free(attrs \\ %{}) do
|
||||||
%__MODULE__{
|
%__MODULE__{
|
||||||
paddle_plan_id: "free_10k",
|
paddle_plan_id: "free_10k",
|
||||||
status: "active",
|
status: Subscription.Status.active(),
|
||||||
next_bill_amount: "0",
|
next_bill_amount: "0",
|
||||||
currency_code: "EUR"
|
currency_code: "EUR"
|
||||||
}
|
}
|
||||||
|> cast(attrs, @required_fields)
|
|> cast(attrs, @required_fields)
|
||||||
|> validate_required([:user_id])
|
|> validate_required([:user_id])
|
||||||
|> validate_inclusion(:status, @valid_statuses)
|
|
||||||
|> unique_constraint(:paddle_subscription_id)
|
|> unique_constraint(:paddle_subscription_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
45
lib/plausible/billing/subscription/status.ex
Normal file
45
lib/plausible/billing/subscription/status.ex
Normal 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
|
@ -1,7 +1,8 @@
|
|||||||
defmodule Plausible.Billing.Subscriptions do
|
defmodule Plausible.Billing.Subscriptions do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
||||||
alias Plausible.Billing.{Subscription}
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
@spec expired?(Subscription.t()) :: boolean()
|
@spec expired?(Subscription.t()) :: boolean()
|
||||||
@doc """
|
@doc """
|
||||||
@ -14,9 +15,19 @@ defmodule Plausible.Billing.Subscriptions do
|
|||||||
def expired?(%Subscription{paddle_plan_id: "free_10k"}), do: false
|
def expired?(%Subscription{paddle_plan_id: "free_10k"}), do: false
|
||||||
|
|
||||||
def expired?(%Subscription{status: status, next_bill_date: next_bill_date}) do
|
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
|
expired? = Timex.compare(next_bill_date, Timex.today()) < 0
|
||||||
|
|
||||||
cancelled? && expired?
|
cancelled? && expired?
|
||||||
end
|
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
|
end
|
||||||
|
@ -3,6 +3,7 @@ defmodule PlausibleWeb.Components.Billing do
|
|||||||
|
|
||||||
use Phoenix.Component
|
use Phoenix.Component
|
||||||
import PlausibleWeb.Components.Generic
|
import PlausibleWeb.Components.Generic
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
alias PlausibleWeb.Router.Helpers, as: Routes
|
||||||
alias Plausible.Billing.Subscription
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
@ -50,6 +51,7 @@ defmodule PlausibleWeb.Components.Billing do
|
|||||||
def monthly_quota_box(%{business_tier: true} = assigns) do
|
def monthly_quota_box(%{business_tier: true} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div
|
<div
|
||||||
|
id="monthly-quota-box"
|
||||||
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
class="h-32 px-2 py-4 my-4 text-center bg-gray-100 rounded dark:bg-gray-900"
|
||||||
style="width: 11.75rem;"
|
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">
|
<div class="py-2 text-xl font-medium dark:text-gray-100">
|
||||||
<%= PlausibleWeb.AuthView.subscription_quota(@subscription, format: :long) %>
|
<%= PlausibleWeb.AuthView.subscription_quota(@subscription, format: :long) %>
|
||||||
</div>
|
</div>
|
||||||
<.styled_link href={Routes.billing_path(@conn, :choose_plan)} class="text-sm font-medium">
|
<.styled_link
|
||||||
<%= upgrade_link_text(@subscription) %>
|
: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>
|
</.styled_link>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -77,15 +84,15 @@ defmodule PlausibleWeb.Components.Billing do
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.styled_link
|
<.styled_link
|
||||||
:if={@subscription.status == "active"}
|
:if={@subscription.status == Subscription.Status.active()}
|
||||||
href={Routes.billing_path(@conn, :change_plan_form)}
|
href={Routes.billing_path(PlausibleWeb.Endpoint, :change_plan_form)}
|
||||||
class="text-sm font-medium"
|
class="text-sm font-medium"
|
||||||
>
|
>
|
||||||
Change plan
|
Change plan
|
||||||
</.styled_link>
|
</.styled_link>
|
||||||
|
|
||||||
<span
|
<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"
|
class="text-sm text-gray-600 dark:text-gray-400 font-medium"
|
||||||
tooltip="Please update your billing details before changing plans"
|
tooltip="Please update your billing details before changing plans"
|
||||||
>
|
>
|
||||||
@ -93,7 +100,10 @@ defmodule PlausibleWeb.Components.Billing do
|
|||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="py-2 text-xl font-medium dark:text-gray-100">Free trial</div>
|
<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
|
Upgrade
|
||||||
</.styled_link>
|
</.styled_link>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -101,7 +111,9 @@ defmodule PlausibleWeb.Components.Billing do
|
|||||||
"""
|
"""
|
||||||
end
|
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"""
|
~H"""
|
||||||
<aside class={@class}>
|
<aside class={@class}>
|
||||||
<div class="shadow-md dark:shadow-none rounded-lg bg-yellow-100 p-4">
|
<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_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"""
|
~H"""
|
||||||
<aside class={@class}>
|
<aside class={@class}>
|
||||||
<div class="shadow-md dark:shadow-none rounded-lg bg-red-100 p-4">
|
<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 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
|
||||||
|> Money.to_string!(format: :short, fractional_digits: 2)
|
|> Money.to_string!(opts)
|
||||||
|> String.replace(".00", "")
|
|> String.replace(".00", "")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp upgrade_link_text(nil), do: "Upgrade"
|
def paddle_button(assigns) do
|
||||||
defp upgrade_link_text(_subscription), do: "Change plan"
|
~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
|
end
|
||||||
|
@ -42,6 +42,7 @@ defmodule PlausibleWeb.Components.Generic do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attr :id, :any, default: nil
|
||||||
attr :href, :string, required: true
|
attr :href, :string, required: true
|
||||||
attr :new_tab, :boolean
|
attr :new_tab, :boolean
|
||||||
attr :class, :string, default: ""
|
attr :class, :string, default: ""
|
||||||
@ -53,6 +54,7 @@ defmodule PlausibleWeb.Components.Generic do
|
|||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<.link
|
<.link
|
||||||
|
id={@id}
|
||||||
class={[
|
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",
|
"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
|
@class
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
defmodule PlausibleWeb.BillingController do
|
defmodule PlausibleWeb.BillingController do
|
||||||
use PlausibleWeb, :controller
|
use PlausibleWeb, :controller
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
alias Plausible.Billing
|
|
||||||
require Logger
|
require Logger
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing
|
||||||
|
alias Plausible.Billing.{Plans, Subscription}
|
||||||
|
|
||||||
plug PlausibleWeb.RequireAccountPlug
|
plug PlausibleWeb.RequireAccountPlug
|
||||||
|
|
||||||
@ -12,18 +14,14 @@ defmodule PlausibleWeb.BillingController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def upgrade(conn, _params) do
|
def upgrade(conn, _params) do
|
||||||
user =
|
user = conn.assigns[:current_user]
|
||||||
conn.assigns[:current_user]
|
|
||||||
|> Repo.preload(:enterprise_plan)
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
user.subscription && user.subscription.status == "active" ->
|
Plausible.Auth.enterprise_configured?(user) ->
|
||||||
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
|
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|
||||||
|
|
||||||
user.enterprise_plan ->
|
user.subscription && user.subscription.status == Subscription.Status.active() ->
|
||||||
redirect(conn,
|
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
|
||||||
to: Routes.billing_path(conn, :upgrade_enterprise_plan, user.enterprise_plan.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
render(conn, "upgrade.html",
|
render(conn, "upgrade.html",
|
||||||
@ -36,7 +34,7 @@ defmodule PlausibleWeb.BillingController do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def choose_plan(conn, _params) do
|
def choose_plan(conn, _params) do
|
||||||
user = conn.assigns[:current_user]
|
user = conn.assigns.current_user
|
||||||
|
|
||||||
if FunWithFlags.enabled?(:business_tier, for: user) do
|
if FunWithFlags.enabled?(:business_tier, for: user) do
|
||||||
render(conn, "choose_plan.html",
|
render(conn, "choose_plan.html",
|
||||||
@ -50,54 +48,66 @@ defmodule PlausibleWeb.BillingController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def upgrade_enterprise_plan(conn, %{"plan_id" => plan_id}) do
|
def upgrade_to_enterprise_plan(conn, _params) do
|
||||||
user = conn.assigns[:current_user]
|
user = Plausible.Users.with_subscription(conn.assigns.current_user)
|
||||||
subscription = user.subscription
|
|
||||||
plan = Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: user.id, id: plan_id)
|
if FunWithFlags.enabled?(:business_tier, for: user) do
|
||||||
|
{latest_enterprise_plan, price} = Plans.latest_enterprise_plan_with_price(user)
|
||||||
|
|
||||||
|
subscription_resumable? = Plausible.Billing.Subscriptions.resumable?(user.subscription)
|
||||||
|
|
||||||
|
subscribed_to_latest? =
|
||||||
|
subscription_resumable? &&
|
||||||
|
user.subscription.paddle_plan_id == latest_enterprise_plan.paddle_plan_id
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
plan && subscription && plan.paddle_plan_id == subscription.paddle_plan_id ->
|
user.subscription &&
|
||||||
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
|
user.subscription.status in [
|
||||||
|
Subscription.Status.past_due(),
|
||||||
|
Subscription.Status.paused()
|
||||||
|
] ->
|
||||||
|
redirect(conn, to: Routes.auth_path(conn, :user_settings))
|
||||||
|
|
||||||
plan ->
|
subscribed_to_latest? ->
|
||||||
render(conn, "upgrade_to_plan.html",
|
render(conn, "change_enterprise_plan_contact_us.html",
|
||||||
skip_plausible_tracking: true,
|
skip_plausible_tracking: true,
|
||||||
user: user,
|
|
||||||
plan: plan,
|
|
||||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||||
)
|
)
|
||||||
|
|
||||||
true ->
|
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)
|
render_error(conn, 404)
|
||||||
end
|
end
|
||||||
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
|
def upgrade_success(conn, _params) do
|
||||||
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
|
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_plan_form(conn, _params) do
|
def change_plan_form(conn, _params) do
|
||||||
user =
|
user = conn.assigns[:current_user]
|
||||||
conn.assigns[:current_user]
|
|
||||||
|> Repo.preload(:enterprise_plan)
|
|
||||||
|
|
||||||
subscription = Billing.active_subscription_for(user.id)
|
subscription = Billing.active_subscription_for(user.id)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
subscription && user.enterprise_plan &&
|
Plausible.Auth.enterprise_configured?(user) ->
|
||||||
subscription.paddle_plan_id !== user.enterprise_plan.paddle_plan_id ->
|
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|
||||||
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"}
|
|
||||||
)
|
|
||||||
|
|
||||||
subscription ->
|
subscription ->
|
||||||
render(conn, "change_plan.html",
|
render(conn, "change_plan.html",
|
||||||
@ -111,26 +121,10 @@ defmodule PlausibleWeb.BillingController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_enterprise_plan(conn, %{"plan_id" => plan_id}) do
|
def change_enterprise_plan(conn, _params) do
|
||||||
user = conn.assigns[:current_user]
|
# DEPRECATED - For some time we need to ensure that the existing
|
||||||
|
# links sent out to customers will lead the user to the right place
|
||||||
new_plan = Repo.get_by(Plausible.Billing.EnterprisePlan, user_id: user.id, id: plan_id)
|
redirect(conn, to: Routes.billing_path(conn, :upgrade_to_enterprise_plan))
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
|
def change_plan_preview(conn, %{"plan_id" => new_plan_id}) do
|
||||||
@ -144,7 +138,7 @@ defmodule PlausibleWeb.BillingController do
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
redirect(conn, to: "/billing/upgrade")
|
redirect(conn, to: Routes.billing_path(conn, :upgrade))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -4,12 +4,15 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
"""
|
"""
|
||||||
use Phoenix.LiveView
|
use Phoenix.LiveView
|
||||||
use Phoenix.HTML
|
use Phoenix.HTML
|
||||||
alias Plausible.Users
|
|
||||||
alias Plausible.Billing.{Plans, Plan, Quota}
|
|
||||||
alias PlausibleWeb.Router.Helpers, as: Routes
|
|
||||||
|
|
||||||
import PlausibleWeb.Components.Billing
|
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"
|
@contact_link "https://plausible.io/contact"
|
||||||
@billing_faq_link "https://plausible.io/docs/billing"
|
@billing_faq_link "https://plausible.io/docs/billing"
|
||||||
|
|
||||||
@ -255,7 +258,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
<%= cond do %>
|
<%= cond do %>
|
||||||
<% !@available -> %>
|
<% !@available -> %>
|
||||||
<.contact_button class="bg-indigo-600 hover:bg-indigo-500 text-white" />
|
<.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
|
<.render_change_plan_link
|
||||||
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
|
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
|
||||||
text={
|
text={
|
||||||
@ -270,9 +273,12 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
/>
|
/>
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<.paddle_button
|
<.paddle_button
|
||||||
|
id={"#{@kind}-checkout"}
|
||||||
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
|
paddle_product_id={get_paddle_product_id(@plan_to_render, @selected_interval)}
|
||||||
{assigns}
|
{assigns}
|
||||||
/>
|
>
|
||||||
|
Upgrade
|
||||||
|
</.paddle_button>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
@ -325,7 +331,8 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
<.change_plan_link
|
<.change_plan_link
|
||||||
plan_already_owned={@text == "Currently on this plan"}
|
plan_already_owned={@text == "Currently on this plan"}
|
||||||
billing_details_expired={
|
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}
|
{assigns}
|
||||||
/>
|
/>
|
||||||
@ -336,7 +343,7 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
~H"""
|
~H"""
|
||||||
<.link
|
<.link
|
||||||
id={"#{@kind}-checkout"}
|
id={"#{@kind}-checkout"}
|
||||||
href={"/billing/change-plan/preview/" <> @paddle_product_id}
|
href={Routes.billing_path(PlausibleWeb.Endpoint, :change_plan_preview, @paddle_product_id)}
|
||||||
class={[
|
class={[
|
||||||
"w-full mt-6 block rounded-md py-2 px-3 text-center text-sm font-semibold leading-6 text-white",
|
"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",
|
!(@plan_already_owned || @billing_details_expired) && "bg-indigo-600 hover:bg-indigo-500",
|
||||||
@ -355,18 +362,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
"""
|
"""
|
||||||
end
|
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
|
defp contact_button(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<.link
|
<.link
|
||||||
@ -519,19 +514,6 @@ defmodule PlausibleWeb.Live.ChoosePlan do
|
|||||||
"""
|
"""
|
||||||
end
|
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
|
defp slider_styles(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<style>
|
<style>
|
||||||
|
@ -201,7 +201,7 @@ defmodule PlausibleWeb.Router do
|
|||||||
post "/billing/change-plan/:new_plan_id", BillingController, :change_plan
|
post "/billing/change-plan/:new_plan_id", BillingController, :change_plan
|
||||||
get "/billing/upgrade", BillingController, :upgrade
|
get "/billing/upgrade", BillingController, :upgrade
|
||||||
get "/billing/choose-plan", BillingController, :choose_plan
|
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/upgrade/enterprise/:plan_id", BillingController, :upgrade_enterprise_plan
|
||||||
get "/billing/change-plan/enterprise/:plan_id", BillingController, :change_enterprise_plan
|
get "/billing/change-plan/enterprise/:plan_id", BillingController, :change_enterprise_plan
|
||||||
get "/billing/upgrade-success", BillingController, :upgrade_success
|
get "/billing/upgrade-success", BillingController, :upgrade_success
|
||||||
|
@ -26,7 +26,9 @@
|
|||||||
<div class="my-4 border-b border-gray-400"></div>
|
<div class="my-4 border-b border-gray-400"></div>
|
||||||
|
|
||||||
<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"
|
class="p-2 bg-red-100 rounded-lg sm:p-3"
|
||||||
>
|
>
|
||||||
<div class="flex flex-wrap items-center justify-between">
|
<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">
|
<div class="flex flex-col items-center justify-between mt-8 sm:flex-row sm:items-start">
|
||||||
<PlausibleWeb.Components.Billing.monthly_quota_box
|
<PlausibleWeb.Components.Billing.monthly_quota_box
|
||||||
|
user={@user}
|
||||||
subscription={@subscription}
|
subscription={@subscription}
|
||||||
conn={@conn}
|
|
||||||
business_tier={FunWithFlags.enabled?(:business_tier, for: @user)}
|
business_tier={FunWithFlags.enabled?(:business_tier, for: @user)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@ -70,7 +72,7 @@
|
|||||||
style="width: 11.75rem;"
|
style="width: 11.75rem;"
|
||||||
>
|
>
|
||||||
<h4 class="font-black dark:text-gray-100">Next bill amount</h4>
|
<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">
|
<div class="py-2 text-xl font-medium dark:text-gray-100">
|
||||||
<%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %>
|
<%= PlausibleWeb.BillingView.present_currency(@subscription.currency_code) %><%= @subscription.next_bill_amount %>
|
||||||
</div>
|
</div>
|
||||||
@ -90,7 +92,7 @@
|
|||||||
>
|
>
|
||||||
<h4 class="font-black dark:text-gray-100">Next bill date</h4>
|
<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">
|
<div class="py-2 text-xl font-medium dark:text-gray-100">
|
||||||
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
|
<%= Timex.format!(@subscription.next_bill_date, "{Mshort} {D}, {YYYY}") %>
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +142,7 @@
|
|||||||
</article>
|
</article>
|
||||||
|
|
||||||
<%= cond do %>
|
<%= 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">
|
<div class="mt-8">
|
||||||
<%= link("Cancel my subscription",
|
<%= link("Cancel my subscription",
|
||||||
to: @subscription.cancel_url,
|
to: @subscription.cancel_url,
|
||||||
@ -150,15 +152,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<% true -> %>
|
<% true -> %>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<%= link("Upgrade",
|
<PlausibleWeb.Components.Billing.upgrade_link
|
||||||
to:
|
user={@user}
|
||||||
if(FunWithFlags.enabled?(:business_tier, for: @user),
|
business_tier={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"
|
|
||||||
) %>
|
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@ -344,7 +341,7 @@
|
|||||||
Deleting your account removes all sites and stats you've collected
|
Deleting your account removes all sites and stats you've collected
|
||||||
</p>
|
</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">
|
<span class="mt-6 bg-gray-300 button dark:bg-gray-600 hover:shadow-none hover:bg-gray-300">
|
||||||
Delete my account
|
Delete my account
|
||||||
</span>
|
</span>
|
||||||
|
@ -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") %>
|
|
@ -66,12 +66,12 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between mt-10">
|
<div class="flex items-center justify-between mt-10">
|
||||||
<span class="flex rounded-md shadow-sm">
|
<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
|
Back
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex space-betwee rounded-md shadow-sm">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -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="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>
|
<div class="mb-4 text-xs font-medium dark:text-gray-100">+ VAT if applicable</div>
|
||||||
<span class="inline-flex rounded-md shadow-sm">
|
<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>
|
<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
|
Pay securely via Paddle
|
||||||
</button>
|
</button>
|
||||||
|
@ -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 />
|
@ -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") %>
|
|
@ -23,7 +23,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ml-3">
|
<div class="ml-3">
|
||||||
<h3 class="text-sm font-medium text-yellow-800">
|
<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
|
You have outgrown your Plausible subscription tier
|
||||||
<% else %>
|
<% else %>
|
||||||
Please upgrade your account
|
Please upgrade your account
|
||||||
@ -31,7 +31,7 @@
|
|||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2 text-sm text-yellow-700">
|
<div class="mt-2 text-sm text-yellow-700">
|
||||||
<p>
|
<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
|
In order to keep your stats running, we require you to upgrade
|
||||||
your account to accommodate your new usage levels. Please contact
|
your account to accommodate your new usage levels. Please contact
|
||||||
us to discuss a new custom enterprise plan. <%= link("Contact us →",
|
us to discuss a new custom enterprise plan. <%= link("Contact us →",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
defmodule PlausibleWeb.AuthView do
|
defmodule PlausibleWeb.AuthView do
|
||||||
use PlausibleWeb, :view
|
use PlausibleWeb, :view
|
||||||
alias Plausible.Billing.Plans
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.{Plans, Subscription}
|
||||||
|
|
||||||
def base_domain do
|
def base_domain do
|
||||||
PlausibleWeb.Endpoint.host()
|
PlausibleWeb.Endpoint.host()
|
||||||
@ -56,15 +57,17 @@ defmodule PlausibleWeb.AuthView do
|
|||||||
:lists.reverse(list) ++ acc
|
:lists.reverse(list) ++ acc
|
||||||
end
|
end
|
||||||
|
|
||||||
def present_subscription_status("active"), do: "Active"
|
@spec present_subscription_status(Subscription.Status.status()) :: String.t()
|
||||||
def present_subscription_status("past_due"), do: "Past due"
|
def present_subscription_status(Subscription.Status.active()), do: "Active"
|
||||||
def present_subscription_status("deleted"), do: "Cancelled"
|
def present_subscription_status(Subscription.Status.past_due()), do: "Past due"
|
||||||
def present_subscription_status("paused"), do: "Paused"
|
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 present_subscription_status(status), do: status
|
||||||
|
|
||||||
def subscription_colors("active"), do: "bg-green-100 text-green-800"
|
@spec subscription_colors(Subscription.Status.status()) :: String.t()
|
||||||
def subscription_colors("past_due"), do: "bg-yellow-100 text-yellow-800"
|
def subscription_colors(Subscription.Status.active()), do: "bg-green-100 text-green-800"
|
||||||
def subscription_colors("paused"), do: "bg-red-100 text-red-800"
|
def subscription_colors(Subscription.Status.past_due()), do: "bg-yellow-100 text-yellow-800"
|
||||||
def subscription_colors("deleted"), do: "bg-red-100 text-red-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: ""
|
def subscription_colors(_), do: ""
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
defmodule Plausible.Workers.CheckUsage do
|
defmodule Plausible.Workers.CheckUsage do
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
use Oban.Worker, queue: :check_usage
|
use Oban.Worker, queue: :check_usage
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
defmacro yesterday() do
|
defmacro yesterday() do
|
||||||
quote do
|
quote do
|
||||||
@ -41,7 +43,7 @@ defmodule Plausible.Workers.CheckUsage do
|
|||||||
left_join: ep in Plausible.Billing.EnterprisePlan,
|
left_join: ep in Plausible.Billing.EnterprisePlan,
|
||||||
on: ep.user_id == u.id,
|
on: ep.user_id == u.id,
|
||||||
where: is_nil(u.grace_period),
|
where: is_nil(u.grace_period),
|
||||||
where: s.status == "active",
|
where: s.status == ^Subscription.Status.active(),
|
||||||
where: not is_nil(s.last_bill_date),
|
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.
|
# 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:
|
where:
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
defmodule Plausible.Workers.NotifyAnnualRenewal do
|
defmodule Plausible.Workers.NotifyAnnualRenewal do
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
use Oban.Worker, queue: :notify_annual_renewal
|
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()
|
@yearly_plans Plausible.Billing.Plans.yearly_product_ids()
|
||||||
|
|
||||||
@ -44,11 +47,11 @@ defmodule Plausible.Workers.NotifyAnnualRenewal do
|
|||||||
|
|
||||||
for user <- users do
|
for user <- users do
|
||||||
case user.subscription.status do
|
case user.subscription.status do
|
||||||
"active" ->
|
Subscription.Status.active() ->
|
||||||
template = PlausibleWeb.Email.yearly_renewal_notification(user)
|
template = PlausibleWeb.Email.yearly_renewal_notification(user)
|
||||||
Plausible.Mailer.send(template)
|
Plausible.Mailer.send(template)
|
||||||
|
|
||||||
"deleted" ->
|
Subscription.Status.deleted() ->
|
||||||
template = PlausibleWeb.Email.yearly_expiration_notification(user)
|
template = PlausibleWeb.Email.yearly_expiration_notification(user)
|
||||||
Plausible.Mailer.send(template)
|
Plausible.Mailer.send(template)
|
||||||
|
|
||||||
|
@ -41,12 +41,12 @@ defmodule Plausible.AuthTest do
|
|||||||
end
|
end
|
||||||
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_without_plan = insert(:user)
|
||||||
user_with_plan = insert(:user, enterprise_plan: build(:enterprise_plan))
|
user_with_plan = insert(:user, enterprise_plan: build(:enterprise_plan))
|
||||||
|
|
||||||
assert Auth.enterprise?(user_with_plan)
|
assert Auth.enterprise_configured?(user_with_plan)
|
||||||
refute Auth.enterprise?(user_without_plan)
|
refute Auth.enterprise_configured?(user_without_plan)
|
||||||
refute Auth.enterprise?(nil)
|
refute Auth.enterprise_configured?(nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
defmodule Plausible.BillingTest do
|
defmodule Plausible.BillingTest do
|
||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
use Bamboo.Test, shared: true
|
use Bamboo.Test, shared: true
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
alias Plausible.Billing
|
alias Plausible.Billing
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
describe "last_two_billing_cycles" do
|
describe "last_two_billing_cycles" do
|
||||||
test "billing on the 1st" 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
|
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))
|
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
|
assert Billing.check_needs_to_upgrade(user) == :no_upgrade_needed
|
||||||
end
|
end
|
||||||
@ -162,7 +169,7 @@ defmodule Plausible.BillingTest do
|
|||||||
|
|
||||||
insert(:subscription,
|
insert(:subscription,
|
||||||
user: user,
|
user: user,
|
||||||
status: "deleted",
|
status: Subscription.Status.deleted(),
|
||||||
next_bill_date: Timex.shift(Timex.today(), days: -1)
|
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
|
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))
|
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}
|
assert Billing.check_needs_to_upgrade(user) == {:needs_to_upgrade, :no_active_subscription}
|
||||||
end
|
end
|
||||||
@ -311,7 +319,7 @@ defmodule Plausible.BillingTest do
|
|||||||
|
|
||||||
test "unlocks sites if subscription is changed from past_due to active" do
|
test "unlocks sites if subscription is changed from past_due to active" do
|
||||||
user = insert(:user)
|
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])
|
site = insert(:site, locked: true, members: [user])
|
||||||
|
|
||||||
Billing.subscription_updated(%{
|
Billing.subscription_updated(%{
|
||||||
@ -445,7 +453,7 @@ defmodule Plausible.BillingTest do
|
|||||||
describe "subscription_cancelled" do
|
describe "subscription_cancelled" do
|
||||||
test "sets the status to deleted" do
|
test "sets the status to deleted" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
subscription = insert(:subscription, status: "active", user: user)
|
subscription = insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||||
|
|
||||||
Billing.subscription_cancelled(%{
|
Billing.subscription_cancelled(%{
|
||||||
"alert_name" => "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)
|
subscription = Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
|
||||||
assert subscription.status == "deleted"
|
assert subscription.status == Subscription.Status.deleted()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ignores if the subscription cannot be found" do
|
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
|
test "sends an email to confirm cancellation" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
subscription = insert(:subscription, status: "active", user: user)
|
subscription = insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||||
|
|
||||||
Billing.subscription_cancelled(%{
|
Billing.subscription_cancelled(%{
|
||||||
"alert_name" => "subscription_cancelled",
|
"alert_name" => "subscription_cancelled",
|
||||||
@ -528,8 +536,8 @@ defmodule Plausible.BillingTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "active_subscription_for/1 returns active subscription" do
|
test "active_subscription_for/1 returns active subscription" do
|
||||||
active = insert(:subscription, user: insert(:user), status: "active")
|
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
|
||||||
paused = insert(:subscription, user: insert(:user), status: "paused")
|
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
|
||||||
user_without_subscription = insert(:user)
|
user_without_subscription = insert(:user)
|
||||||
|
|
||||||
assert Billing.active_subscription_for(active.user_id).id == active.id
|
assert Billing.active_subscription_for(active.user_id).id == active.id
|
||||||
@ -538,8 +546,8 @@ defmodule Plausible.BillingTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "has_active_subscription?/1 returns whether the user has an active subscription" do
|
test "has_active_subscription?/1 returns whether the user has an active subscription" do
|
||||||
active = insert(:subscription, user: insert(:user), status: "active")
|
active = insert(:subscription, user: insert(:user), status: Subscription.Status.active())
|
||||||
paused = insert(:subscription, user: insert(:user), status: "paused")
|
paused = insert(:subscription, user: insert(:user), status: Subscription.Status.paused())
|
||||||
user_without_subscription = insert(:user)
|
user_without_subscription = insert(:user)
|
||||||
|
|
||||||
assert Billing.has_active_subscription?(active.user_id)
|
assert Billing.has_active_subscription?(active.user_id)
|
||||||
|
@ -61,6 +61,28 @@ defmodule Plausible.Billing.PlansTest do
|
|||||||
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v4_business_plan_id
|
(%Money{} = plan.monthly_cost) && plan.monthly_product_id == @v4_business_plan_id
|
||||||
end)
|
end)
|
||||||
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
|
end
|
||||||
|
|
||||||
describe "subscription_interval" do
|
describe "subscription_interval" do
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
defmodule Plausible.Billing.SiteLockerTest do
|
defmodule Plausible.Billing.SiteLockerTest do
|
||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
use Bamboo.Test, shared: true
|
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
|
describe "update_sites_for/1" do
|
||||||
test "does not lock sites if user is on trial" 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
|
test "does not lock if user has an active subscription" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "active", user: user)
|
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||||
|
|
||||||
site =
|
site =
|
||||||
insert(:site,
|
insert(:site,
|
||||||
@ -39,7 +40,7 @@ defmodule Plausible.Billing.SiteLockerTest do
|
|||||||
|
|
||||||
test "does not lock user who is past due" do
|
test "does not lock user who is past due" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "past_due", user: user)
|
insert(:subscription, status: Subscription.Status.past_due(), user: user)
|
||||||
|
|
||||||
site =
|
site =
|
||||||
insert(: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
|
test "does not lock user who cancelled subscription but it hasn't expired yet" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "deleted", user: user)
|
insert(:subscription, status: Subscription.Status.deleted(), user: user)
|
||||||
|
|
||||||
site =
|
site =
|
||||||
insert(: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 =
|
site =
|
||||||
insert(:site,
|
insert(:site,
|
||||||
@ -96,7 +97,7 @@ defmodule Plausible.Billing.SiteLockerTest do
|
|||||||
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
user = insert(:user, trial_expiry_date: Timex.shift(Timex.today(), days: -1))
|
||||||
|
|
||||||
insert(:subscription,
|
insert(:subscription,
|
||||||
status: "deleted",
|
status: Subscription.Status.deleted(),
|
||||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||||
user: user
|
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 =
|
site =
|
||||||
insert(: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,
|
insert(:site,
|
||||||
memberships: [
|
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,
|
insert(:site,
|
||||||
memberships: [
|
memberships: [
|
||||||
|
@ -3,12 +3,19 @@ defmodule PlausibleWeb.AuthControllerTest do
|
|||||||
use Bamboo.Test
|
use Bamboo.Test
|
||||||
use Plausible.Repo
|
use Plausible.Repo
|
||||||
|
|
||||||
|
import Plausible.Test.Support.HTML
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
|
||||||
alias Plausible.Auth.User
|
alias Plausible.Auth.User
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
|
@v3_plan_id "749355"
|
||||||
|
@configured_enterprise_plan_paddle_plan_id "123"
|
||||||
|
|
||||||
describe "GET /register" do
|
describe "GET /register" do
|
||||||
test "shows the register form", %{conn: conn} do
|
test "shows the register form", %{conn: conn} do
|
||||||
conn = get(conn, "/register")
|
conn = get(conn, "/register")
|
||||||
@ -460,15 +467,10 @@ defmodule PlausibleWeb.AuthControllerTest do
|
|||||||
test "shows enterprise plan subscription", %{conn: conn, user: user} do
|
test "shows enterprise plan subscription", %{conn: conn, user: user} do
|
||||||
insert(:subscription, paddle_plan_id: "123", user: user)
|
insert(:subscription, paddle_plan_id: "123", user: user)
|
||||||
|
|
||||||
insert(:enterprise_plan,
|
configure_enterprise_plan(user)
|
||||||
paddle_plan_id: "123",
|
|
||||||
user: user,
|
|
||||||
monthly_pageview_limit: 10_000_000,
|
|
||||||
billing_interval: :yearly
|
|
||||||
)
|
|
||||||
|
|
||||||
conn = get(conn, "/settings")
|
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"
|
assert html_response(conn, 200) =~ "yearly billing"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -476,27 +478,139 @@ defmodule PlausibleWeb.AuthControllerTest do
|
|||||||
conn: conn,
|
conn: conn,
|
||||||
user: user
|
user: user
|
||||||
} do
|
} 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,
|
insert(:enterprise_plan,
|
||||||
paddle_plan_id: "123",
|
paddle_plan_id: "1234",
|
||||||
user: user,
|
user: user,
|
||||||
monthly_pageview_limit: 10_000_000,
|
monthly_pageview_limit: 10_000_000,
|
||||||
billing_interval: :yearly
|
billing_interval: :yearly
|
||||||
)
|
)
|
||||||
|
|
||||||
insert(:enterprise_plan,
|
configure_enterprise_plan(user)
|
||||||
paddle_plan_id: "1234",
|
|
||||||
user: user,
|
|
||||||
monthly_pageview_limit: 20_000_000,
|
|
||||||
billing_interval: :yearly
|
|
||||||
)
|
|
||||||
|
|
||||||
conn = get(conn, "/settings")
|
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"
|
assert html_response(conn, 200) =~ "yearly billing"
|
||||||
end
|
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
|
test "shows invoices for subscribed user", %{conn: conn, user: user} do
|
||||||
insert(:subscription,
|
insert(:subscription,
|
||||||
paddle_plan_id: "558018",
|
paddle_plan_id: "558018",
|
||||||
@ -613,8 +727,8 @@ defmodule PlausibleWeb.AuthControllerTest do
|
|||||||
])
|
])
|
||||||
|
|
||||||
insert(:google_auth, site: site, user: user)
|
insert(:google_auth, site: site, user: user)
|
||||||
insert(:subscription, user: user, status: "deleted")
|
insert(:subscription, user: user, status: Subscription.Status.deleted())
|
||||||
insert(:subscription, user: user, status: "active")
|
insert(:subscription, user: user, status: Subscription.Status.active())
|
||||||
|
|
||||||
conn = delete(conn, "/me")
|
conn = delete(conn, "/me")
|
||||||
assert redirected_to(conn) == "/"
|
assert redirected_to(conn) == "/"
|
||||||
@ -760,4 +874,13 @@ defmodule PlausibleWeb.AuthControllerTest do
|
|||||||
end
|
end
|
||||||
)
|
)
|
||||||
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
|
end
|
||||||
|
@ -1,53 +1,41 @@
|
|||||||
defmodule PlausibleWeb.BillingControllerTest do
|
defmodule PlausibleWeb.BillingControllerTest do
|
||||||
use PlausibleWeb.ConnCase, async: true
|
use PlausibleWeb.ConnCase, async: true
|
||||||
|
import Plausible.Test.Support.HTML
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
describe "GET /upgrade" do
|
describe "GET /upgrade" do
|
||||||
setup [:create_user, :log_in]
|
setup [:create_user, :log_in]
|
||||||
|
|
||||||
test "shows upgrade page when user does not have a subcription already", %{conn: conn} do
|
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"
|
assert html_response(conn, 200) =~ "Upgrade your free trial"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects user to change plan if they already have a plan", %{conn: conn, user: user} do
|
test "redirects user to change plan if they already have a plan", %{conn: conn, user: user} do
|
||||||
insert(:subscription, user: user)
|
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
|
end
|
||||||
|
|
||||||
test "redirects user to enteprise plan page if they are configured with one", %{
|
test "redirects user to enteprise plan page if they are configured with one", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
user: user
|
user: user
|
||||||
} do
|
} do
|
||||||
plan = insert(:enterprise_plan, user: user)
|
insert(:enterprise_plan, user: user)
|
||||||
conn = get(conn, "/billing/upgrade")
|
conn = get(conn, Routes.billing_path(conn, :upgrade))
|
||||||
|
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
|
||||||
assert redirected_to(conn) == "/billing/upgrade/enterprise/#{plan.id}"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /upgrade/enterprise/:plan_id" do
|
describe "GET /upgrade/enterprise/:plan_id (deprecated)" do
|
||||||
setup [:create_user, :log_in]
|
setup [:create_user, :log_in]
|
||||||
|
|
||||||
test "renders enteprise plan upgrade page", %{conn: conn, user: user} do
|
test "redirects to the new :upgrade_to_enterprise_plan action", %{conn: conn} do
|
||||||
plan = insert(:enterprise_plan, user: user)
|
conn = get(conn, Routes.billing_path(conn, :upgrade_enterprise_plan, "123"))
|
||||||
|
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
|
||||||
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"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -56,68 +44,31 @@ defmodule PlausibleWeb.BillingControllerTest do
|
|||||||
|
|
||||||
test "shows change plan page if user has subsription", %{conn: conn, user: user} do
|
test "shows change plan page if user has subsription", %{conn: conn, user: user} do
|
||||||
insert(:subscription, user: user)
|
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"
|
assert html_response(conn, 200) =~ "Change subscription plan"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "redirects to /upgrade if user does not have a subscription", %{conn: conn} do
|
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
|
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
|
%{conn: conn, user: user} do
|
||||||
insert(:subscription, user: user, paddle_plan_id: "standard-plan-id")
|
insert(:enterprise_plan, user: user, paddle_plan_id: "123")
|
||||||
enterprise_plan = insert(:enterprise_plan, user: user, paddle_plan_id: "new-custom-id")
|
conn = get(conn, Routes.billing_path(conn, :change_plan_form))
|
||||||
|
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
|
||||||
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"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "GET /change-plan/enterprise/:plan_id" do
|
describe "GET /change-plan/enterprise/:plan_id (deprecated)" do
|
||||||
setup [:create_user, :log_in]
|
setup [:create_user, :log_in]
|
||||||
|
|
||||||
test "shows change plan page if user has subsription and enterprise plan", %{
|
test "redirects to the new :upgrade_to_enterprise_plan action", %{conn: conn} do
|
||||||
conn: conn,
|
conn = get(conn, Routes.billing_path(conn, :change_enterprise_plan, "123"))
|
||||||
user: user
|
assert redirected_to(conn) == Routes.billing_path(conn, :upgrade_to_enterprise_plan)
|
||||||
} 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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -127,7 +78,7 @@ defmodule PlausibleWeb.BillingControllerTest do
|
|||||||
test "calls Paddle API to update subscription", %{conn: conn, user: user} do
|
test "calls Paddle API to update subscription", %{conn: conn, user: user} do
|
||||||
insert(:subscription, user: user)
|
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)
|
subscription = Plausible.Repo.get_by(Plausible.Billing.Subscription, user_id: user.id)
|
||||||
assert subscription.paddle_plan_id == "123123"
|
assert subscription.paddle_plan_id == "123123"
|
||||||
@ -140,9 +91,238 @@ defmodule PlausibleWeb.BillingControllerTest do
|
|||||||
setup [:create_user, :log_in]
|
setup [:create_user, :log_in]
|
||||||
|
|
||||||
test "shows success page after user subscribes", %{conn: conn} do
|
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"
|
assert html_response(conn, 200) =~ "Your account is being upgraded"
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
@ -6,6 +6,7 @@ defmodule PlausibleWeb.SiteControllerTest do
|
|||||||
|
|
||||||
import ExUnit.CaptureLog
|
import ExUnit.CaptureLog
|
||||||
import Mox
|
import Mox
|
||||||
|
|
||||||
setup :verify_on_exit!
|
setup :verify_on_exit!
|
||||||
|
|
||||||
describe "GET /sites/new" do
|
describe "GET /sites/new" do
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
defmodule PlausibleWeb.Live.ChoosePlanTest do
|
defmodule PlausibleWeb.Live.ChoosePlanTest do
|
||||||
alias Plausible.{Repo, Billing.Subscription}
|
|
||||||
use PlausibleWeb.ConnCase, async: true
|
use PlausibleWeb.ConnCase, async: true
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
import Plausible.Test.Support.HTML
|
import Plausible.Test.Support.HTML
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.{Repo, Billing.Subscription}
|
||||||
|
|
||||||
@v1_10k_yearly_plan_id "572810"
|
@v1_10k_yearly_plan_id "572810"
|
||||||
@v4_growth_200k_yearly_plan_id "change-me-749347"
|
@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)
|
growth_checkout_button = find(doc, @growth_checkout_button)
|
||||||
|
|
||||||
assert text_of_attr(growth_checkout_button, "href") =~
|
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})
|
element(lv, @slider_input) |> render_change(%{slider: 6})
|
||||||
doc = element(lv, @monthly_interval_button) |> render_click()
|
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)
|
business_checkout_button = find(doc, @business_checkout_button)
|
||||||
|
|
||||||
assert text_of_attr(business_checkout_button, "href") =~
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -469,7 +470,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
|||||||
defp create_past_due_subscription(%{user: user}) do
|
defp create_past_due_subscription(%{user: user}) do
|
||||||
create_subscription_for(user,
|
create_subscription_for(user,
|
||||||
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
||||||
status: "past_due",
|
status: Subscription.Status.past_due(),
|
||||||
update_url: "https://update.billing.details"
|
update_url: "https://update.billing.details"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -477,7 +478,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
|||||||
defp create_paused_subscription(%{user: user}) do
|
defp create_paused_subscription(%{user: user}) do
|
||||||
create_subscription_for(user,
|
create_subscription_for(user,
|
||||||
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
||||||
status: "paused",
|
status: Subscription.Status.paused(),
|
||||||
update_url: "https://update.billing.details"
|
update_url: "https://update.billing.details"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -485,7 +486,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
|||||||
defp create_cancelled_subscription(%{user: user}) do
|
defp create_cancelled_subscription(%{user: user}) do
|
||||||
create_subscription_for(user,
|
create_subscription_for(user,
|
||||||
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
paddle_plan_id: @v4_growth_200k_yearly_plan_id,
|
||||||
status: "deleted"
|
status: Subscription.Status.deleted()
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -503,7 +504,7 @@ defmodule PlausibleWeb.Live.ChoosePlanTest do
|
|||||||
|
|
||||||
defp get_liveview(conn) do
|
defp get_liveview(conn) do
|
||||||
conn = assign(conn, :live_module, PlausibleWeb.Live.ChoosePlan)
|
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
|
end
|
||||||
|
|
||||||
defp get_paddle_checkout_params(element) do
|
defp get_paddle_checkout_params(element) do
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
defmodule Plausible.Factory do
|
defmodule Plausible.Factory do
|
||||||
use ExMachina.Ecto, repo: Plausible.Repo
|
use ExMachina.Ecto, repo: Plausible.Repo
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
def user_factory(attrs) do
|
def user_factory(attrs) do
|
||||||
pw = Map.get(attrs, :password, "password")
|
pw = Map.get(attrs, :password, "password")
|
||||||
@ -88,7 +90,7 @@ defmodule Plausible.Factory do
|
|||||||
paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"),
|
paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"),
|
||||||
cancel_url: "cancel.com",
|
cancel_url: "cancel.com",
|
||||||
update_url: "cancel.com",
|
update_url: "cancel.com",
|
||||||
status: "active",
|
status: Subscription.Status.active(),
|
||||||
next_bill_amount: "6.00",
|
next_bill_amount: "6.00",
|
||||||
next_bill_date: Timex.today(),
|
next_bill_date: Timex.today(),
|
||||||
last_bill_date: Timex.today(),
|
last_bill_date: Timex.today(),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
defmodule Plausible.Workers.LockSitesTest do
|
defmodule Plausible.Workers.LockSitesTest do
|
||||||
use Plausible.DataCase, async: true
|
use Plausible.DataCase, async: true
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
alias Plausible.Workers.LockSites
|
alias Plausible.Workers.LockSites
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
test "does not lock enterprise site on grace period" do
|
test "does not lock enterprise site on grace period" do
|
||||||
user =
|
user =
|
||||||
@ -36,7 +38,7 @@ defmodule Plausible.Workers.LockSitesTest do
|
|||||||
|
|
||||||
test "does not lock active subsriber's sites" do
|
test "does not lock active subsriber's sites" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "active", user: user)
|
insert(:subscription, status: Subscription.Status.active(), user: user)
|
||||||
site = insert(:site, members: [user])
|
site = insert(:site, members: [user])
|
||||||
|
|
||||||
LockSites.perform(nil)
|
LockSites.perform(nil)
|
||||||
@ -46,7 +48,7 @@ defmodule Plausible.Workers.LockSitesTest do
|
|||||||
|
|
||||||
test "does not lock user who is past due" do
|
test "does not lock user who is past due" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "past_due", user: user)
|
insert(:subscription, status: Subscription.Status.past_due(), user: user)
|
||||||
site = insert(:site, members: [user])
|
site = insert(:site, members: [user])
|
||||||
|
|
||||||
LockSites.perform(nil)
|
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
|
test "does not lock user who cancelled subscription but it hasn't expired yet" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
insert(:subscription, status: "deleted", user: user)
|
insert(:subscription, status: Subscription.Status.deleted(), user: user)
|
||||||
site = insert(:site, members: [user])
|
site = insert(:site, members: [user])
|
||||||
|
|
||||||
LockSites.perform(nil)
|
LockSites.perform(nil)
|
||||||
@ -68,7 +70,7 @@ defmodule Plausible.Workers.LockSitesTest do
|
|||||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||||
|
|
||||||
insert(:subscription,
|
insert(:subscription,
|
||||||
status: "deleted",
|
status: Subscription.Status.deleted(),
|
||||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||||
user: user
|
user: user
|
||||||
)
|
)
|
||||||
@ -84,13 +86,13 @@ defmodule Plausible.Workers.LockSitesTest do
|
|||||||
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
user = insert(:user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1))
|
||||||
|
|
||||||
insert(:subscription,
|
insert(:subscription,
|
||||||
status: "deleted",
|
status: Subscription.Status.deleted(),
|
||||||
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
next_bill_date: Timex.today() |> Timex.shift(days: -1),
|
||||||
user: user,
|
user: user,
|
||||||
inserted_at: Timex.now() |> Timex.shift(days: -1)
|
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])
|
site = insert(:site, members: [user])
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
defmodule Plausible.Workers.NotifyAnnualRenewalTest do
|
defmodule Plausible.Workers.NotifyAnnualRenewalTest do
|
||||||
use Plausible.DataCase, async: true
|
use Plausible.DataCase, async: true
|
||||||
use Bamboo.Test
|
use Bamboo.Test
|
||||||
|
require Plausible.Billing.Subscription.Status
|
||||||
alias Plausible.Workers.NotifyAnnualRenewal
|
alias Plausible.Workers.NotifyAnnualRenewal
|
||||||
|
alias Plausible.Billing.Subscription
|
||||||
|
|
||||||
setup [:create_user, :create_site]
|
setup [:create_user, :create_site]
|
||||||
@monthly_plan "558018"
|
@monthly_plan "558018"
|
||||||
@ -185,7 +186,7 @@ defmodule Plausible.Workers.NotifyAnnualRenewalTest do
|
|||||||
user: user,
|
user: user,
|
||||||
paddle_plan_id: @yearly_plan,
|
paddle_plan_id: @yearly_plan,
|
||||||
next_bill_date: Timex.shift(Timex.today(), days: 7),
|
next_bill_date: Timex.shift(Timex.today(), days: 7),
|
||||||
status: "deleted"
|
status: Subscription.Status.deleted()
|
||||||
)
|
)
|
||||||
|
|
||||||
NotifyAnnualRenewal.perform(nil)
|
NotifyAnnualRenewal.perform(nil)
|
||||||
|
@ -2,7 +2,6 @@ defmodule Plausible.Workers.SendTrialNotificationsTest do
|
|||||||
use Plausible.DataCase
|
use Plausible.DataCase
|
||||||
use Bamboo.Test
|
use Bamboo.Test
|
||||||
use Oban.Testing, repo: Plausible.Repo
|
use Oban.Testing, repo: Plausible.Repo
|
||||||
|
|
||||||
alias Plausible.Workers.SendTrialNotifications
|
alias Plausible.Workers.SendTrialNotifications
|
||||||
|
|
||||||
test "does not send a notification if user didn't create a site" do
|
test "does not send a notification if user didn't create a site" do
|
||||||
|
Loading…
Reference in New Issue
Block a user