defmodule HiiveRisk do @moduledoc "Durable review intake for existing case snapshots and marketplace Ecto transactions." import Ecto.Query alias Ecto.Multi alias HiiveRisk.{DecisionWorker, Repo, RiskCase} def enqueue_review(snapshot, opts \\ []) def enqueue_review(%Multi{} = multi, snapshot) do enqueue_review(multi, snapshot, []) end def enqueue_review(snapshot, opts) when is_map(snapshot) and is_list(opts) do case Multi.new() |> enqueue_review(snapshot, opts) |> Repo.transaction() do {:ok, %{risk_case: record, risk_job: job}} -> {:ok, %{case_id: record.id, job_id: job.id, status: record.status}} {:error, _operation, error, _changes} -> {:error, error} end end @doc "Add :risk_case and :risk_job operations to a Multi. A snapshot function receives prior changes." def enqueue_review(%Multi{} = multi, snapshot, opts) do multi |> Multi.run(:risk_case, fn repo, changes -> value = if is_function(snapshot, 1), do: snapshot.(changes), else: snapshot insert_case(repo, value, opts) end) |> Oban.insert(:risk_job, fn %{risk_case: record} -> DecisionWorker.new(%{"case_id" => record.id}) end) end def get_assessment(case_id) do with {:ok, id} <- Ecto.UUID.cast(case_id), %RiskCase{} = record <- Repo.get(RiskCase, id) do {:ok, assessment(record)} else _ -> {:error, :not_found} end end def list_reviews(opts \\ []) do disposition = Keyword.get(opts, :disposition, "human_review") query = from(c in RiskCase, where: c.status == "completed" and c.disposition == ^disposition, order_by: [desc: c.risk_level, asc: c.inserted_at, asc: c.id], limit: ^Keyword.get(opts, :limit, 100) ) query = case Keyword.get(opts, :priority) do nil -> query priority -> where(query, [c], c.risk_level == ^priority) end query |> Repo.all() |> Enum.map(&assessment/1) end def subscribe(case_id), do: Phoenix.PubSub.subscribe(HiiveRisk.PubSub, "risk:#{case_id}") @doc false def assessment(record) do Map.take(record, [ :id, :source_event_id, :policy_version, :snapshot, :status, :disposition, :risk_level, :reasons, :audit, :decision, :verdict, :last_error, :assessed_at ]) |> Map.put(:case_id, record.id) |> Map.delete(:id) end defp insert_case(repo, snapshot, opts) when is_map(snapshot) do changeset = RiskCase.changeset(snapshot, opts) if changeset.valid? do fields = [:source_event_id, :policy_version, :snapshot, :signals, :audit_sample] attrs = changeset |> Ecto.Changeset.apply_changes() |> Map.from_struct() |> Map.take(fields) attrs = Map.update!(attrs, :snapshot, &Jason.decode!(Jason.encode!(&1))) now = DateTime.utc_now() attrs = Map.merge(attrs, %{id: Ecto.UUID.generate(), inserted_at: now, updated_at: now}) repo.insert_all(RiskCase, [attrs], on_conflict: :nothing, conflict_target: [:source_event_id, :policy_version] ) record = repo.get_by!(RiskCase, source_event_id: attrs.source_event_id, policy_version: attrs.policy_version ) if record.snapshot == attrs.snapshot and record.signals == attrs.signals do {:ok, record} else {:error, :idempotency_conflict} end else {:error, changeset} end end defp insert_case(_repo, _snapshot, _opts), do: {:error, :invalid_snapshot} end