diff --git a/elixir/apps/domain/lib/domain/accounts/limits.ex b/elixir/apps/domain/lib/domain/accounts/limits.ex index f8d3032a1..4b61df7ed 100644 --- a/elixir/apps/domain/lib/domain/accounts/limits.ex +++ b/elixir/apps/domain/lib/domain/accounts/limits.ex @@ -3,6 +3,7 @@ defmodule Domain.Accounts.Limits do @primary_key false embedded_schema do + field :users_count, :integer field :monthly_active_users_count, :integer field :service_accounts_count, :integer field :gateway_groups_count, :integer diff --git a/elixir/apps/domain/lib/domain/accounts/limits/changeset.ex b/elixir/apps/domain/lib/domain/accounts/limits/changeset.ex index c20b8efd9..d38acc2eb 100644 --- a/elixir/apps/domain/lib/domain/accounts/limits/changeset.ex +++ b/elixir/apps/domain/lib/domain/accounts/limits/changeset.ex @@ -2,11 +2,16 @@ defmodule Domain.Accounts.Limits.Changeset do use Domain, :changeset alias Domain.Accounts.Limits - @fields ~w[monthly_active_users_count service_accounts_count gateway_groups_count account_admin_users_count]a + @fields ~w[users_count + monthly_active_users_count + service_accounts_count + gateway_groups_count + account_admin_users_count]a def changeset(limits \\ %Limits{}, attrs) do limits |> cast(attrs, @fields) + |> validate_number(:users_count, greater_than_or_equal_to: 0) |> validate_number(:monthly_active_users_count, greater_than_or_equal_to: 0) |> validate_number(:service_accounts_count, greater_than_or_equal_to: 0) |> validate_number(:gateway_groups_count, greater_than_or_equal_to: 0) diff --git a/elixir/apps/domain/lib/domain/actors.ex b/elixir/apps/domain/lib/domain/actors.ex index cdaaf59fa..4dbaddb53 100644 --- a/elixir/apps/domain/lib/domain/actors.ex +++ b/elixir/apps/domain/lib/domain/actors.ex @@ -312,6 +312,13 @@ defmodule Domain.Actors do # Actors + def count_users_for_account(%Accounts.Account{} = account) do + Actor.Query.not_disabled() + |> Actor.Query.by_account_id(account.id) + |> Actor.Query.by_type({:in, [:account_admin_user, :account_user]}) + |> Repo.aggregate(:count) + end + def count_account_admin_users_for_account(%Accounts.Account{} = account) do Actor.Query.not_disabled() |> Actor.Query.by_account_id(account.id) diff --git a/elixir/apps/domain/lib/domain/actors/actor/query.ex b/elixir/apps/domain/lib/domain/actors/actor/query.ex index 5c40a8f36..270298376 100644 --- a/elixir/apps/domain/lib/domain/actors/actor/query.ex +++ b/elixir/apps/domain/lib/domain/actors/actor/query.ex @@ -30,6 +30,10 @@ defmodule Domain.Actors.Actor.Query do where(queryable, [actors: actors], actors.account_id == ^account_id) end + def by_type(queryable, {:in, types}) do + where(queryable, [actors: actors], actors.type in ^types) + end + def by_type(queryable, type) do where(queryable, [actors: actors], actors.type == ^type) end diff --git a/elixir/apps/domain/lib/domain/billing.ex b/elixir/apps/domain/lib/domain/billing.ex index 206eadc82..14b91f535 100644 --- a/elixir/apps/domain/lib/domain/billing.ex +++ b/elixir/apps/domain/lib/domain/billing.ex @@ -36,17 +36,33 @@ defmodule Domain.Billing do # Limits and Features + def users_limit_exceeded?(%Accounts.Account{} = account, users_count) do + not is_nil(account.limits.users_count) and + users_count > account.limits.users_count + end + def seats_limit_exceeded?(%Accounts.Account{} = account, active_users_count) do not is_nil(account.limits.monthly_active_users_count) and active_users_count > account.limits.monthly_active_users_count end def can_create_users?(%Accounts.Account{} = account) do + users_count = Actors.count_users_for_account(account) active_users_count = Clients.count_1m_active_users_for_account(account) - Accounts.account_active?(account) and - (is_nil(account.limits.monthly_active_users_count) or - active_users_count < account.limits.monthly_active_users_count) + cond do + not Accounts.account_active?(account) -> + false + + not is_nil(account.limits.monthly_active_users_count) -> + active_users_count < account.limits.monthly_active_users_count + + not is_nil(account.limits.users_count) -> + users_count < account.limits.users_count + + true -> + true + end end def service_accounts_limit_exceeded?(%Accounts.Account{} = account, service_accounts_count) do diff --git a/elixir/apps/domain/lib/domain/billing/event_handler.ex b/elixir/apps/domain/lib/domain/billing/event_handler.ex index afc0b138e..c42cb81b0 100644 --- a/elixir/apps/domain/lib/domain/billing/event_handler.ex +++ b/elixir/apps/domain/lib/domain/billing/event_handler.ex @@ -354,12 +354,9 @@ defmodule Domain.Billing.EventHandler do end) |> Enum.into(%{}) - {monthly_active_users_count, features_and_limits} = - Map.pop(features_and_limits, "monthly_active_users_count", quantity) - + {users_count, features_and_limits} = Map.pop(features_and_limits, "users_count", quantity) {limits, features} = Map.split(features_and_limits, limit_fields) - - limits = Map.merge(limits, %{"monthly_active_users_count" => monthly_active_users_count}) + limits = Map.merge(limits, %{"users_count" => users_count}) %{ features: features, diff --git a/elixir/apps/domain/lib/domain/billing/jobs.ex b/elixir/apps/domain/lib/domain/billing/jobs.ex index 6fbede8f3..479e5a293 100644 --- a/elixir/apps/domain/lib/domain/billing/jobs.ex +++ b/elixir/apps/domain/lib/domain/billing/jobs.ex @@ -7,6 +7,7 @@ defmodule Domain.Billing.Jobs do |> Enum.each(fn account -> if Billing.enabled?() and Billing.account_provisioned?(account) do [] + |> check_users_limit(account) |> check_seats_limit(account) |> check_service_accounts_limit(account) |> check_gateway_groups_limit(account) @@ -41,6 +42,16 @@ defmodule Domain.Billing.Jobs do end) end + defp check_users_limit(limits_exceeded, account) do + users_count = Actors.count_users_for_account(account) + + if Billing.users_limit_exceeded?(account, users_count) do + limits_exceeded ++ ["users"] + else + limits_exceeded + end + end + defp check_seats_limit(limits_exceeded, account) do active_users_count = Clients.count_1m_active_users_for_account(account) diff --git a/elixir/apps/domain/priv/repo/seeds.exs b/elixir/apps/domain/priv/repo/seeds.exs index 9c8c7626b..3671a4ec7 100644 --- a/elixir/apps/domain/priv/repo/seeds.exs +++ b/elixir/apps/domain/priv/repo/seeds.exs @@ -45,6 +45,7 @@ account = } }, limits: %{ + users_count: 15, monthly_active_users_count: 10, service_accounts_count: 10, gateway_groups_count: 3, diff --git a/elixir/apps/domain/test/domain/actors_test.exs b/elixir/apps/domain/test/domain/actors_test.exs index f48e3f8cf..28ea1e77f 100644 --- a/elixir/apps/domain/test/domain/actors_test.exs +++ b/elixir/apps/domain/test/domain/actors_test.exs @@ -1947,6 +1947,41 @@ defmodule Domain.ActorsTest do end end + describe "count_users_for_account/1" do + test "returns 0 when actors are in another account", %{} do + account = Fixtures.Accounts.create_account() + Fixtures.Actors.create_actor(type: :account_admin_user) + + assert count_users_for_account(account) == 0 + end + + test "returns count of account users" do + account = Fixtures.Accounts.create_account() + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + Fixtures.Actors.create_actor(type: :account_user, account: account) + + assert count_users_for_account(account) == 2 + end + + test "does not count disabled" do + account = Fixtures.Accounts.create_account() + + Fixtures.Actors.create_actor(type: :account_user, account: account) + |> Fixtures.Actors.disable() + + assert count_users_for_account(account) == 0 + end + + test "does not count deleted" do + account = Fixtures.Accounts.create_account() + + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + |> Fixtures.Actors.delete() + + assert count_users_for_account(account) == 0 + end + end + describe "count_account_admin_users_for_account/1" do test "returns 0 when actors are in another account", %{} do account = Fixtures.Accounts.create_account() diff --git a/elixir/apps/domain/test/domain/billing/jobs_test.exs b/elixir/apps/domain/test/domain/billing/jobs_test.exs index 505e58213..8699c3b65 100644 --- a/elixir/apps/domain/test/domain/billing/jobs_test.exs +++ b/elixir/apps/domain/test/domain/billing/jobs_test.exs @@ -47,6 +47,7 @@ defmodule Domain.Billing.JobsTest do Domain.Accounts.update_account(account, %{ limits: %{ + users_count: 1, monthly_active_users_count: 1, service_accounts_count: 1, gateway_groups_count: 1, @@ -59,6 +60,7 @@ defmodule Domain.Billing.JobsTest do account = Repo.get!(Domain.Accounts.Account, account.id) assert account.warning =~ "You have exceeded the following limits:" + assert account.warning =~ "users" assert account.warning =~ "monthly active users" assert account.warning =~ "service accounts" assert account.warning =~ "sites" diff --git a/elixir/apps/domain/test/domain/billing_test.exs b/elixir/apps/domain/test/domain/billing_test.exs index 3e827bd42..5e02668df 100644 --- a/elixir/apps/domain/test/domain/billing_test.exs +++ b/elixir/apps/domain/test/domain/billing_test.exs @@ -44,6 +44,30 @@ defmodule Domain.BillingTest do end end + describe "users_limit_exceeded?/2" do + test "returns false when seats limit is not exceeded", %{account: account} do + {:ok, account} = + Domain.Accounts.update_account(account, %{ + limits: %{monthly_active_users_count: 10} + }) + + assert users_limit_exceeded?(account, 10) == false + end + + test "returns true when seats limit is exceeded", %{account: account} do + {:ok, account} = + Domain.Accounts.update_account(account, %{ + limits: %{users_count: 10} + }) + + assert users_limit_exceeded?(account, 11) == true + end + + test "returns false when seats limit is not set", %{account: account} do + assert users_limit_exceeded?(account, 0) == false + end + end + describe "seats_limit_exceeded?/2" do test "returns false when seats limit is not exceeded", %{account: account} do {:ok, account} = @@ -63,7 +87,7 @@ defmodule Domain.BillingTest do assert seats_limit_exceeded?(account, 11) == true end - test "returns true when seats limit is not set", %{account: account} do + test "returns false when seats limit is not set", %{account: account} do assert seats_limit_exceeded?(account, 0) == false end end @@ -101,6 +125,18 @@ defmodule Domain.BillingTest do assert can_create_users?(account) == false end + test "returns false when users limit is exceeded", %{account: account} do + {:ok, account} = + Domain.Accounts.update_account(account, %{ + limits: %{users_count: 1} + }) + + Fixtures.Actors.create_actor(type: :account_admin_user, account: account) + Fixtures.Actors.create_actor(type: :account_user, account: account) + + assert can_create_users?(account) == false + end + test "returns false when account is disabled", %{account: account} do {:ok, account} = Domain.Accounts.update_account(account, %{ @@ -111,7 +147,7 @@ defmodule Domain.BillingTest do assert can_create_users?(account) == false end - test "returns true when seats limit is not set", %{account: account} do + test "returns false when seats limit is not set", %{account: account} do assert can_create_users?(account) == true end end @@ -741,7 +777,8 @@ defmodule Domain.BillingTest do "self_hosted_relays" => "true", "monthly_active_users_count" => "15", "service_accounts_count" => "unlimited", - "gateway_groups_count" => 1 + "gateway_groups_count" => 1, + "users_count" => 14 } }) @@ -771,7 +808,8 @@ defmodule Domain.BillingTest do assert account.limits == %Domain.Accounts.Limits{ monthly_active_users_count: 15, gateway_groups_count: 5, - service_accounts_count: nil + service_accounts_count: nil, + users_count: 14 } assert account.features == %Domain.Accounts.Features{ diff --git a/elixir/apps/web/lib/web/live/settings/billing.ex b/elixir/apps/web/lib/web/live/settings/billing.ex index f53b088a9..aad19ded1 100644 --- a/elixir/apps/web/lib/web/live/settings/billing.ex +++ b/elixir/apps/web/lib/web/live/settings/billing.ex @@ -7,6 +7,7 @@ defmodule Web.Settings.Billing do if Billing.account_provisioned?(socket.assigns.account) do admins_count = Actors.count_account_admin_users_for_account(socket.assigns.account) service_accounts_count = Actors.count_service_accounts_for_account(socket.assigns.account) + users_count = Actors.count_users_for_account(socket.assigns.account) active_users_count = Clients.count_1m_active_users_for_account(socket.assigns.account) gateway_groups_count = Gateways.count_groups_for_account(socket.assigns.account) @@ -15,6 +16,7 @@ defmodule Web.Settings.Billing do page_title: "Billing", error: nil, admins_count: admins_count, + users_count: users_count, active_users_count: active_users_count, service_accounts_count: service_accounts_count, gateway_groups_count: gateway_groups_count @@ -83,6 +85,21 @@ defmodule Web.Settings.Billing do + <.vertical_table_row :if={not is_nil(@account.limits.users_count)}> + <:label> +
Users
+ + <:value> + @account.limits.users_count && "text-red-500" + ]}> + <%= @users_count %> used + + / <%= @account.limits.users_count %> allowed + + + <.vertical_table_row :if={not is_nil(@account.limits.monthly_active_users_count)}> <:label>Seats
@@ -111,7 +128,6 @@ defmodule Web.Settings.Billing do <%= @service_accounts_count %> used / <%= @account.limits.service_accounts_count %> allowed -users with at least one device signed-in within last month
diff --git a/elixir/apps/web/test/web/live/settings/billing_test.exs b/elixir/apps/web/test/web/live/settings/billing_test.exs index 7647d202f..931c6cb00 100644 --- a/elixir/apps/web/test/web/live/settings/billing_test.exs +++ b/elixir/apps/web/test/web/live/settings/billing_test.exs @@ -18,7 +18,8 @@ defmodule Web.Live.Settings.BillingTest do monthly_active_users_count: 100, service_accounts_count: 100, gateway_groups_count: 10, - account_admin_users_count: 2 + account_admin_users_count: 2, + users_count: 200 } ) @@ -75,6 +76,7 @@ defmodule Web.Live.Settings.BillingTest do assert rows["billing email"] =~ account.metadata.stripe.billing_email assert rows["current plan"] =~ account.metadata.stripe.product_name + assert rows["users"] =~ "1 used / 200 allowed" assert rows["seats"] =~ "0 used / 100 allowed" assert rows["sites"] =~ "0 used / 10 allowed" assert rows["admins"] =~ "1 used / 2 allowed"