prop/lib/microwaveprop/radio/enrichment_status.ex
Graham McIntire 51e390959c Add dialyzer specs and types across the codebase
277 @spec/@type annotations added to 58 files covering all public
APIs: contexts (propagation, radio, weather, terrain, beacons,
commercial), GRIB2 decoders, terrain analysis, duct detection,
rain scatter, CSV/ADIF import, weather clients, and all Ecto
schemas. Dialyzer passes with 0 errors.
2026-04-12 08:55:04 -05:00

66 lines
2.4 KiB
Elixir

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