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