who_need_help/lib/who_need_help/release.ex

366 lines
12 KiB
Elixir

defmodule WhoNeedHelp.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :who_need_help
import Ecto.Query
def migrate do
load_app()
for repo <- repos() do
{:ok, _, _} =
Ecto.Migrator.with_repo(repo, fn repo ->
Ecto.Migrator.run(repo, :up, all: true)
WhoNeedHelp.Catalog.seed_defaults()
end)
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
def bootstrap_admin(email) when is_binary(email) do
WhoNeedHelp.Repo.transact(fn ->
WhoNeedHelp.Repo.query!(
"SELECT pg_advisory_xact_lock(hashtext($1))",
["who_need_help.bootstrap_admin"]
)
admin_count =
WhoNeedHelp.Repo.aggregate(
from(assignment in WhoNeedHelp.Accounts.StaffRoleAssignment,
where: assignment.role == :admin
),
:count
)
cond do
admin_count > 0 ->
{:error, :admin_already_exists}
user = WhoNeedHelp.Repo.get_by(WhoNeedHelp.Accounts.User, email: email) ->
with {:ok, _assignment} <-
%WhoNeedHelp.Accounts.StaffRoleAssignment{}
|> WhoNeedHelp.Accounts.StaffRoleAssignment.changeset(%{
user_id: user.id,
role: :admin
})
|> WhoNeedHelp.Repo.insert(),
{:ok, _audit} <-
WhoNeedHelp.Trust.audit(nil, "user.admin_bootstrapped", "user", user.id) do
{:ok, %{id: user.id, email: user.email, roles: [:admin]}}
end
true ->
{:error, :user_not_found}
end
end)
end
@play_review_request_title "Play review — medicine pickup"
@play_review_activity_title "Play review — public cinema meetup"
@doc """
Checks whether two existing ordinary accounts can be used for Google Play
review without creating or changing any records.
"""
def play_review_readiness(reviewer_email, counterpart_email)
when is_binary(reviewer_email) and is_binary(counterpart_email) do
load_app()
with {:ok, reviewer} <- play_review_user(reviewer_email, :reviewer),
{:ok, counterpart} <- play_review_user(counterpart_email, :counterpart),
:ok <- different_play_review_users(reviewer, counterpart),
{:ok, medicine_category} <- active_category("medicine-pickup", :help),
{:ok, cinema_category} <- active_category("cinema-meetup", :activity) do
{:ok,
%{
reviewer: reviewer,
counterpart: counterpart,
medicine_category: medicine_category,
cinema_category: cinema_category
}}
end
end
def play_review_readiness(_reviewer_email, _counterpart_email),
do: {:error, :invalid_email}
@doc """
Prepares stable, synthetic records for Google Play review using two existing
ordinary accounts.
Both accounts must already be confirmed, active, have accepted the terms,
have a password, and have no staff roles. The function never creates or
changes credentials. Repeating it returns the same request and activity.
"""
def prepare_play_review(reviewer_email, counterpart_email)
when is_binary(reviewer_email) and is_binary(counterpart_email) do
with {:ok,
%{
reviewer: reviewer,
counterpart: counterpart,
medicine_category: medicine_category,
cinema_category: cinema_category
}} <- play_review_readiness(reviewer_email, counterpart_email),
{:ok, request} <-
ensure_play_review_request(reviewer, counterpart, medicine_category),
{:ok, activity} <-
ensure_play_review_activity(reviewer, counterpart, cinema_category) do
{:ok,
%{
reviewer_id: reviewer.id,
counterpart_id: counterpart.id,
request_id: request.id,
request_path: "/requests/#{request.id}",
activity_id: activity.id,
activity_path: "/activities/#{activity.id}"
}}
end
end
def prepare_play_review(_reviewer_email, _counterpart_email),
do: {:error, :invalid_email}
defp play_review_user(email, role) do
email = email |> String.trim() |> String.downcase()
case WhoNeedHelp.Repo.get_by(WhoNeedHelp.Accounts.User, email: email) do
nil ->
{:error, {role, :user_not_found}}
user ->
cond do
user.moderation_status != :active ->
{:error, {role, :account_not_active}}
is_nil(user.confirmed_at) ->
{:error, {role, :email_not_confirmed}}
is_nil(user.accepted_terms_at) ->
{:error, {role, :terms_not_accepted}}
not (is_binary(user.hashed_password) and byte_size(user.hashed_password) > 0) ->
{:error, {role, :password_not_set}}
WhoNeedHelp.Accounts.staff_roles(user) != [] ->
{:error, {role, :staff_account_forbidden}}
true ->
{:ok, user}
end
end
end
defp different_play_review_users(%{id: id}, %{id: id}),
do: {:error, :accounts_must_be_distinct}
defp different_play_review_users(_reviewer, _counterpart), do: :ok
defp active_category(slug, mode) do
case WhoNeedHelp.Repo.get_by(WhoNeedHelp.Catalog.Category,
slug: slug,
mode: mode,
active: true
) do
nil -> {:error, {:category_not_found, slug}}
category -> {:ok, category}
end
end
defp ensure_play_review_request(reviewer, counterpart, category) do
existing =
WhoNeedHelp.Help.HelpRequest
|> where(
[request],
request.requester_id == ^reviewer.id and
request.title == ^@play_review_request_title and
is_nil(request.hidden_at)
)
|> order_by([request], desc: request.inserted_at)
|> limit(1)
|> WhoNeedHelp.Repo.one()
with {:ok, request} <- reusable_or_create_request(existing, reviewer, category),
{:ok, _assignment} <- ensure_play_review_assignment(request, counterpart) do
{:ok, WhoNeedHelp.Repo.get!(WhoNeedHelp.Help.HelpRequest, request.id)}
end
end
defp reusable_or_create_request(
%{status: status} = request,
_reviewer,
_category
)
when status in [:open, :matched, :in_progress],
do: {:ok, request}
defp reusable_or_create_request(nil, reviewer, category) do
WhoNeedHelp.Help.create_request(WhoNeedHelp.Accounts.Scope.for_user(reviewer), %{
"title" => @play_review_request_title,
"description" =>
"Synthetic Google Play review flow. Coordinate only with the assigned review counterpart.",
"pickup_instructions" =>
"Synthetic review data only; no medicine, payment, or real-world handover is required.",
"structured_data" => %{"pickup_status" => "already_paid"},
"location_label" => "Kyiv public review area — synthetic data",
"latitude" => 50.4501,
"longitude" => 30.5234,
"location_radius_meters" => 1_000,
"location_visibility" => "approximate_public",
"urgency" => "scheduled",
"expires_at" => DateTime.add(DateTime.utc_now(:second), 120, :day),
"category_id" => category.id,
"safety_confirmed" => true
})
end
defp reusable_or_create_request(_finished_request, _reviewer, _category),
do: {:error, :review_request_not_reusable}
defp ensure_play_review_assignment(request, counterpart) do
assignment =
WhoNeedHelp.Repo.get_by(WhoNeedHelp.Help.Assignment,
request_id: request.id,
active: true
)
case assignment do
nil when request.status == :open ->
WhoNeedHelp.Help.accept_request(
WhoNeedHelp.Accounts.Scope.for_user(counterpart),
request.id
)
%{helper_id: helper_id} = assignment when helper_id == counterpart.id ->
{:ok, assignment}
nil ->
{:error, :review_assignment_missing}
_other_assignment ->
{:error, :review_request_assigned_to_another_user}
end
end
defp ensure_play_review_activity(reviewer, counterpart, category) do
existing =
WhoNeedHelp.Activities.Activity
|> where(
[activity],
activity.creator_id == ^reviewer.id and
activity.title == ^@play_review_activity_title and
is_nil(activity.hidden_at)
)
|> order_by([activity], desc: activity.inserted_at)
|> limit(1)
|> WhoNeedHelp.Repo.one()
with {:ok, activity} <- reusable_or_create_activity(existing, reviewer, category),
{:ok, participant} <- ensure_play_review_participant(activity, counterpart),
{:ok, _participant} <- approve_play_review_participant(activity, participant, reviewer) do
{:ok, WhoNeedHelp.Repo.get!(WhoNeedHelp.Activities.Activity, activity.id)}
end
end
defp reusable_or_create_activity(%{status: :open} = activity, _reviewer, _category),
do: {:ok, activity}
defp reusable_or_create_activity(nil, reviewer, category) do
now = DateTime.utc_now(:second)
WhoNeedHelp.Activities.create_activity(WhoNeedHelp.Accounts.Scope.for_user(reviewer), %{
"title" => @play_review_activity_title,
"description" =>
"Synthetic Google Play review meetup in a public place. No real attendance is expected.",
"structured_data" => %{"film_or_genre" => "Synthetic public screening"},
"location_label" => "Kyiv public cinema area — synthetic data",
"latitude" => 50.4508,
"longitude" => 30.5248,
"location_visibility" => "approximate_public",
"starts_at" => DateTime.add(now, 120, :day),
"join_deadline" => DateTime.add(now, 110, :day),
"capacity" => 4,
"category_id" => category.id,
"safety_confirmed" => true
})
end
defp reusable_or_create_activity(_finished_activity, _reviewer, _category),
do: {:error, :review_activity_not_reusable}
defp ensure_play_review_participant(activity, counterpart) do
case WhoNeedHelp.Repo.get_by(WhoNeedHelp.Activities.Participant,
activity_id: activity.id,
user_id: counterpart.id
) do
nil ->
WhoNeedHelp.Activities.request_to_join(
WhoNeedHelp.Accounts.Scope.for_user(counterpart),
activity.id
)
%{status: status} = participant when status in [:requested, :approved] ->
{:ok, participant}
_participant ->
{:error, :review_participant_not_reusable}
end
end
defp approve_play_review_participant(_activity, %{status: :approved} = participant, _reviewer),
do: {:ok, participant}
defp approve_play_review_participant(activity, %{status: :requested} = participant, reviewer) do
case WhoNeedHelp.Activities.approve_participant(
WhoNeedHelp.Accounts.Scope.for_user(reviewer),
participant.id
) do
{:ok, _activity} ->
{:ok,
WhoNeedHelp.Repo.get_by!(WhoNeedHelp.Activities.Participant,
activity_id: activity.id,
user_id: participant.user_id
)}
error ->
error
end
end
def await_migrations do
load_app()
Enum.each(repos(), &await_repo/1)
end
defp await_repo(repo) do
{:ok, ready?, _} =
Ecto.Migrator.with_repo(repo, fn started_repo ->
case Ecto.Migrator.migrations(started_repo) do
[] -> false
migrations -> Enum.all?(migrations, fn {status, _version, _name} -> status == :up end)
end
end)
unless ready? do
Process.sleep(1_000)
await_repo(repo)
end
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
# Many platforms require SSL when connecting to the database
Application.ensure_all_started(:ssl)
Application.ensure_loaded(@app)
end
end