diff --git a/elixir/apps/domain/lib/domain/accounts/account/query.ex b/elixir/apps/domain/lib/domain/accounts/account/query.ex index d51906fcd..b69abab83 100644 --- a/elixir/apps/domain/lib/domain/accounts/account/query.ex +++ b/elixir/apps/domain/lib/domain/accounts/account/query.ex @@ -9,6 +9,10 @@ defmodule Domain.Accounts.Account.Query do where(queryable, [accounts: accounts], is_nil(accounts.deleted_at)) end + def disabled(queryable \\ all()) do + where(queryable, [accounts: accounts], not is_nil(accounts.disabled_at)) + end + def not_disabled(queryable \\ not_deleted()) do where(queryable, [accounts: accounts], is_nil(accounts.disabled_at)) end diff --git a/elixir/apps/domain/lib/domain/ops.ex b/elixir/apps/domain/lib/domain/ops.ex index becfc8382..d3b12fadc 100644 --- a/elixir/apps/domain/lib/domain/ops.ex +++ b/elixir/apps/domain/lib/domain/ops.ex @@ -83,4 +83,17 @@ defmodule Domain.Ops do |> Domain.Billing.EventHandler.handle_event() end) end + + @doc """ + To delete an account you need to disable it first by cancelling its subscription in Stripe. + """ + def delete_disabled_account(id) do + Domain.Accounts.Account.Query.not_deleted() + |> Domain.Accounts.Account.Query.disabled() + |> Domain.Accounts.Account.Query.by_id(id) + |> Domain.Repo.one!() + |> Domain.Repo.delete() + + :ok + end end diff --git a/elixir/apps/domain/priv/repo/migrations/20240409154035_add_account_deletion_cascade.exs b/elixir/apps/domain/priv/repo/migrations/20240409154035_add_account_deletion_cascade.exs new file mode 100644 index 000000000..761f93671 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20240409154035_add_account_deletion_cascade.exs @@ -0,0 +1,117 @@ +defmodule Domain.Repo.Migrations.AddAccountDeletionCascade do + use Ecto.Migration + + def change do + execute(""" + ALTER TABLE "actor_group_memberships" + DROP CONSTRAINT "actor_group_memberships_account_id_fkey", + ADD CONSTRAINT "actor_group_memberships_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "actors" + DROP CONSTRAINT "actors_account_id_fkey", + ADD CONSTRAINT "actors_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "auth_identities" + DROP CONSTRAINT "auth_identities_account_id_fkey", + ADD CONSTRAINT "auth_identities_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "auth_providers" + DROP CONSTRAINT "auth_providers_account_id_fkey", + ADD CONSTRAINT "auth_providers_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "clients" + DROP CONSTRAINT "devices_account_id_fkey", + ADD CONSTRAINT "devices_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "configurations" + DROP CONSTRAINT "configurations_account_id_fkey", + ADD CONSTRAINT "configurations_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "gateway_groups" + DROP CONSTRAINT "gateway_groups_account_id_fkey", + ADD CONSTRAINT "gateway_groups_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "gateways" + DROP CONSTRAINT "gateways_account_id_fkey", + ADD CONSTRAINT "gateways_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "network_addresses" + DROP CONSTRAINT "network_addresses_account_id_fkey", + ADD CONSTRAINT "network_addresses_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "policies" + DROP CONSTRAINT "policies_account_id_fkey", + ADD CONSTRAINT "policies_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "relay_groups" + DROP CONSTRAINT "relay_groups_account_id_fkey", + ADD CONSTRAINT "relay_groups_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "relays" + DROP CONSTRAINT "relays_account_id_fkey", + ADD CONSTRAINT "relays_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "resource_connections" + DROP CONSTRAINT "resource_connections_account_id_fkey", + ADD CONSTRAINT "resource_connections_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + + execute(""" + ALTER TABLE "resources" + DROP CONSTRAINT "resources_account_id_fkey", + ADD CONSTRAINT "resources_account_id_fkey" + FOREIGN KEY ("account_id") + REFERENCES "accounts" ("id") ON DELETE CASCADE; + """) + end +end diff --git a/elixir/apps/domain/test/domain/ops_test.exs b/elixir/apps/domain/test/domain/ops_test.exs index ecfe7156c..c3e234450 100644 --- a/elixir/apps/domain/test/domain/ops_test.exs +++ b/elixir/apps/domain/test/domain/ops_test.exs @@ -80,4 +80,38 @@ defmodule Domain.OpsTest do end end end + + describe "delete_disabled_account/1" do + test "doesn't delete an account that is not disabled" do + account = Fixtures.Accounts.create_account() + + assert_raise Ecto.NoResultsError, fn -> + delete_disabled_account(account.id) + end + end + + test "deletes account along with all related entities" do + account = Fixtures.Accounts.create_account() + Fixtures.Actors.create_group(account: account) + Fixtures.Actors.create_actor(account: account) + Fixtures.Auth.create_identity(account: account) + Fixtures.Clients.create_client(account: account) + Fixtures.Flows.create_activity(account: account) + Fixtures.Gateways.create_gateway(account: account) + Fixtures.Policies.create_policy(account: account) + Fixtures.Relays.create_relay(account: account) + Fixtures.Resources.create_resource(account: account) + Fixtures.Tokens.create_token(account: account) + + Fixtures.Accounts.disable_account(account) + + assert delete_disabled_account(account.id) == :ok + + assert_raise Ecto.NoResultsError, fn -> + assert delete_disabled_account(account.id) == :ok + end + + refute Repo.one(Domain.Accounts.Account) + end + end end