towerops/lib/towerops_web/controllers/api/v1/escalation_policies_controller.ex
Graham McIntire a4e08b2aca Major codebase cleanup: DRY schemas, remove dead config, consolidate modules
- Created Towerops.Schema base macro (95+ schemas updated to use it)
- Created Towerops.Snmp.Reading macro (7 reading schemas consolidated)
- Created Towerops.Gaiia.BaseSchema macro (4 Gaiia schemas consolidated)
- Created Towerops.SyncLog shared module (UISP + Preseem merge)
- Created Towerops.LogFilters (3 log filter modules merged into 1)
- Created Towerops.OnCall.ChangesetHelpers (9 on-call schemas simplified)
- Created API v1 ResourceController shared helpers (7 controllers)
- Extracted ConnectionHelpers.format_connection_result (2 LiveViews)
- Removed 5 dead config keys (scopes, mib_dirs, stripe_meter_id, etc.)
- Fixed 2 pre-existing broken tests
- All 13,219 tests pass, Credo clean (0 issues)
2026-06-16 15:29:22 -05:00

286 lines
8.5 KiB
Elixir

defmodule ToweropsWeb.Api.V1.EscalationPoliciesController do
@moduledoc """
API controller for managing escalation policies.
All endpoints require API token authentication and operations are scoped
to the organization associated with the token.
"""
use ToweropsWeb, :controller
alias Towerops.OnCall
alias Towerops.OnCall.EscalationPolicy
alias Towerops.OnCall.EscalationRule
alias Towerops.OnCall.EscalationTarget
alias Towerops.Repo
alias ToweropsWeb.Api.ParamFilter
alias ToweropsWeb.Api.V1.ResourceController, as: Resources
alias ToweropsWeb.ScopedResource
require Logger
# --- Policy CRUD ---
@doc "GET /api/v1/escalation_policies"
def index(conn, _params) do
organization_id = conn.assigns.current_organization_id
policies =
organization_id
|> OnCall.list_escalation_policies()
|> Enum.map(&format_policy/1)
json(conn, %{escalation_policies: policies})
end
@doc "POST /api/v1/escalation_policies"
def create(conn, %{"escalation_policy" => policy_params}) do
organization_id = conn.assigns.current_organization_id
attrs =
policy_params
|> ParamFilter.strip_sensitive()
|> Map.put("organization_id", organization_id)
Resources.created_or_error(conn, OnCall.create_escalation_policy(attrs), &format_policy/1)
end
def create(conn, _params) do
Resources.missing_param(conn, "escalation_policy")
end
@doc "GET /api/v1/escalation_policies/:id"
def show(conn, %{"id" => id}) do
organization_id = conn.assigns.current_organization_id
Resources.fetch_and_render(
conn,
EscalationPolicy,
id,
organization_id,
[rules: :targets],
&format_policy_detail/1,
"Escalation policy not found"
)
end
@doc "PATCH /api/v1/escalation_policies/:id"
def update(conn, %{"id" => id, "escalation_policy" => policy_params}) do
organization_id = conn.assigns.current_organization_id
Resources.update_and_render(
conn,
EscalationPolicy,
id,
organization_id,
%{
context: OnCall,
update_fn: :update_escalation_policy,
params: ParamFilter.strip_sensitive(policy_params),
format_fn: &format_policy/1,
not_found_msg: "Escalation policy not found"
}
)
end
def update(conn, _params) do
Resources.missing_param(conn, "escalation_policy")
end
@doc "DELETE /api/v1/escalation_policies/:id"
def delete(conn, %{"id" => id}) do
organization_id = conn.assigns.current_organization_id
Resources.delete_and_render(
conn,
EscalationPolicy,
id,
organization_id,
OnCall,
:delete_escalation_policy,
"Escalation policy not found"
)
end
# --- Rules ---
@doc "POST /api/v1/escalation_policies/:id/rules"
def create_rule(conn, %{"id" => policy_id, "rule" => rule_params}) do
organization_id = conn.assigns.current_organization_id
case ScopedResource.fetch(EscalationPolicy, policy_id, organization_id) do
{:ok, _policy} ->
attrs = Map.put(rule_params, "escalation_policy_id", policy_id)
case OnCall.create_escalation_rule(attrs) do
{:ok, rule} ->
conn
|> put_status(:created)
|> json(format_rule(rule))
{:error, %Ecto.Changeset{} = changeset} ->
Resources.unprocessable_entity(conn, changeset)
end
{:error, :not_found} ->
Resources.not_found(conn, "Escalation policy not found")
end
end
@doc "PATCH /api/v1/escalation_policies/:id/rules/:rule_id"
def update_rule(conn, %{"id" => policy_id, "rule_id" => rule_id, "rule" => rule_params}) do
organization_id = conn.assigns.current_organization_id
with {:ok, _policy} <- ScopedResource.fetch(EscalationPolicy, policy_id, organization_id),
{:ok, rule} <- fetch_rule(rule_id, policy_id) do
case OnCall.update_escalation_rule(rule, rule_params) do
{:ok, updated} ->
json(conn, format_rule(updated))
{:error, %Ecto.Changeset{} = changeset} ->
Resources.unprocessable_entity(conn, changeset)
end
else
{:error, :not_found} ->
Resources.not_found(conn, "Escalation policy not found")
{:error, :rule_not_found} ->
Resources.not_found(conn, "Rule not found")
end
end
@doc "DELETE /api/v1/escalation_policies/:id/rules/:rule_id"
def delete_rule(conn, %{"id" => policy_id, "rule_id" => rule_id}) do
organization_id = conn.assigns.current_organization_id
with {:ok, _policy} <- ScopedResource.fetch(EscalationPolicy, policy_id, organization_id),
{:ok, rule} <- fetch_rule(rule_id, policy_id) do
{:ok, _} = OnCall.delete_escalation_rule(rule)
json(conn, %{success: true})
else
{:error, :not_found} ->
Resources.not_found(conn, "Escalation policy not found")
{:error, :rule_not_found} ->
Resources.not_found(conn, "Rule not found")
end
end
# --- Targets ---
@doc "POST /api/v1/escalation_policies/:id/rules/:rule_id/targets"
def create_target(conn, %{"id" => policy_id, "rule_id" => rule_id, "target" => target_params}) do
organization_id = conn.assigns.current_organization_id
with {:ok, _policy} <- ScopedResource.fetch(EscalationPolicy, policy_id, organization_id),
{:ok, _rule} <- fetch_rule(rule_id, policy_id) do
attrs = Map.put(target_params, "escalation_rule_id", rule_id)
case OnCall.create_escalation_target(attrs) do
{:ok, target} ->
conn
|> put_status(:created)
|> json(format_target(target))
{:error, %Ecto.Changeset{} = changeset} ->
Resources.unprocessable_entity(conn, changeset)
end
else
{:error, :not_found} ->
Resources.not_found(conn, "Escalation policy not found")
{:error, :rule_not_found} ->
Resources.not_found(conn, "Rule not found")
end
end
@doc "DELETE /api/v1/escalation_policies/:id/rules/:rule_id/targets/:target_id"
def delete_target(conn, %{"id" => policy_id, "rule_id" => rule_id, "target_id" => target_id}) do
organization_id = conn.assigns.current_organization_id
with {:ok, _policy} <- ScopedResource.fetch(EscalationPolicy, policy_id, organization_id),
{:ok, _rule} <- fetch_rule(rule_id, policy_id),
{:ok, target} <- fetch_target(target_id, rule_id) do
case OnCall.delete_escalation_target(target) do
{:ok, _} ->
json(conn, %{success: true})
{:error, reason} ->
Logger.error("Failed to delete escalation target #{target_id}: #{inspect(reason)}")
conn |> put_status(:internal_server_error) |> json(%{error: "Failed to delete escalation target"})
end
else
{:error, :not_found} ->
Resources.not_found(conn, "Escalation policy not found")
{:error, :rule_not_found} ->
Resources.not_found(conn, "Rule not found")
{:error, :target_not_found} ->
Resources.not_found(conn, "Target not found")
end
end
# --- Private helpers ---
defp fetch_rule(rule_id, policy_id) do
case Repo.get(EscalationRule, rule_id) do
%EscalationRule{escalation_policy_id: ^policy_id} = rule -> {:ok, rule}
_ -> {:error, :rule_not_found}
end
end
defp fetch_target(target_id, rule_id) do
case Repo.get(EscalationTarget, target_id) do
%EscalationTarget{escalation_rule_id: ^rule_id} = target -> {:ok, target}
_ -> {:error, :target_not_found}
end
end
defp format_policy(policy) do
%{
id: policy.id,
name: policy.name,
description: policy.description,
repeat_count: policy.repeat_count,
inserted_at: policy.inserted_at
}
end
defp format_policy_detail(policy) do
%{
id: policy.id,
name: policy.name,
description: policy.description,
repeat_count: policy.repeat_count,
inserted_at: policy.inserted_at,
rules: Enum.map(policy.rules, &format_rule_detail/1)
}
end
defp format_rule(rule) do
%{
id: rule.id,
position: rule.position,
timeout_minutes: rule.timeout_minutes,
escalation_policy_id: rule.escalation_policy_id,
inserted_at: rule.inserted_at
}
end
defp format_rule_detail(rule) do
rule
|> format_rule()
|> Map.put(:targets, Enum.map(rule.targets, &format_target/1))
end
defp format_target(target) do
%{
id: target.id,
target_type: target.target_type,
user_id: target.user_id,
schedule_id: target.schedule_id,
escalation_rule_id: target.escalation_rule_id,
inserted_at: target.inserted_at
}
end
end