prop/lib/microwaveprop/radio/enrichment_status.ex
Graham McIntire 55a51f69ab
Replace boolean enrichment flags with enum status fields
- New fields: hrrr_status, weather_status, terrain_status, iemre_status
  with values: pending, queued, processing, complete, failed, unavailable
- EnrichmentStatus module defines valid state transitions
- Migration backfills: queued=true → complete, false → pending
- Workers set status on transitions (queued on enqueue, complete on finish)
- Show page reads status from contact struct, not computed dynamically
- Backfill dashboard queries status fields for accurate progress counts
- Remove cron-scheduled ContactWeatherEnqueueWorker (enrichment only on
  submission, page view, or manual backfill)
- Partial indexes on status != 'complete' for fast unprocessed lookups
2026-04-02 12:07:51 -05:00

57 lines
1.9 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)
"""
@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]
}
def states, do: @states
def fields, do: @fields
@doc "Returns true if transitioning from `from` to `to` is valid."
def valid_transition?(from, to) do
to in Map.get(@transitions, from, [])
end
@doc "Validates and applies a status transition on a changeset."
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)."
def force_status(changeset, field, status) when field in @fields and status in @states do
Ecto.Changeset.put_change(changeset, field, status)
end
end