REST API v1 endpoints for schedules (14 endpoints) and escalation policies (10 endpoints) with full CRUD for nested resources (layers, members, overrides, rules, targets). GraphQL types, resolvers, and schema mutations for both resource trees.
340 lines
11 KiB
Elixir
340 lines
11 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
|
|
|
|
import ToweropsWeb.Api.ErrorHelpers, only: [translate_errors: 1]
|
|
|
|
alias Towerops.OnCall
|
|
alias Towerops.OnCall.EscalationPolicy
|
|
alias Towerops.OnCall.EscalationRule
|
|
alias Towerops.OnCall.EscalationTarget
|
|
alias Towerops.Repo
|
|
alias ToweropsWeb.ScopedResource
|
|
|
|
# --- 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 = Map.put(policy_params, "organization_id", organization_id)
|
|
|
|
case OnCall.create_escalation_policy(attrs) do
|
|
{:ok, policy} ->
|
|
conn
|
|
|> put_status(:created)
|
|
|> json(format_policy(policy))
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
end
|
|
|
|
def create(conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Missing 'escalation_policy' parameter"})
|
|
end
|
|
|
|
@doc "GET /api/v1/escalation_policies/:id"
|
|
def show(conn, %{"id" => id}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
case ScopedResource.fetch_preload(EscalationPolicy, id, organization_id, rules: :targets) do
|
|
{:ok, policy} ->
|
|
json(conn, format_policy_detail(policy))
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Escalation policy not found"})
|
|
end
|
|
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
|
|
|
|
case ScopedResource.fetch(EscalationPolicy, id, organization_id) do
|
|
{:ok, policy} ->
|
|
case OnCall.update_escalation_policy(policy, policy_params) do
|
|
{:ok, updated} ->
|
|
json(conn, format_policy(updated))
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Escalation policy not found"})
|
|
end
|
|
end
|
|
|
|
def update(conn, _params) do
|
|
conn
|
|
|> put_status(:bad_request)
|
|
|> json(%{error: "Missing 'escalation_policy' parameter"})
|
|
end
|
|
|
|
@doc "DELETE /api/v1/escalation_policies/:id"
|
|
def delete(conn, %{"id" => id}) do
|
|
organization_id = conn.assigns.current_organization_id
|
|
|
|
case ScopedResource.fetch(EscalationPolicy, id, organization_id) do
|
|
{:ok, policy} ->
|
|
case OnCall.delete_escalation_policy(policy) do
|
|
{:ok, _} ->
|
|
json(conn, %{success: true})
|
|
|
|
{:error, %Ecto.Changeset{} = changeset} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "Escalation policy not found"})
|
|
end
|
|
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} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
|
|
{:error, :forbidden} ->
|
|
conn
|
|
|> put_status(:forbidden)
|
|
|> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn
|
|
|> put_status(:not_found)
|
|
|> json(%{error: "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} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
else
|
|
{:error, :forbidden} ->
|
|
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
|
|
|
|
{:error, :rule_not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "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, :forbidden} ->
|
|
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
|
|
|
|
{:error, :rule_not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "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} ->
|
|
conn
|
|
|> put_status(:unprocessable_entity)
|
|
|> json(%{errors: translate_errors(changeset)})
|
|
end
|
|
else
|
|
{:error, :forbidden} ->
|
|
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
|
|
|
|
{:error, :rule_not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "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
|
|
{:ok, _} = OnCall.delete_escalation_target(target)
|
|
json(conn, %{success: true})
|
|
else
|
|
{:error, :forbidden} ->
|
|
conn |> put_status(:forbidden) |> json(%{error: "Access denied to this escalation policy"})
|
|
|
|
{:error, :not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "Escalation policy not found"})
|
|
|
|
{:error, :rule_not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "Rule not found"})
|
|
|
|
{:error, :target_not_found} ->
|
|
conn |> put_status(:not_found) |> json(%{error: "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
|