diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index 0bb29e5bcc9b..1278a07cfeea 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -265,7 +265,13 @@ defmodule Plausible.Billing do user = User |> Repo.get!(subscription.user_id) - |> Map.put(:subscription, subscription) + |> Plausible.Users.with_subscription() + + if subscription.id != user.subscription.id do + Sentry.capture_message("Susbscription ID mismatch", + extra: %{subscription: inspect(subscription), user_id: user.id} + ) + end user |> Plausible.Users.update_accept_traffic_until() diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex index dc5afa232910..9a0a418571e3 100644 --- a/lib/workers/check_usage.ex +++ b/lib/workers/check_usage.ex @@ -36,19 +36,34 @@ defmodule Plausible.Workers.CheckUsage do def perform(_job, usage_mod \\ Quota.Usage, today \\ Date.utc_today()) do yesterday = today |> Date.shift(day: -1) + last_subscription_query = + from(s in Subscription, + order_by: [desc: s.inserted_at], + where: s.user_id == parent_as(:user).id, + where: + s.status in [ + ^Subscription.Status.active(), + ^Subscription.Status.past_due(), + ^Subscription.Status.deleted() + ], + 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: s.next_bill_date >= ^today, + where: + least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) == + day_of_month(^yesterday), + limit: 1 + ) + active_subscribers = Repo.all( from(u in User, - join: s in Plausible.Billing.Subscription, - on: s.user_id == u.id, + as: :user, + inner_lateral_join: s in subquery(last_subscription_query), + on: true, left_join: ep in Plausible.Billing.EnterprisePlan, on: ep.user_id == u.id, - where: s.status == ^Subscription.Status.active(), - where: not is_nil(s.last_bill_date), - # Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01. - where: - least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) == - day_of_month(^yesterday), + order_by: u.id, preload: [subscription: s, enterprise_plan: ep] ) ) diff --git a/test/workers/check_usage_test.exs b/test/workers/check_usage_test.exs index e05be4f8d989..ffdf7feb529c 100644 --- a/test/workers/check_usage_test.exs +++ b/test/workers/check_usage_test.exs @@ -5,37 +5,33 @@ defmodule Plausible.Workers.CheckUsageTest do alias Plausible.Workers.CheckUsage + require Plausible.Billing.Subscription.Status + setup [:create_user, :create_site] @paddle_id_10k "558018" @date_range Date.range(Timex.today(), Timex.today()) - test "ignores user without subscription" do - CheckUsage.perform(nil) - - assert_no_emails_delivered() - end - - test "ignores user with subscription but no usage", %{user: user} do - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) + @accepted_status_values [ + Plausible.Billing.Subscription.Status.active(), + Plausible.Billing.Subscription.Status.past_due(), + Plausible.Billing.Subscription.Status.deleted() + ] + test "ignores user without subscription" do CheckUsage.perform(nil) assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil end - test "does not send an email if account has been over the limit for one billing month", %{ - user: user - } do + test "operates on the current subscription", + %{ + user: user + } do usage_stub = Plausible.Billing.Quota.Usage |> stub(:monthly_pageview_usage, fn _user -> %{ - penultimate_cycle: %{date_range: @date_range, total: 9_000}, + penultimate_cycle: %{date_range: @date_range, total: 11_000}, last_cycle: %{date_range: @date_range, total: 11_000} } end) @@ -43,42 +39,27 @@ defmodule Plausible.Workers.CheckUsageTest do insert(:subscription, user: user, paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: :active ) - CheckUsage.perform(nil, usage_stub) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil - end - - test "does not send an email if account is over the limit by less than 10%", %{ - user: user - } do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 10_999}, - last_cycle: %{date_range: @date_range, total: 11_000} - } - end) - insert(:subscription, user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) + paddle_plan_id: "wont-exist-should-crash", + last_bill_date: Timex.shift(Timex.today(), days: -1), + inserted_at: DateTime.shift(DateTime.utc_now(), day: -2), + status: :deleted ) CheckUsage.perform(nil, usage_stub) - assert_no_emails_delivered() - assert Repo.reload(user).grace_period == nil + assert_email_delivered_with( + to: [user], + subject: "[Action required] You have outgrown your Plausible subscription tier" + ) end - test "sends an email when an account is over their limit for two consecutive billing months", %{ - user: user - } do + test "sends more than one email", %{user: user} do usage_stub = Plausible.Billing.Quota.Usage |> stub(:monthly_pageview_usage, fn _user -> @@ -88,11 +69,18 @@ defmodule Plausible.Workers.CheckUsageTest do } end) - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) + user2 = insert(:user) + insert(:site, members: [user2]) + + for u <- [user, user2] do + insert(:subscription, + user: u, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + next_bill_date: Timex.shift(Timex.today(), days: +5), + status: :active + ) + end CheckUsage.perform(nil, usage_stub) @@ -101,233 +89,357 @@ defmodule Plausible.Workers.CheckUsageTest do subject: "[Action required] You have outgrown your Plausible subscription tier" ) - assert Repo.reload(user).grace_period.end_date == Timex.shift(Timex.today(), days: 7) - end - - test "sends an email suggesting enterprise plan when usage is greater than 10M ", %{ - user: user - } do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 11_000_000}, - last_cycle: %{date_range: @date_range, total: 11_000_000} - } - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) + assert_email_delivered_with( + to: [user2], + subject: "[Action required] You have outgrown your Plausible subscription tier" ) - - CheckUsage.perform(nil, usage_stub) - - assert_delivered_email_matches(%{html_body: html_body}) - - assert html_body =~ - "Your usage exceeds our standard plans, so please reply back to this email for a tailored quote" end - test "skips checking users who already have a grace period", %{user: user} do - %{grace_period: existing_grace_period} = - user - |> Plausible.Auth.GracePeriod.start_changeset() - |> Repo.update!() - - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 11_000}, - last_cycle: %{date_range: @date_range, total: 11_000} - } - end) - + test "ignores user with paused subscription", %{user: user} do insert(:subscription, user: user, paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: Plausible.Billing.Subscription.Status.paused() ) - CheckUsage.perform(nil, usage_stub) + CheckUsage.perform(nil) assert_no_emails_delivered() - assert Repo.reload(user).grace_period.id == existing_grace_period.id - end - - test "recommends a plan to upgrade to", %{ - user: user - } do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 11_000}, - last_cycle: %{date_range: @date_range, total: 11_000} - } - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, usage_stub) - - assert_delivered_email_matches(%{ - html_body: html_body - }) - - assert html_body =~ "We recommend you upgrade to the 100k/mo plan" - end - - test "clears grace period when plan is applicable again", %{user: user} do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 11_000}, - last_cycle: %{date_range: @date_range, total: 11_000} - } - end) - - insert(:subscription, - user: user, - paddle_plan_id: @paddle_id_10k, - last_bill_date: Timex.shift(Timex.today(), days: -1) - ) - - CheckUsage.perform(nil, usage_stub) - assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() - - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 11_000}, - last_cycle: %{date_range: @date_range, total: 9_000} - } - end) - - CheckUsage.perform(nil, usage_stub) - refute user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() end - describe "enterprise customers" do - test "skips checking enterprise users who already have a grace period", %{user: user} do - %{grace_period: existing_grace_period} = - user - |> Plausible.Auth.GracePeriod.start_manual_lock_changeset() - |> Repo.update!() - - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, - last_cycle: %{date_range: @date_range, total: 1_100_000} - } - 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, usage_stub) - - assert_no_emails_delivered() - assert Repo.reload(user).grace_period.id == existing_grace_period.id + for status <- @accepted_status_values do + describe "#{status} subscription, regular customers" do + test "ignores user with subscription but no usage", %{user: user} do + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil) + + assert_no_emails_delivered() + assert Repo.reload(user).grace_period == nil + end + + test "does not send an email if account has been over the limit for one billing month", %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 9_000}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_no_emails_delivered() + assert Repo.reload(user).grace_period == nil + end + + test "does not send an email if account is over the limit by less than 10%", %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 10_999}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_no_emails_delivered() + assert Repo.reload(user).grace_period == nil + end + + test "sends an email when an account is over their limit for two consecutive billing months", + %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_email_delivered_with( + to: [user], + subject: "[Action required] You have outgrown your Plausible subscription tier" + ) + + assert Repo.reload(user).grace_period.end_date == Timex.shift(Timex.today(), days: 7) + end + + test "sends an email suggesting enterprise plan when usage is greater than 10M ", %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000_000}, + last_cycle: %{date_range: @date_range, total: 11_000_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_delivered_email_matches(%{html_body: html_body}) + + assert html_body =~ + "Your usage exceeds our standard plans, so please reply back to this email for a tailored quote" + end + + test "skips checking users who already have a grace period", %{user: user} do + %{grace_period: existing_grace_period} = + user + |> Plausible.Auth.GracePeriod.start_changeset() + |> Repo.update!() + + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_no_emails_delivered() + assert Repo.reload(user).grace_period.id == existing_grace_period.id + end + + test "recommends a plan to upgrade to", %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_delivered_email_matches(%{ + html_body: html_body + }) + + assert html_body =~ "We recommend you upgrade to the 100k/mo plan" + end + + test "clears grace period when plan is applicable again", %{user: user} do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000}, + last_cycle: %{date_range: @date_range, total: 11_000} + } + end) + + insert(:subscription, + user: user, + paddle_plan_id: @paddle_id_10k, + last_bill_date: Timex.shift(Timex.today(), days: -1), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() + + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 11_000}, + last_cycle: %{date_range: @date_range, total: 9_000} + } + end) + + CheckUsage.perform(nil, usage_stub) + refute user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() + end end + end - test "checks billable pageview usage for enterprise customer, sends usage information to enterprise@plausible.io", - %{ - user: user - } do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, - last_cycle: %{date_range: @date_range, total: 1_100_000} - } - 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, usage_stub) - - 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 - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 1}, - last_cycle: %{date_range: @date_range, total: 1} - } - 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, usage_stub) - - assert_email_delivered_with( - to: [{nil, "enterprise@plausible.io"}], - subject: "#{user.email} has outgrown their enterprise plan" - ) - end - - test "starts grace period when plan is outgrown", %{user: user} do - usage_stub = - Plausible.Billing.Quota.Usage - |> stub(:monthly_pageview_usage, fn _user -> - %{ - penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, - last_cycle: %{date_range: @date_range, total: 1_100_000} - } - 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, usage_stub) - assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() + for status <- @accepted_status_values do + describe "#{status} subscription, enterprise customers" do + test "skips checking enterprise users who already have a grace period", %{user: user} do + %{grace_period: existing_grace_period} = + user + |> Plausible.Auth.GracePeriod.start_manual_lock_changeset() + |> Repo.update!() + + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, + last_cycle: %{date_range: @date_range, total: 1_100_000} + } + 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), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_no_emails_delivered() + assert Repo.reload(user).grace_period.id == existing_grace_period.id + end + + test "checks billable pageview usage for enterprise customer, sends usage information to enterprise@plausible.io", + %{ + user: user + } do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, + last_cycle: %{date_range: @date_range, total: 1_100_000} + } + 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), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + 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 + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 1}, + last_cycle: %{date_range: @date_range, total: 1} + } + 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), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + + assert_email_delivered_with( + to: [{nil, "enterprise@plausible.io"}], + subject: "#{user.email} has outgrown their enterprise plan" + ) + end + + test "starts grace period when plan is outgrown", %{user: user} do + usage_stub = + Plausible.Billing.Quota.Usage + |> stub(:monthly_pageview_usage, fn _user -> + %{ + penultimate_cycle: %{date_range: @date_range, total: 1_100_000}, + last_cycle: %{date_range: @date_range, total: 1_100_000} + } + 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), + status: unquote(status) + ) + + CheckUsage.perform(nil, usage_stub) + assert user |> Repo.reload() |> Plausible.Auth.GracePeriod.active?() + end end end