diff --git a/lib/plausible/billing/enterprise_plan.ex b/lib/plausible/billing/enterprise_plan.ex index 65141098323636619f0af1119bb7b296a48296e1..cfae0d4ff7f2bcb85aad21ead11f6c65223c1aab 100644 --- a/lib/plausible/billing/enterprise_plan.ex +++ b/lib/plausible/billing/enterprise_plan.ex @@ -7,7 +7,8 @@ defmodule Plausible.Billing.EnterprisePlan do :paddle_plan_id, :billing_interval, :monthly_pageview_limit, - :hourly_api_request_limit + :hourly_api_request_limit, + :site_limit ] schema "enterprise_plans" do @@ -15,6 +16,7 @@ defmodule Plausible.Billing.EnterprisePlan do field :billing_interval, Ecto.Enum, values: [:monthly, :yearly] field :monthly_pageview_limit, :integer field :hourly_api_request_limit, :integer + field :site_limit, :integer belongs_to :user, Plausible.Auth.User diff --git a/lib/plausible/billing/enterprise_plan_admin.ex b/lib/plausible/billing/enterprise_plan_admin.ex index 616eea5b66a055d6ea2d30eaf7e5bc21f1d4b227..08c396732863585c711bec2aec6d6a380c6c2a44 100644 --- a/lib/plausible/billing/enterprise_plan_admin.ex +++ b/lib/plausible/billing/enterprise_plan_admin.ex @@ -14,7 +14,8 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do paddle_plan_id: nil, billing_interval: %{choices: [{"Yearly", "yearly"}, {"Monthly", "monthly"}]}, monthly_pageview_limit: nil, - hourly_api_request_limit: nil + hourly_api_request_limit: nil, + site_limit: nil ] end @@ -29,7 +30,8 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do paddle_plan_id: nil, billing_interval: nil, monthly_pageview_limit: nil, - hourly_api_request_limit: nil + hourly_api_request_limit: nil, + site_limit: nil ] end diff --git a/lib/plausible/sites.ex b/lib/plausible/sites.ex index a57df0b487040baf940c6be6ca017742dbcb4f0f..555ee1088bbb52bbc35722917cd4a5efc21ac059 100644 --- a/lib/plausible/sites.ex +++ b/lib/plausible/sites.ex @@ -124,6 +124,17 @@ defmodule Plausible.Sites do ) end + def count_owned_by(user) do + Repo.one( + from s in Plausible.Site, + join: sm in Plausible.Site.Membership, + on: sm.site_id == s.id, + where: sm.role == :owner, + where: sm.user_id == ^user.id, + select: count(sm) + ) + end + def owner_for(site) do Repo.one( from u in Plausible.Auth.User, diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex index d2f6fee7bb9f2b269d2ecfdb43aea00449d93647..698282ab5b0fe293dde478b6b618ed2fb0223e83 100644 --- a/lib/plausible_web/email.ex +++ b/lib/plausible_web/email.ex @@ -128,7 +128,7 @@ defmodule PlausibleWeb.Email do }) end - def enterprise_over_limit_email(user, usage, last_cycle) do + def enterprise_over_limit_email(user, usage, last_cycle, site_usage, site_allowance) do base_email() |> to("enterprise@plausible.io") |> tag("enterprise-over-limit") @@ -136,7 +136,9 @@ defmodule PlausibleWeb.Email do |> render("enterprise_over_limit.html", %{ user: user, usage: usage, - last_cycle: last_cycle + last_cycle: last_cycle, + site_usage: site_usage, + site_allowance: site_allowance }) end diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 233a814ce7dd7f8a8de215f23937b7efebcf4aef..0b95355343a6ca21eaae6fd07a6077fe9b8ca8af 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -41,7 +41,7 @@ defmodule PlausibleWeb.Router do plug PlausibleWeb.Firewall end - if Application.get_env(:plausible, :environment) == "dev" do + if Mix.env() == :dev do forward "/sent-emails", Bamboo.SentEmailViewerPlug end diff --git a/lib/plausible_web/templates/email/enterprise_over_limit.html.eex b/lib/plausible_web/templates/email/enterprise_over_limit.html.eex index 6fea4741b28a6e79d8c7aeedfb6f3f96e7341543..af82475e05f5b9248c346ffcb3b1fa84bf1696a0 100644 --- a/lib/plausible_web/templates/email/enterprise_over_limit.html.eex +++ b/lib/plausible_web/templates/email/enterprise_over_limit.html.eex @@ -1,8 +1,9 @@ -Automated notice about an account that has gone over their enteprise plan limit. +Automated notice about an enterprise account that has gone over their limits. <br /><br /> -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 +Customer email: <%= @user.email %><br /> +Last billing cycle: <%= date_format(@last_cycle.first) %> to <%= date_format(@last_cycle.last) %><br > +Pageview Usage: <%= PlausibleWeb.StatsView.large_number_format(@usage) %> billable pageviews<br /> +Site usage: <%= @site_usage %> / <%= @site_allowance %> allowed sites<br /> --<br /> <%= plausible_url() %><br /> diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex index a596b86dd321d9311d6ad32f2f81155ff3493a56..cf31aaeda4fba5598248b4428f33e4b15cf24366 100644 --- a/lib/workers/check_usage.ex +++ b/lib/workers/check_usage.ex @@ -50,38 +50,80 @@ defmodule Plausible.Workers.CheckUsage do ) 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 subscriber.enterprise_plan do + check_enterprise_subscriber(subscriber, billing_mod) + else + check_regular_subscriber(subscriber, billing_mod) + end + end - cond do - is_over_limit && subscriber.enterprise_plan -> - {_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber) + :ok + end - template = - PlausibleWeb.Email.enterprise_over_limit_email(subscriber, last_month, last_cycle) + def check_enterprise_subscriber(subscriber, billing_mod) do + pageview_limit = check_pageview_limit(subscriber, billing_mod) + site_limit = check_site_limit(subscriber) - Plausible.Mailer.send_email_safe(template) + case {pageview_limit, site_limit} do + {{:within_limit, _}, {:within_limit, _}} -> + nil - is_over_limit -> - {_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber) - suggested_plan = Plausible.Billing.Plans.suggested_plan(subscriber, last_month) + {{_, {last_cycle, last_cycle_usage}}, {_, {site_usage, site_allowance}}} -> + template = + PlausibleWeb.Email.enterprise_over_limit_email( + subscriber, + last_cycle_usage, + last_cycle, + site_usage, + site_allowance + ) - template = - PlausibleWeb.Email.over_limit_email( - subscriber, - last_month, - last_cycle, - suggested_plan - ) + Plausible.Mailer.send_email_safe(template) + end + end - Plausible.Mailer.send_email_safe(template) + defp check_regular_subscriber(subscriber, billing_mod) do + case check_pageview_limit(subscriber, billing_mod) do + {:over_limit, {last_cycle, last_cycle_usage}} -> + suggested_plan = Plausible.Billing.Plans.suggested_plan(subscriber, last_cycle) - true -> - nil - end + template = + PlausibleWeb.Email.over_limit_email( + subscriber, + last_cycle_usage, + last_cycle, + suggested_plan + ) + + Plausible.Mailer.send_email_safe(template) + + _ -> + nil end + end - :ok + defp check_pageview_limit(subscriber, billing_mod) do + allowance = Plausible.Billing.Plans.allowance(subscriber.subscription) + {_, last_cycle} = billing_mod.last_two_billing_cycles(subscriber) + + {last_last_cycle_usage, last_cycle_usage} = + billing_mod.last_two_billing_months_usage(subscriber) + + if last_last_cycle_usage > allowance && last_cycle_usage > allowance do + {:over_limit, {last_cycle, last_cycle_usage, allowance}} + else + {:within_limit, {last_cycle, last_cycle_usage}} + end + end + + defp check_site_limit(subscriber) do + allowance = subscriber.enterprise_plan.site_limit + total_sites = Plausible.Sites.count_owned_by(subscriber) + + if total_sites >= allowance do + {:over_limit, {total_sites, allowance}} + else + {:within_limit, {total_sites, allowance}} + end end end diff --git a/priv/repo/migrations/20211022084427_add_site_limit_to_enterprise_plans.exs b/priv/repo/migrations/20211022084427_add_site_limit_to_enterprise_plans.exs new file mode 100644 index 0000000000000000000000000000000000000000..82879a8cd7d962f1e5c8fd303bed4aba6ff58bef --- /dev/null +++ b/priv/repo/migrations/20211022084427_add_site_limit_to_enterprise_plans.exs @@ -0,0 +1,18 @@ +defmodule Plausible.Repo.Migrations.AddSiteLimitToEnterprisePlans do + use Ecto.Migration + use Plausible.Repo + + def change do + alter table(:enterprise_plans) do + add :site_limit, :integer + end + + flush() + + Repo.update_all("enterprise_plans", set: [site_limit: 50]) + + alter table(:enterprise_plans) do + modify :site_limit, :integer, null: false + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index 02727def53c14ca89d67580101832f7b3f989864..dc2249a55de0ac00db48ec7b7959d3747ad34175 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -121,7 +121,8 @@ defmodule Plausible.Factory do paddle_plan_id: sequence(:paddle_plan_id, &"plan-#{&1}"), billing_interval: :monthly, monthly_pageview_limit: 1_000_000, - hourly_api_request_limit: 3000 + hourly_api_request_limit: 3000, + site_limit: 100 } end diff --git a/test/workers/check_usage_test.exs b/test/workers/check_usage_test.exs index 7441f64e8a77a3cef3fbd5f1e9e1cd3bef8ded4d..833593da3c54b77b6b4f44120cedce39a546bbc4 100644 --- a/test/workers/check_usage_test.exs +++ b/test/workers/check_usage_test.exs @@ -31,6 +31,9 @@ defmodule Plausible.Workers.CheckUsageTest do } do billing_stub = Plausible.Billing + |> stub(:last_two_billing_cycles, fn _user -> + {Date.range(Timex.today(), Timex.today()), Date.range(Timex.today(), Timex.today())} + end) |> stub(:last_two_billing_months_usage, fn _user -> {9_000, 11_000} end) insert(:subscription, @@ -68,31 +71,64 @@ defmodule Plausible.Workers.CheckUsageTest do ) 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) + describe "enterprise customers" do + test "checks billable pageview 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) + 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) - ) + 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) + CheckUsage.perform(nil, billing_stub) - assert_email_delivered_with( - to: [{nil, "enterprise@plausible.io"}], - subject: "#{user.email} has outgrown their enterprise plan" - ) + assert_email_delivered_with( + to: [{nil, "enterprise@plausible.io"}], + subject: "#{user.email} has outgrown their enterprise plan" + ) + end + + test "checks site limit 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, 1} 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, site_limit: 2) + + insert(:site, members: [user]) + insert(:site, members: [user]) + insert(:site, members: [user]) + + 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 end describe "timing" do