defmodule Microwaveprop.Radio.EnrichmentStatus do @moduledoc """ State machine for contact enrichment lifecycle. States: :pending → initial, no work started :queued → Oban job inserted :processing → worker actively running :complete → data exists in DB :failed → worker exhausted retries :unavailable → data cannot be fetched (missing pos, archive gone, etc.) Valid transitions: pending → queued, unavailable queued → processing, failed processing → complete, failed failed → queued (retry) unavailable → queued (retry with new data source) complete → pending (reset for re-enrichment) """ @type status :: :pending | :queued | :processing | :complete | :failed | :unavailable @type field_name :: :hrrr_status | :weather_status | :terrain_status | :iemre_status @states [:pending, :queued, :processing, :complete, :failed, :unavailable] @fields [:hrrr_status, :weather_status, :terrain_status, :iemre_status] @transitions %{ pending: [:queued, :unavailable], queued: [:processing, :failed], processing: [:complete, :failed], failed: [:queued], unavailable: [:queued], complete: [:pending] } @spec states() :: [status()] def states, do: @states @spec fields() :: [field_name()] def fields, do: @fields @doc "Returns true if transitioning from `from` to `to` is valid." @spec valid_transition?(status(), status()) :: boolean() def valid_transition?(from, to) do to in Map.get(@transitions, from, []) end @doc "Validates and applies a status transition on a changeset." @spec transition(Ecto.Changeset.t(), field_name(), status()) :: Ecto.Changeset.t() def transition(changeset, field, new_status) when field in @fields and new_status in @states do current = Ecto.Changeset.get_field(changeset, field) if valid_transition?(current, new_status) do Ecto.Changeset.put_change(changeset, field, new_status) else Ecto.Changeset.add_error(changeset, field, "invalid transition from #{current} to #{new_status}") end end @doc "Force-sets status without transition validation (for migrations, resets)." @spec force_status(Ecto.Changeset.t(), field_name(), status()) :: Ecto.Changeset.t() def force_status(changeset, field, status) when field in @fields and status in @states do Ecto.Changeset.put_change(changeset, field, status) end end