Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
]
]
]

Expand Down
2 changes: 1 addition & 1 deletion lib/plausible/auth/api_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Plausible.Auth.ApiKey do
schema "api_keys" do
field :name, :string
field :scopes, {:array, :string}, default: ["stats:read:*"]
field :hourly_request_limit, :integer, default: 1000
field :hourly_request_limit, :integer, default: 600

field :key, :string, virtual: true
field :key_hash, :string
Expand Down
1 change: 1 addition & 0 deletions lib/plausible/auth/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 24 additions & 2 deletions lib/plausible/billing/billing.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ defmodule Plausible.Billing do

changeset = Subscription.changeset(%Subscription{}, format_subscription(params))

Repo.insert(changeset) |> check_lock_status
Repo.insert(changeset)
|> check_lock_status
|> maybe_adjust_api_key_limits
end

def subscription_updated(params) do
subscription = Repo.get_by!(Subscription, paddle_subscription_id: params["subscription_id"])
changeset = Subscription.changeset(subscription, format_subscription(params))

Repo.update(changeset) |> check_lock_status
Repo.update(changeset)
|> check_lock_status
|> maybe_adjust_api_key_limits
end

def subscription_cancelled(params) do
Expand Down Expand Up @@ -236,5 +240,23 @@ defmodule Plausible.Billing do

defp check_lock_status(err), do: err

defp maybe_adjust_api_key_limits({:ok, subscription}) do
plan =
Repo.get_by(Plausible.Billing.EnterprisePlan,
user_id: subscription.user_id,
paddle_plan_id: subscription.paddle_plan_id
)

if plan do
user_id = subscription.user_id
api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id == ^user_id)
Repo.update_all(api_keys, set: [hourly_request_limit: plan.hourly_api_request_limit])
end

{:ok, subscription}
end

defp maybe_adjust_api_key_limits(err), do: err

defp paddle_api(), do: Application.fetch_env!(:plausible, :paddle_api)
end
30 changes: 30 additions & 0 deletions lib/plausible/billing/enterprise_plan.ex
Original file line number Diff line number Diff line change
@@ -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 changes: 37 additions & 0 deletions lib/plausible/billing/enterprise_plan_admin.ex
Original file line number Diff line number Diff line change
@@ -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
21 changes: 11 additions & 10 deletions lib/plausible/billing/plans.ex
Original file line number Diff line number Diff line change
@@ -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"}
]
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
12 changes: 12 additions & 0 deletions lib/plausible/mailer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
153 changes: 110 additions & 43 deletions lib/plausible_web/controllers/billing_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Loading