defmodule BilldogEng.Surveys do @moduledoc """ Surveys DATA API (no UI rendering). Wraps the `bdsurvey-*` edge functions. Lifecycle: `list` → `fetch` → `start` → `record_partial`* → `submit` (or `abandon`). The same `:idempotency_key` SHOULD be carried across `start` and `submit` so retries are safe. Answer shape: `%{question_id: ..., choice_id?: ..., answer_text?: ..., answer_number?: ..., answer_json?: ...}` (string or atom keys accepted). Mirrors the Node SDK `surveys.ts`. Each function takes a `%BilldogEng{}` client and returns `{:ok, result}` or `{:error, %BilldogEng.Error{}}`. """ alias BilldogEng.Transport @type client :: BilldogEng.t() @type result :: {:ok, term()} | {:error, BilldogEng.Error.t()} @doc "List active surveys eligible for the (optionally identified) user." @spec list(client(), keyword()) :: {:ok, [map()]} | {:error, BilldogEng.Error.t()} def list(client, opts \\ []) do body = %{api_key: client.api_key} |> put_if(:customer_id, Keyword.get(opts, :distinct_id)) |> put_if(:anonymous_id, Keyword.get(opts, :anonymous_id)) case req(client, "/bdsurvey-list", body) do {:ok, data} when is_map(data) -> {:ok, Map.get(data, "surveys") || []} {:ok, _} -> {:ok, []} {:error, err} -> {:error, err} end end @doc "Fetch the full configuration for a single survey." @spec fetch(client(), String.t(), keyword()) :: result() def fetch(client, survey_id, opts \\ []) do body = %{api_key: client.api_key, survey_id: survey_id} |> put_if(:customer_id, Keyword.get(opts, :distinct_id)) |> put_if(:anonymous_id, Keyword.get(opts, :anonymous_id)) req(client, "/bdsurvey-fetch", body) end @doc "Begin a survey response. Returns `{:ok, %{\"respondent_id\" => ...}}`." @spec start(client(), String.t(), keyword()) :: result() def start(client, survey_id, context \\ []) do req(client, "/bdsurvey-submit", build_body("start", client, survey_id, nil, context)) end @doc "Progressively persist a partial set of answers under an in-progress respondent." @spec record_partial(client(), String.t(), String.t(), [map()], keyword()) :: result() def record_partial(client, survey_id, respondent_id, answers, context \\ []) do context = Keyword.put(context, :respondent_id, respondent_id) req(client, "/bdsurvey-submit", build_body("partial", client, survey_id, answers, context)) end @doc "Submit the final answers, completing the response." @spec submit(client(), String.t(), [map()], keyword()) :: result() def submit(client, survey_id, answers, context \\ []) do req(client, "/bdsurvey-submit", build_body("submit", client, survey_id, answers, context)) end @doc "Mark an in-progress response as abandoned." @spec abandon(client(), String.t(), String.t()) :: result() def abandon(client, survey_id, respondent_id) do body = build_body("abandon", client, survey_id, nil, respondent_id: respondent_id) req(client, "/bdsurvey-submit", body) end # ── Internals ──────────────────────────────────────────────────────────────── defp req(client, path, body) do Transport.request(client.transport, path: path, body: body, headers: [{"x-api-key", client.api_key}], gzip: false ) end defp build_body(action, client, survey_id, answers, context) do %{action: action, api_key: client.api_key, survey_id: survey_id} |> put_if(:answers, answers && Enum.map(answers, &normalize_answer/1)) |> put_if(:respondent_id, Keyword.get(context, :respondent_id)) |> put_if(:customer_id, Keyword.get(context, :customer_id)) |> put_if(:anonymous_id, Keyword.get(context, :anonymous_id)) |> put_if(:session_id, Keyword.get(context, :session_id)) |> put_if(:platform, Keyword.get(context, :platform)) |> put_if(:device_info, Keyword.get(context, :device_info)) |> put_if(:duration_ms, Keyword.get(context, :duration_ms)) |> put_if(:context, Keyword.get(context, :context)) |> put_if(:collector_id, Keyword.get(context, :collector_id)) |> put_if(:collector_type, Keyword.get(context, :collector_type)) |> put_if(:idempotency_key, Keyword.get(context, :idempotency_key)) end # Accept atom- or string-keyed answers; ship them on the wire with string keys. defp normalize_answer(answer) when is_map(answer) do Map.new(answer, fn {k, v} -> {to_string(k), v} end) end defp put_if(map, _key, nil), do: map defp put_if(map, key, value), do: Map.put(map, key, value) end