- 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)
286 lines
8.5 KiB
Elixir
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
|