diff --git a/lib/plausible/auth/user.ex b/lib/plausible/auth/user.ex index bfb3eafd2..3a9021c42 100644 --- a/lib/plausible/auth/user.ex +++ b/lib/plausible/auth/user.ex @@ -4,6 +4,15 @@ defimpl Bamboo.Formatter, for: Plausible.Auth.User do end end +defmodule Plausible.Auth.GracePeriod do + use Ecto.Schema + + embedded_schema do + field :end_date, :date + field :allowance_required, :integer + end +end + defmodule Plausible.Auth.User do use Ecto.Schema import Ecto.Changeset @@ -17,9 +26,9 @@ defmodule Plausible.Auth.User do field :name, :string field :last_seen, :naive_datetime field :trial_expiry_date, :date - field :grace_period_end, :date field :theme, :string field :email_verified, :boolean + embeds_one :grace_period, Plausible.Auth.GracePeriod, on_replace: :update has_many :site_memberships, Plausible.Site.Membership has_many :sites, through: [:site_memberships, :site] @@ -80,8 +89,17 @@ defmodule Plausible.Auth.User do change(user, trial_expiry_date: Timex.today() |> Timex.shift(days: -1)) end - def start_grace_period(user) do - change(user, grace_period_end: Timex.today() |> Timex.shift(days: 7)) + def start_grace_period(user, allowance_required) do + grace_period = %Plausible.Auth.GracePeriod{ + end_date: Timex.today() |> Timex.shift(days: 7), + allowance_required: allowance_required + } + + change(user, grace_period: grace_period) + end + + def remove_grace_period(user) do + change(user, grace_period: nil) end defp trial_expiry() do diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index e292cb06d..bd7854f79 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -17,20 +17,29 @@ defmodule Plausible.Billing do changeset = Subscription.changeset(%Subscription{}, format_subscription(params)) - Repo.insert(changeset) - |> check_lock_status - |> maybe_adjust_api_key_limits + Repo.insert(changeset) |> after_subscription_update 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) + Repo.update(changeset) |> after_subscription_update + end + + defp after_subscription_update({:ok, subscription}) do + user = + Repo.get(Plausible.Auth.User, subscription.user_id) + |> Map.put(:subscription, subscription) + + {:ok, user} + |> maybe_remove_grace_period |> check_lock_status |> maybe_adjust_api_key_limits end + defp after_subscription_update(err), do: err + def subscription_cancelled(params) do subscription = Repo.get_by(Subscription, paddle_subscription_id: params["subscription_id"]) @@ -114,7 +123,7 @@ defmodule Plausible.Billing do subscription_active = subscription_is_active?(user.subscription) grace_period_ended = - user.grace_period_end && Timex.before?(user.grace_period_end, Timex.today()) + user.grace_period && Timex.before?(user.grace_period.end_date, Timex.today()) cond do trial_is_over && !subscription_active -> {true, :no_active_subscription} @@ -225,31 +234,48 @@ defmodule Plausible.Billing do defp present?(nil), do: false defp present?(_), do: true - defp check_lock_status({:ok, subscription}) do - user = - Repo.get(Plausible.Auth.User, subscription.user_id) - |> Map.put(:subscription, subscription) + defp maybe_remove_grace_period({:ok, user}) do + alias Plausible.Auth.GracePeriod + case user.grace_period do + %GracePeriod{allowance_required: allowance_required} -> + new_allowance = Plausible.Billing.Plans.allowance(user.subscription) + + if new_allowance > allowance_required do + Plausible.Auth.User.remove_grace_period(user) + |> Repo.update() + else + {:ok, user} + end + + _ -> + {:ok, user} + end + end + + defp maybe_remove_grace_period(err), do: err + + defp check_lock_status({:ok, user}) do Plausible.Billing.SiteLocker.check_sites_for(user) - {:ok, subscription} + {:ok, user} end defp check_lock_status(err), do: err - defp maybe_adjust_api_key_limits({:ok, subscription}) do + defp maybe_adjust_api_key_limits({:ok, user}) do plan = Repo.get_by(Plausible.Billing.EnterprisePlan, - user_id: subscription.user_id, - paddle_plan_id: subscription.paddle_plan_id + user_id: user.id, + paddle_plan_id: user.subscription.paddle_plan_id ) if plan do - user_id = subscription.user_id + user_id = 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} + {:ok, user} end defp maybe_adjust_api_key_limits(err), do: err diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex index 41abcb2b7..ca0193025 100644 --- a/lib/workers/check_usage.ex +++ b/lib/workers/check_usage.ex @@ -96,7 +96,7 @@ defmodule Plausible.Workers.CheckUsage do ) Plausible.Mailer.send_email_safe(template) - Plausible.Auth.User.start_grace_period(subscriber) |> Repo.update() + Plausible.Auth.User.start_grace_period(subscriber, last_cycle_usage) |> Repo.update() _ -> nil diff --git a/priv/repo/migrations/20211028122202_grace_period_end.exs b/priv/repo/migrations/20211028122202_grace_period_end.exs index c0b6a64ca..e86400b0d 100644 --- a/priv/repo/migrations/20211028122202_grace_period_end.exs +++ b/priv/repo/migrations/20211028122202_grace_period_end.exs @@ -3,7 +3,7 @@ defmodule Plausible.Repo.Migrations.GracePeriodEnd do def change do alter table(:users) do - add :grace_period_end, :date + add :grace_period, :map end end end diff --git a/test/plausible/billing/billing_test.exs b/test/plausible/billing/billing_test.exs index 998addde5..8e5499f56 100644 --- a/test/plausible/billing/billing_test.exs +++ b/test/plausible/billing/billing_test.exs @@ -229,7 +229,8 @@ defmodule Plausible.BillingTest do end @subscription_id "subscription-123" - @plan_id "plan-123" + @plan_id_10k "654177" + @plan_id_100k "654178" describe "subscription_created" do test "creates a subscription" do @@ -238,7 +239,7 @@ defmodule Plausible.BillingTest do Billing.subscription_created(%{ "alert_name" => "subscription_created", "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id, + "subscription_plan_id" => @plan_id_10k, "update_url" => "update_url.com", "cancel_url" => "cancel_url.com", "passthrough" => user.id, @@ -263,7 +264,7 @@ defmodule Plausible.BillingTest do "email" => user.email, "alert_name" => "subscription_created", "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id, + "subscription_plan_id" => @plan_id_10k, "update_url" => "update_url.com", "cancel_url" => "cancel_url.com", "status" => "active", @@ -285,7 +286,7 @@ defmodule Plausible.BillingTest do Billing.subscription_created(%{ "alert_name" => "subscription_created", "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id, + "subscription_plan_id" => @plan_id_10k, "update_url" => "update_url.com", "cancel_url" => "cancel_url.com", "passthrough" => user.id, @@ -304,7 +305,7 @@ defmodule Plausible.BillingTest do plan = insert(:enterprise_plan, user: user, - paddle_plan_id: @plan_id, + paddle_plan_id: @plan_id_10k, hourly_api_request_limit: 10_000 ) @@ -313,7 +314,7 @@ defmodule Plausible.BillingTest do Billing.subscription_created(%{ "alert_name" => "subscription_created", "subscription_id" => @subscription_id, - "subscription_plan_id" => @plan_id, + "subscription_plan_id" => @plan_id_10k, "update_url" => "update_url.com", "cancel_url" => "cancel_url.com", "passthrough" => user.id, @@ -401,6 +402,66 @@ defmodule Plausible.BillingTest do assert Repo.reload!(api_key).hourly_request_limit == plan.hourly_api_request_limit end + + test "if user's grace period has ended, upgrading to the proper plan will unlock sites and remove grace period" do + user = + insert(:user, + grace_period: %Plausible.Auth.GracePeriod{ + end_date: Timex.shift(Timex.today(), days: -1), + allowance_required: 11_000 + } + ) + + subscription = insert(:subscription, user: user) + site = insert(:site, locked: true, members: [user]) + + Billing.subscription_updated(%{ + "alert_name" => "subscription_updated", + "subscription_id" => subscription.paddle_subscription_id, + "subscription_plan_id" => @plan_id_100k, + "update_url" => "update_url.com", + "cancel_url" => "cancel_url.com", + "passthrough" => user.id, + "old_status" => "past_due", + "status" => "active", + "next_bill_date" => "2019-06-01", + "new_unit_price" => "12.00", + "currency" => "EUR" + }) + + assert Repo.reload!(site).locked == false + assert Repo.reload!(user).grace_period == nil + end + + test "does not remove grace period if upgraded plan allowance is too low" do + user = + insert(:user, + grace_period: %Plausible.Auth.GracePeriod{ + end_date: Timex.shift(Timex.today(), days: -1), + allowance_required: 11_000 + } + ) + + subscription = insert(:subscription, user: user) + site = insert(:site, locked: true, members: [user]) + + Billing.subscription_updated(%{ + "alert_name" => "subscription_updated", + "subscription_id" => subscription.paddle_subscription_id, + "subscription_plan_id" => @plan_id_10k, + "update_url" => "update_url.com", + "cancel_url" => "cancel_url.com", + "passthrough" => user.id, + "old_status" => "past_due", + "status" => "active", + "next_bill_date" => "2019-06-01", + "new_unit_price" => "12.00", + "currency" => "EUR" + }) + + assert Repo.reload!(site).locked == true + assert Repo.reload!(user).grace_period.allowance_required == 11_000 + end end describe "subscription_cancelled" do diff --git a/test/plausible/billing/site_locker_test.exs b/test/plausible/billing/site_locker_test.exs index e42a2e0d4..8b92304f9 100644 --- a/test/plausible/billing/site_locker_test.exs +++ b/test/plausible/billing/site_locker_test.exs @@ -50,7 +50,14 @@ defmodule Plausible.Billing.SiteLockerTest do end test "does not lock user who has an active subscription and is on grace period" do - user = insert(:user, grace_period_end: Timex.shift(Timex.today(), days: 1)) + user = + insert(:user, + grace_period: %Plausible.Auth.GracePeriod{ + end_date: Timex.shift(Timex.today(), days: 1), + allowance_required: 10_000 + } + ) + insert(:subscription, status: "active", user: user) user = Repo.preload(user, :subscription) site = insert(:site, members: [user]) @@ -79,7 +86,14 @@ defmodule Plausible.Billing.SiteLockerTest do end test "locks all sites if user has active subscription but grace period has ended" do - user = insert(:user, grace_period_end: Timex.shift(Timex.today(), days: -1)) + user = + insert(:user, + grace_period: %Plausible.Auth.GracePeriod{ + end_date: Timex.shift(Timex.today(), days: -1), + allowance_required: 10_000 + } + ) + insert(:subscription, status: "active", user: user) user = Repo.preload(user, :subscription) site = insert(:site, members: [user]) @@ -90,7 +104,14 @@ defmodule Plausible.Billing.SiteLockerTest do end test "sends email if grace period has ended" do - user = insert(:user, grace_period_end: Timex.shift(Timex.today(), days: -1)) + user = + insert(:user, + grace_period: %Plausible.Auth.GracePeriod{ + end_date: Timex.shift(Timex.today(), days: -1), + allowance_required: 10_000 + } + ) + insert(:subscription, status: "active", user: user) user = Repo.preload(user, :subscription) insert(:site, members: [user]) diff --git a/test/workers/check_usage_test.exs b/test/workers/check_usage_test.exs index c54584583..3dc641857 100644 --- a/test/workers/check_usage_test.exs +++ b/test/workers/check_usage_test.exs @@ -24,7 +24,7 @@ defmodule Plausible.Workers.CheckUsageTest do CheckUsage.perform(nil) assert_no_emails_delivered() - assert Repo.reload(user).grace_period_end == nil + 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", %{ @@ -46,7 +46,7 @@ defmodule Plausible.Workers.CheckUsageTest do CheckUsage.perform(nil, billing_stub) assert_no_emails_delivered() - assert Repo.reload(user).grace_period_end == nil + assert Repo.reload(user).grace_period == nil end test "sends an email when an account is over their limit for two consecutive billing months", %{ @@ -72,7 +72,7 @@ defmodule Plausible.Workers.CheckUsageTest do subject: "[Action required] You have outgrown your Plausible subscription tier" ) - assert Repo.reload(user).grace_period_end == Timex.shift(Timex.today(), days: 7) + assert Repo.reload(user).grace_period.end_date == Timex.shift(Timex.today(), days: 7) end describe "enterprise customers" do