defmodule HiiveRisk.Pipeline do @moduledoc false import Ecto.Query alias HiiveRisk.{Contract, Policy, Repo, RiskCase, SFTWorker} def perform(stage, %Oban.Job{args: %{"case_id" => id}} = job) do case Repo.get(RiskCase, id) do nil -> {:discard, :case_not_found} record -> if processable?(record, stage) do client = Application.fetch_env!(:hiive_risk, :model_client) with {:ok, response} <- apply(client, stage, [record.snapshot]), {:ok, result} <- validate(stage, response) do commit(stage, id, result) else {:error, error} -> fail(stage, id, job, error) end else :ok end end rescue error -> fail(stage, id, job, :worker_exception) reraise error, __STACKTRACE__ end defp validate(:decision, value), do: Contract.decision(value) defp validate(:sft, value), do: Contract.verdict(value) defp processable?(record, :decision) do record.status in ["pending_decision", "failed"] and is_nil(record.decision) end defp processable?(record, :sft) do record.status in ["pending_sft", "failed"] and not is_nil(record.decision) and is_nil(record.verdict) end defp commit(stage, id, result) do case Repo.transaction(fn -> record = Repo.one!(from(c in RiskCase, where: c.id == ^id, lock: "FOR UPDATE")) if processable?(record, stage) do attrs = apply(Policy, stage, [record, result]) updated = record |> Ecto.Changeset.change(attrs) |> Repo.update!() if updated.status == "pending_sft" do Oban.insert!(SFTWorker.new(%{"case_id" => id})) end {:updated, updated} else {:unchanged, record} end end) do {:ok, {:updated, record}} -> notify(record) :ok {:ok, {:unchanged, _}} -> :ok {:error, _} -> {:error, :review_commit_failed} end end defp fail(stage, id, job, error) do code = case error do value when is_atom(value) -> Atom.to_string(value) {:model_http_status, status} -> "model_http_status_#{status}" _ -> "model_request_failed" end result = Repo.transaction(fn -> record = Repo.one!(from(c in RiskCase, where: c.id == ^id, lock: "FOR UPDATE")) if processable?(record, stage) do attrs = %{last_error: code} attrs = if job.attempt >= job.max_attempts, do: Map.put(attrs, :status, "failed"), else: attrs record |> Ecto.Changeset.change(attrs) |> Repo.update!() end end) case result do {:ok, %RiskCase{} = record} -> notify(record) _ -> :ok end {:error, code} end defp notify(record) do Phoenix.PubSub.broadcast( HiiveRisk.PubSub, "risk:#{record.id}", {:risk_updated, HiiveRisk.assessment(record)} ) end end