defmodule Mix.Tasks.JoinRequests.CleanupExpired do @moduledoc """ Hard-deletes JoinRequests in status `pending_confirmation` whose confirmation link has expired. Retention: records with `confirmation_token_expires_at` older than now are deleted. Intended for cron or Oban (e.g. every hour). See docs/onboarding-join-concept.md. ## Usage mix join_requests.cleanup_expired ## Examples $ mix join_requests.cleanup_expired Deleted 3 expired join request(s). """ use Mix.Task require Ash.Query require Logger alias Mv.Membership.JoinRequest @shortdoc "Deletes join requests in pending_confirmation with expired confirmation token" @impl Mix.Task def run(_args) do Mix.Task.run("app.start") now = DateTime.utc_now() query = JoinRequest |> Ash.Query.filter(status == :pending_confirmation) |> Ash.Query.filter(confirmation_token_expires_at < ^now) # Bypass authorization: cleanup is a system maintenance task (cron/Oban). # Use bulk_destroy so the data layer can delete in one pass when supported. opts = [domain: Mv.Membership, authorize?: false] count = case Ash.count(query, opts) do {:ok, n} -> n {:error, _} -> 0 end do_run(query, opts, count) end defp do_run(_query, _opts, 0) do Mix.shell().info("No expired join requests to delete.") 0 end defp do_run(query, opts, count) do case Ash.bulk_destroy(query, :destroy, %{}, opts) do %{status: status, errors: errors} when status in [:success, :partial_success] -> maybe_log_errors(errors) Mix.shell().info("Deleted #{count} expired join request(s).") count %{status: :error, errors: errors} -> Mix.raise("Failed to delete expired join requests: #{inspect(errors)}") end end defp maybe_log_errors(nil), do: :ok defp maybe_log_errors([]), do: :ok defp maybe_log_errors(errors) do Logger.warning( "Join requests cleanup: #{length(errors)} error(s) while deleting expired requests: #{inspect(errors)}" ) end end