mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
Add enterprise plans
This commit is contained in:
parent
fa1e39133d
commit
6a5b383e2b
@ -377,6 +377,14 @@ config :kaffy,
|
||||
resources: [
|
||||
site: [schema: Plausible.Site, admin: Plausible.SiteAdmin]
|
||||
]
|
||||
],
|
||||
billing: [
|
||||
resources: [
|
||||
enterprise_plan: [
|
||||
schema: Plausible.Billing.EnterprisePlan,
|
||||
admin: Plausible.Billing.EnterprisePlanAdmin
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
@ -25,6 +25,7 @@ defmodule Plausible.Auth.User do
|
||||
has_many :api_keys, Plausible.Auth.ApiKey
|
||||
has_one :google_auth, Plausible.Site.GoogleAuth
|
||||
has_one :subscription, Plausible.Billing.Subscription
|
||||
has_one :enterprise_plan, Plausible.Billing.EnterprisePlan
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
30
lib/plausible/billing/enterprise_plan.ex
Normal file
30
lib/plausible/billing/enterprise_plan.ex
Normal file
@ -0,0 +1,30 @@
|
||||
defmodule Plausible.Billing.EnterprisePlan do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
@required_fields [
|
||||
:user_id,
|
||||
:paddle_plan_id,
|
||||
:billing_interval,
|
||||
:monthly_pageview_limit,
|
||||
:hourly_api_request_limit
|
||||
]
|
||||
|
||||
schema "enterprise_plans" do
|
||||
field :paddle_plan_id, :string
|
||||
field :billing_interval, Ecto.Enum, values: [:monthly, :yearly]
|
||||
field :monthly_pageview_limit, :integer
|
||||
field :hourly_api_request_limit, :integer
|
||||
|
||||
belongs_to :user, Plausible.Auth.User
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(model, attrs \\ %{}) do
|
||||
model
|
||||
|> cast(attrs, @required_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> unique_constraint(:user_id)
|
||||
end
|
||||
end
|
37
lib/plausible/billing/enterprise_plan_admin.ex
Normal file
37
lib/plausible/billing/enterprise_plan_admin.ex
Normal file
@ -0,0 +1,37 @@
|
||||
defmodule Plausible.Billing.EnterprisePlanAdmin do
|
||||
use Plausible.Repo
|
||||
|
||||
def search_fields(_schema) do
|
||||
[
|
||||
:paddle_plan_id,
|
||||
user: [:name, :email]
|
||||
]
|
||||
end
|
||||
|
||||
def form_fields(_) do
|
||||
[
|
||||
user_id: nil,
|
||||
paddle_plan_id: nil,
|
||||
billing_interval: %{choices: [{"Yearly", "yearly"}, {"Monthly", "monthly"}]},
|
||||
monthly_pageview_limit: nil,
|
||||
hourly_api_request_limit: nil
|
||||
]
|
||||
end
|
||||
|
||||
def custom_index_query(_conn, _schema, query) do
|
||||
from(r in query, preload: :user)
|
||||
end
|
||||
|
||||
def index(_) do
|
||||
[
|
||||
id: nil,
|
||||
user_email: %{value: &get_user_email/1},
|
||||
paddle_plan_id: nil,
|
||||
billing_interval: nil,
|
||||
monthly_pageview_limit: nil,
|
||||
hourly_api_request_limit: nil
|
||||
]
|
||||
end
|
||||
|
||||
defp get_user_email(plan), do: plan.user.email
|
||||
end
|
@ -1,4 +1,6 @@
|
||||
defmodule Plausible.Billing.Plans do
|
||||
use Plausible.Repo
|
||||
|
||||
@unlisted_plans_v1 [
|
||||
%{limit: 150_000_000, yearly_product_id: "648089", yearly_cost: "$4800"}
|
||||
]
|
||||
@ -30,21 +32,15 @@ defmodule Plausible.Billing.Plans do
|
||||
end)
|
||||
end
|
||||
|
||||
def subscription_quota("free_10k"), do: "10k"
|
||||
|
||||
def subscription_quota(product_id) do
|
||||
case for_product_id(product_id) do
|
||||
nil -> raise "Unknown quota for subscription #{product_id}"
|
||||
product -> number_format(product[:limit])
|
||||
end
|
||||
end
|
||||
|
||||
def subscription_interval("free_10k"), do: "N/A"
|
||||
|
||||
def subscription_interval(product_id) do
|
||||
case for_product_id(product_id) do
|
||||
nil ->
|
||||
raise "Unknown interval for subscription #{product_id}"
|
||||
enterprise_plan =
|
||||
Repo.get_by(Plausible.Billing.EnterprisePlan, paddle_plan_id: product_id)
|
||||
|
||||
enterprise_plan && enterprise_plan.billing_interval
|
||||
|
||||
plan ->
|
||||
if product_id == plan[:monthly_product_id] do
|
||||
@ -62,6 +58,11 @@ defmodule Plausible.Billing.Plans do
|
||||
|
||||
if found do
|
||||
Map.fetch!(found, :limit)
|
||||
else
|
||||
enterprise_plan =
|
||||
Repo.get_by(Plausible.Billing.EnterprisePlan, paddle_plan_id: subscription.paddle_plan_id)
|
||||
|
||||
enterprise_plan && enterprise_plan.monthly_pageview_limit
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -14,4 +14,16 @@ defmodule Plausible.Mailer do
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
def send_email_safe(email) do
|
||||
try do
|
||||
Plausible.Mailer.deliver_now!(email)
|
||||
rescue
|
||||
error ->
|
||||
Sentry.capture_exception(error,
|
||||
stacktrace: __STACKTRACE__,
|
||||
extra: %{extra: "Error while sending email"}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -6,20 +6,122 @@ defmodule PlausibleWeb.BillingController do
|
||||
|
||||
plug PlausibleWeb.RequireAccountPlug
|
||||
|
||||
def admin_email do
|
||||
Application.get_env(:plausible, :admin_email)
|
||||
def upgrade(conn, _params) do
|
||||
user =
|
||||
conn.assigns[:current_user]
|
||||
|> Repo.preload(:enterprise_plan)
|
||||
|
||||
cond do
|
||||
user.subscription && user.subscription.status == "active" ->
|
||||
redirect(conn, to: Routes.billing_path(conn, :change_plan_form))
|
||||
|
||||
user.enterprise_plan ->
|
||||
redirect(conn,
|
||||
to: Routes.billing_path(conn, :upgrade_enterprise_plan, user.enterprise_plan.id)
|
||||
)
|
||||
|
||||
true ->
|
||||
render(conn, "upgrade.html",
|
||||
usage: Plausible.Billing.usage(user),
|
||||
user: user,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def change_plan_form(conn, _params) do
|
||||
subscription = Billing.active_subscription_for(conn.assigns[:current_user].id)
|
||||
def upgrade_enterprise_plan(conn, %{"plan_id" => plan_id}) do
|
||||
user =
|
||||
conn.assigns[:current_user]
|
||||
|> Repo.preload(:enterprise_plan)
|
||||
|
||||
if subscription do
|
||||
render(conn, "change_plan.html",
|
||||
subscription: subscription,
|
||||
if user.enterprise_plan && user.enterprise_plan.id == String.to_integer(plan_id) do
|
||||
usage = Plausible.Billing.usage(conn.assigns[:current_user])
|
||||
|
||||
render(conn, "upgrade_to_plan.html",
|
||||
usage: usage,
|
||||
user: user,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
else
|
||||
redirect(conn, to: "/billing/upgrade")
|
||||
render_error(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
def upgrade_to_plan(conn, %{"plan_id" => plan_id}) do
|
||||
plan = Plausible.Billing.Plans.for_product_id(plan_id)
|
||||
|
||||
if plan do
|
||||
cycle = if plan[:monthly_product_id] == plan_id, do: "monthly", else: "yearly"
|
||||
plan = Map.merge(plan, %{cycle: cycle, product_id: plan_id})
|
||||
usage = Plausible.Billing.usage(conn.assigns[:current_user])
|
||||
|
||||
render(conn, "upgrade_to_plan.html",
|
||||
usage: usage,
|
||||
plan: plan,
|
||||
user: conn.assigns[:current_user],
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
else
|
||||
render_error(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
def upgrade_success(conn, _params) do
|
||||
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
|
||||
end
|
||||
|
||||
def change_plan_form(conn, _params) do
|
||||
user =
|
||||
conn.assigns[:current_user]
|
||||
|> Repo.preload(:enterprise_plan)
|
||||
|
||||
subscription = Billing.active_subscription_for(user.id)
|
||||
|
||||
cond do
|
||||
subscription && user.enterprise_plan ->
|
||||
redirect(conn,
|
||||
to: Routes.billing_path(conn, :change_enterprise_plan, user.enterprise_plan.id)
|
||||
)
|
||||
|
||||
subscription ->
|
||||
render(conn, "change_plan.html",
|
||||
subscription: subscription,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
|
||||
true ->
|
||||
redirect(conn, to: Routes.billing_path(conn, :upgrade))
|
||||
end
|
||||
end
|
||||
|
||||
def change_enterprise_plan(conn, %{"plan_id" => plan_id}) do
|
||||
user =
|
||||
conn.assigns[:current_user]
|
||||
|> Repo.preload(:enterprise_plan)
|
||||
|
||||
cond do
|
||||
is_nil(user.subscription) ->
|
||||
redirect(conn, to: "/billing/upgrade")
|
||||
|
||||
is_nil(user.enterprise_plan) ->
|
||||
render_error(conn, 404)
|
||||
|
||||
user.enterprise_plan.id !== String.to_integer(plan_id) ->
|
||||
render_error(conn, 404)
|
||||
|
||||
user.enterprise_plan.paddle_plan_id == user.subscription.paddle_plan_id ->
|
||||
render(conn, "change_enterprise_plan_contact_us.html",
|
||||
user: user,
|
||||
plan: user.enterprise_plan,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
|
||||
true ->
|
||||
render(conn, "change_enterprise_plan.html",
|
||||
user: user,
|
||||
plan: user.enterprise_plan,
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@ -77,39 +179,4 @@ defmodule PlausibleWeb.BillingController do
|
||||
|> redirect(to: "/settings")
|
||||
end
|
||||
end
|
||||
|
||||
def upgrade(conn, _params) do
|
||||
usage = Plausible.Billing.usage(conn.assigns[:current_user])
|
||||
today = Timex.today()
|
||||
|
||||
render(conn, "upgrade.html",
|
||||
usage: usage,
|
||||
today: today,
|
||||
user: conn.assigns[:current_user],
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
end
|
||||
|
||||
def upgrade_to_plan(conn, %{"plan_id" => plan_id}) do
|
||||
plan = Plausible.Billing.Plans.for_product_id(plan_id)
|
||||
|
||||
if plan do
|
||||
cycle = if plan[:monthly_product_id] == plan_id, do: "monthly", else: "yearly"
|
||||
plan = Map.merge(plan, %{cycle: cycle, product_id: plan_id})
|
||||
usage = Plausible.Billing.usage(conn.assigns[:current_user])
|
||||
|
||||
render(conn, "upgrade_to_plan.html",
|
||||
usage: usage,
|
||||
plan: plan,
|
||||
user: conn.assigns[:current_user],
|
||||
layout: {PlausibleWeb.LayoutView, "focus.html"}
|
||||
)
|
||||
else
|
||||
render_error(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
def upgrade_success(conn, _params) do
|
||||
render(conn, "upgrade_success.html", layout: {PlausibleWeb.LayoutView, "focus.html"})
|
||||
end
|
||||
end
|
||||
|
@ -119,7 +119,7 @@ defmodule PlausibleWeb.Email do
|
||||
base_email()
|
||||
|> to(user)
|
||||
|> tag("over-limit")
|
||||
|> subject("You have outgrown your Plausible subscription tier ")
|
||||
|> subject("You have outgrown your Plausible subscription tier")
|
||||
|> render("over_limit.html", %{
|
||||
user: user,
|
||||
usage: usage,
|
||||
@ -128,6 +128,18 @@ defmodule PlausibleWeb.Email do
|
||||
})
|
||||
end
|
||||
|
||||
def enterprise_over_limit_email(user, usage, last_cycle) do
|
||||
base_email()
|
||||
|> to("enterprise@plausible.io")
|
||||
|> tag("enterprise-over-limit")
|
||||
|> subject("#{user.email} has outgrown their enterprise plan")
|
||||
|> render("enterprise_over_limit.html", %{
|
||||
user: user,
|
||||
usage: usage,
|
||||
last_cycle: last_cycle
|
||||
})
|
||||
end
|
||||
|
||||
def yearly_renewal_notification(user) do
|
||||
date = Timex.format!(user.subscription.next_bill_date, "{Mfull} {D}, {YYYY}")
|
||||
|
||||
|
@ -147,6 +147,8 @@ defmodule PlausibleWeb.Router do
|
||||
post "/billing/change-plan/:new_plan_id", BillingController, :change_plan
|
||||
get "/billing/upgrade", BillingController, :upgrade
|
||||
get "/billing/upgrade/:plan_id", BillingController, :upgrade_to_plan
|
||||
get "/billing/upgrade/enterprise/:plan_id", BillingController, :upgrade_enterprise_plan
|
||||
get "/billing/change-plan/enterprise/:plan_id", BillingController, :change_enterprise_plan
|
||||
get "/billing/upgrade-success", BillingController, :upgrade_success
|
||||
|
||||
get "/sites", SiteController, :index
|
||||
|
@ -0,0 +1,68 @@
|
||||
<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('https://checkout.paddle.com/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.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? Contact <%= link("support@plausible.io", to: "mailto: support@plausible.io", class: "text-indigo-500") %>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://cdn.paddle.com/paddle/paddle.js"></script>
|
||||
<script>Paddle.Setup({vendor: 49430})</script>
|
@ -0,0 +1,16 @@
|
||||
<div class="mx-auto mt-6 text-center">
|
||||
<h1 class="text-3xl font-black dark:text-gray-100">Change subscription plan</h1>
|
||||
</div>
|
||||
|
||||
<div class="w-full max-w-lg px-4 mx-auto mt-4">
|
||||
<div class="flex-1 p-8 mt-8 bg-white rounded shadow-md dark:bg-gray-800">
|
||||
<div class="w-full pb-4 dark:text-gray-100">
|
||||
<span>Need to change your limits?</span>
|
||||
</div>
|
||||
|
||||
<ul class="w-full py-4 dark:text-gray-100">
|
||||
<span>Your account is on an enterprise plan. If you want to increase or decrease the limits on your account, please contact us at enterprise@plausible.io</span>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
@ -36,7 +36,7 @@
|
||||
|
||||
<div class="pt-6"></div>
|
||||
|
||||
<div class="py-2 text-lg font-bold">Next payment</div>
|
||||
<div class="py-4 dark:text-gray-100 text-lg font-bold">Next payment</div>
|
||||
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
@ -25,7 +25,6 @@
|
||||
'GBP': '£'
|
||||
}[plan.currency]
|
||||
|
||||
console.log(plan)
|
||||
return currency + plan.price.net
|
||||
},
|
||||
fetchPlans() {
|
||||
|
@ -0,0 +1,44 @@
|
||||
<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>
|
||||
<div class="flex flex-col w-full max-w-4xl px-4 mx-auto mt-4 md:flex-row">
|
||||
<div class="flex-1 px-8 py-4 mt-8 mb-4 bg-white rounded shadow-md dark:bg-gray-800">
|
||||
<div class="w-full py-4 dark:text-gray-100">
|
||||
<span>You've used <b><%= PlausibleWeb.AuthView.delimit_integer(@usage) %></b> billable pageviews in the last 30 days</span>
|
||||
</div>
|
||||
|
||||
<div class="w-full py-4 dark:text-gray-100">
|
||||
<span>With this link you can upgrade to an enterprise plan with <b><%= PlausibleWeb.StatsView.large_number_format(@plan[:limit]) %> monthly pageviews</b></span>, billed on a <%= @plan[:cycle] %> basis.
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-left">
|
||||
<span class="inline-flex w-full rounded-md shadow-sm">
|
||||
<button type="button" data-theme="none" data-product="<%= @plan[:product_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="items-center button paddle_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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 pl-8 pt-14">
|
||||
<h3 class="text-lg font-medium text-gray-900 leading-6 dark:text-gray-100">
|
||||
What happens if I go over my page views limit?
|
||||
</h3>
|
||||
<div class="mt-2 text-base text-gray-500 leading-6 dark:text-gray-200">
|
||||
You will never be charged extra for an occasional traffic spike. There are no surprise fees and your card will never be charged unexpectedly.<br /><br />
|
||||
If your page views exceed your plan for two consecutive months, we will contact you to upgrade to a higher plan for the following month. You will have two weeks to make a decision. You can decide to continue with a higher plan or to cancel your account at that point.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-center dark:text-gray-100">
|
||||
Questions? Contact <%= link("support@plausible.io", to: "mailto: support@plausible.io", class: "text-indigo-500") %>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="https://cdn.paddle.com/paddle/paddle.js"></script>
|
||||
<script>Paddle.Setup({vendor: 49430})</script>
|
@ -10,12 +10,12 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full py-4 dark:text-gray-100">
|
||||
<span>With this link you can upgrade to a plan with <b><%= PlausibleWeb.StatsView.large_number_format(@plan[:limit]) %> monthly pageviews</b></span>, billed on a <%= @plan[:cycle] %> basis.
|
||||
<span>With this link you can upgrade to an enterprise plan with <b><%= PlausibleWeb.StatsView.large_number_format(@user.enterprise_plan.monthly_pageview_limit) %> monthly pageviews</b></span>, billed on a <%= @user.enterprise_plan.billing_interval %> basis.
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-left">
|
||||
<span class="inline-flex w-full rounded-md shadow-sm">
|
||||
<button type="button" data-theme="none" data-product="<%= @plan[:product_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="items-center button paddle_button">
|
||||
<button type="button" data-theme="none" data-product="<%= @user.enterprise_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="items-center button paddle_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>
|
||||
|
@ -0,0 +1,8 @@
|
||||
Automated notice about an account that has gone over their enteprise plan limit.
|
||||
|
||||
Customer email: <% @user.email %>
|
||||
Last billing cycle: <%= date_format(@last_cycle.first) %> to <%= date_format(@last_cycle.last) %>
|
||||
Usage: <%= PlausibleWeb.StatsView.large_number_format(@usage) %> billable pageviews
|
||||
|
||||
--<br />
|
||||
<%= plausible_url() %><br />
|
@ -38,31 +38,47 @@ defmodule Plausible.Workers.CheckUsage do
|
||||
from u in Plausible.Auth.User,
|
||||
join: s in Plausible.Billing.Subscription,
|
||||
on: s.user_id == u.id,
|
||||
left_join: ep in Plausible.Billing.EnterprisePlan,
|
||||
on: ep.user_id == u.id,
|
||||
where: s.status == "active",
|
||||
where: not is_nil(s.last_bill_date),
|
||||
# Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01.
|
||||
where:
|
||||
least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) ==
|
||||
day_of_month(^yesterday),
|
||||
preload: [subscription: s]
|
||||
preload: [subscription: s, enterprise_plan: ep]
|
||||
)
|
||||
|
||||
for subscriber <- active_subscribers do
|
||||
allowance = Plausible.Billing.Plans.allowance(subscriber.subscription)
|
||||
{last_last_month, last_month} = billing_mod.last_two_billing_months_usage(subscriber)
|
||||
is_over_limit = last_last_month > allowance && last_month > allowance
|
||||
|
||||
if last_last_month > allowance && last_month > allowance do
|
||||
{_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber)
|
||||
suggested_plan = Plausible.Billing.Plans.suggested_plan(subscriber, last_month)
|
||||
cond do
|
||||
is_over_limit && subscriber.enterprise_plan ->
|
||||
{_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber)
|
||||
|
||||
template =
|
||||
PlausibleWeb.Email.over_limit_email(subscriber, last_month, last_cycle, suggested_plan)
|
||||
template =
|
||||
PlausibleWeb.Email.enterprise_over_limit_email(subscriber, last_month, last_cycle)
|
||||
|
||||
try do
|
||||
Plausible.Mailer.send_email(template)
|
||||
rescue
|
||||
_ -> nil
|
||||
end
|
||||
Plausible.Mailer.send_email_safe(template)
|
||||
|
||||
is_over_limit ->
|
||||
{_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber)
|
||||
suggested_plan = Plausible.Billing.Plans.suggested_plan(subscriber, last_month)
|
||||
|
||||
template =
|
||||
PlausibleWeb.Email.over_limit_email(
|
||||
subscriber,
|
||||
last_month,
|
||||
last_cycle,
|
||||
suggested_plan
|
||||
)
|
||||
|
||||
Plausible.Mailer.send_email_safe(template)
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
19
priv/repo/migrations/20211020093238_add_enterprise_plans.exs
Normal file
19
priv/repo/migrations/20211020093238_add_enterprise_plans.exs
Normal file
@ -0,0 +1,19 @@
|
||||
defmodule Plausible.Repo.Migrations.AddEnterprisePlans do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create_query = "CREATE TYPE billing_interval AS ENUM ('monthly', 'yearly')"
|
||||
drop_query = "DROP TYPE billing_interval"
|
||||
execute(create_query, drop_query)
|
||||
|
||||
create table(:enterprise_plans) do
|
||||
add :user_id, references(:users), null: false, unique: true
|
||||
add :paddle_plan_id, :string, null: false
|
||||
add :billing_interval, :billing_interval, null: false
|
||||
add :monthly_pageview_limit, :integer, null: false
|
||||
add :hourly_api_request_limit, :integer, null: false
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
end
|
@ -2,6 +2,46 @@ defmodule PlausibleWeb.BillingControllerTest do
|
||||
use PlausibleWeb.ConnCase
|
||||
import Plausible.TestUtils
|
||||
|
||||
describe "GET /upgrade" do
|
||||
setup [:create_user, :log_in]
|
||||
|
||||
test "shows upgrade page when user does not have a subcription already", %{conn: conn} do
|
||||
conn = get(conn, "/billing/upgrade")
|
||||
|
||||
assert html_response(conn, 200) =~ "Upgrade your free trial"
|
||||
end
|
||||
|
||||
test "redirects user to change plan if they already have a plan", %{conn: conn, user: user} do
|
||||
insert(:subscription, user: user)
|
||||
conn = get(conn, "/billing/upgrade")
|
||||
|
||||
assert redirected_to(conn) == "/billing/change-plan"
|
||||
end
|
||||
|
||||
test "redirects user to enteprise plan page if they are configured with one", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
plan = insert(:enterprise_plan, user: user)
|
||||
conn = get(conn, "/billing/upgrade")
|
||||
|
||||
assert redirected_to(conn) == "/billing/upgrade/enterprise/#{plan.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /upgrade/enterprise/:plan_id" do
|
||||
setup [:create_user, :log_in]
|
||||
|
||||
test "renders enteprise plan upgrade page", %{conn: conn, user: user} do
|
||||
plan = insert(:enterprise_plan, user: user)
|
||||
|
||||
conn = get(conn, "/billing/upgrade/enterprise/#{plan.id}")
|
||||
|
||||
assert html_response(conn, 200) =~ "Upgrade your free trial"
|
||||
assert html_response(conn, 200) =~ "enterprise plan"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /change-plan" do
|
||||
setup [:create_user, :log_in]
|
||||
|
||||
@ -17,6 +57,37 @@ defmodule PlausibleWeb.BillingControllerTest do
|
||||
|
||||
assert redirected_to(conn) == "/billing/upgrade"
|
||||
end
|
||||
|
||||
test "redirects to enterprise change plan page if user has enterprise plan and existing subscription",
|
||||
%{conn: conn, user: user} do
|
||||
insert(:subscription, user: user)
|
||||
plan = insert(:enterprise_plan, user: user)
|
||||
conn = get(conn, "/billing/change-plan")
|
||||
|
||||
assert redirected_to(conn) == "/billing/change-plan/enterprise/#{plan.id}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /change-plan/enterprise/:plan_id" do
|
||||
setup [:create_user, :log_in]
|
||||
|
||||
test "shows change plan page if user has subsription and enterprise plan", %{
|
||||
conn: conn,
|
||||
user: user
|
||||
} do
|
||||
insert(:subscription, user: user)
|
||||
plan = insert(:enterprise_plan, user: user)
|
||||
conn = get(conn, "/billing/change-plan/enterprise/#{plan.id}")
|
||||
|
||||
assert html_response(conn, 200) =~ "Change subscription plan"
|
||||
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
|
||||
|
||||
describe "POST /change-plan" do
|
||||
|
@ -116,6 +116,15 @@ defmodule Plausible.Factory do
|
||||
}
|
||||
end
|
||||
|
||||
def enterprise_plan_factory do
|
||||
%Plausible.Billing.EnterprisePlan{
|
||||
paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"),
|
||||
billing_interval: :monthly,
|
||||
monthly_pageview_limit: 1_000_000,
|
||||
hourly_api_request_limit: 3000
|
||||
}
|
||||
end
|
||||
|
||||
def google_auth_factory do
|
||||
%Plausible.Site.GoogleAuth{
|
||||
email: sequence(:google_auth_email, &"email-#{&1}@email.com"),
|
||||
|
@ -64,7 +64,34 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [user],
|
||||
subject: "You have outgrown your Plausible subscription tier "
|
||||
subject: "You have outgrown your Plausible subscription tier"
|
||||
)
|
||||
end
|
||||
|
||||
test "checks usage for enterprise customer, sends usage information to enterprise@plausible.io",
|
||||
%{
|
||||
user: user
|
||||
} do
|
||||
billing_stub =
|
||||
Plausible.Billing
|
||||
|> stub(:last_two_billing_months_usage, fn _user -> {1_100_000, 1_100_000} end)
|
||||
|> stub(:last_two_billing_cycles, fn _user ->
|
||||
{Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())}
|
||||
end)
|
||||
|
||||
enterprise_plan = insert(:enterprise_plan, user: user, monthly_pageview_limit: 1_000_000)
|
||||
|
||||
insert(:subscription,
|
||||
user: user,
|
||||
paddle_plan_id: enterprise_plan.paddle_plan_id,
|
||||
last_bill_date: Timex.shift(Timex.today(), days: -1)
|
||||
)
|
||||
|
||||
CheckUsage.perform(nil, billing_stub)
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [{nil, "enterprise@plausible.io"}],
|
||||
subject: "#{user.email} has outgrown their enterprise plan"
|
||||
)
|
||||
end
|
||||
|
||||
@ -89,7 +116,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [user],
|
||||
subject: "You have outgrown your Plausible subscription tier "
|
||||
subject: "You have outgrown your Plausible subscription tier"
|
||||
)
|
||||
end
|
||||
|
||||
@ -135,7 +162,7 @@ defmodule Plausible.Workers.CheckUsageTest do
|
||||
|
||||
assert_email_delivered_with(
|
||||
to: [user],
|
||||
subject: "You have outgrown your Plausible subscription tier "
|
||||
subject: "You have outgrown your Plausible subscription tier"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user