defmodule HiiveRisk.RiskCase do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} schema "risk_cases" do field(:source_event_id, :string) field(:policy_version, :string) field(:snapshot, :map) field(:signals, :map) field(:audit_sample, :boolean) field(:audit, :boolean, default: false) field(:status, :string, default: "pending_decision") field(:decision, :map) field(:verdict, :map) field(:disposition, :string) field(:risk_level, :integer) field(:reasons, {:array, :string}, default: []) field(:last_error, :string) field(:assessed_at, :utc_datetime_usec) timestamps(type: :utc_datetime_usec) end def changeset(snapshot, opts) do signals = %{ "unusual_input" => Keyword.get(opts, :unusual_input, false), "conflicting_evidence" => Keyword.get(opts, :conflicting_evidence, false), "audit_sample" => Keyword.get(opts, :audit_sample) } attrs = %{ source_event_id: Map.get(snapshot, "case_id", Map.get(snapshot, :case_id)), policy_version: Keyword.get(opts, :policy_version, "research-v1"), snapshot: snapshot, signals: signals, audit_sample: case signals["audit_sample"] do nil -> :rand.uniform() < 0.02 value -> value end } %__MODULE__{} |> cast(attrs, [:source_event_id, :policy_version, :snapshot, :signals, :audit_sample]) |> validate_required([:source_event_id, :policy_version, :snapshot, :signals, :audit_sample]) |> validate_change(:snapshot, fn :snapshot, value -> case HiiveRisk.Contract.snapshot(value) do :ok -> [] {:error, _} -> [snapshot: "does not match the review input contract"] end end) |> validate_change(:signals, fn :signals, value -> if is_boolean(value["unusual_input"]) and is_boolean(value["conflicting_evidence"]) and value["audit_sample"] in [nil, true, false] do [] else [signals: "routing signals must be booleans"] end end) end end