refactor(weather): move hrrr_data_fully_present? from enqueuer to Weather

Pure domain check — "does this contact already have HRRR profiles at
every path point for its QSO hour" — has no business living in an
Oban worker. Weather already owns has_hrrr_profile?/3 and
round_to_hrrr_grid/2, so consolidation keeps HRRR knowledge in one
context and leaves ContactWeatherEnqueueWorker focused on job
coordination.
This commit is contained in:
Graham McIntire 2026-04-21 09:19:05 -05:00
parent a81e55e348
commit baab3c09f5
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 99 additions and 17 deletions

View file

@ -4,9 +4,11 @@ defmodule Microwaveprop.Weather do
import Ecto.Query
alias Microwaveprop.Propagation.ProfilesFile
alias Microwaveprop.Radio
alias Microwaveprop.Repo
alias Microwaveprop.Weather.GefsProfile
alias Microwaveprop.Weather.GridCache
alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.IemClient
@ -865,6 +867,33 @@ defmodule Microwaveprop.Weather do
|> Repo.exists?()
end
@doc """
Returns `true` when every path point for `contact` (pos1 midpoint pos2,
or just pos1 for one-endpoint contacts) already has an HRRR profile at the
nearest HRRR hour. Lets the enrichment enqueuer skip :queued :queued
churn when the backing data is already present.
Returns `false` if `pos1` is nil a contact with no position can't be
looked up.
"""
@spec hrrr_data_fully_present?(map()) :: boolean()
def hrrr_data_fully_present?(%{pos1: nil}), do: false
def hrrr_data_fully_present?(contact) do
rounded = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp)
case Radio.contact_path_points(contact) do
[] ->
false
points ->
Enum.all?(points, fn {lat, lon} ->
{rlat, rlon} = round_to_hrrr_grid(lat, lon)
has_hrrr_profile?(rlat, rlon, rounded)
end)
end
end
@spec hrrr_for_contact(map()) :: HrrrProfile.t() | nil
def hrrr_for_contact(%{pos1: nil}), do: nil
@ -1018,7 +1047,7 @@ defmodule Microwaveprop.Weather do
def hrrr_profiles_for_path(contact) do
contact
|> Microwaveprop.Radio.contact_path_points()
|> Radio.contact_path_points()
|> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1)
end
@ -1093,7 +1122,7 @@ defmodule Microwaveprop.Weather do
def narr_profiles_for_path(contact) do
contact
|> Microwaveprop.Radio.contact_path_points()
|> Radio.contact_path_points()
|> Enum.map(fn {lat, lon} -> find_nearest_narr(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1)
end
@ -1261,7 +1290,7 @@ defmodule Microwaveprop.Weather do
def iemre_for_path(contact) do
contact
|> Microwaveprop.Radio.contact_path_points()
|> Radio.contact_path_points()
|> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1)
end

View file

@ -88,25 +88,12 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
cond do
is_nil(contact.pos1) -> []
NarrClient.in_coverage?(contact.qso_timestamp) -> []
hrrr_data_fully_present?(contact) -> []
Weather.hrrr_data_fully_present?(contact) -> []
true -> [contact.id]
end
end)
end
defp hrrr_data_fully_present?(contact) do
alias Microwaveprop.Weather.HrrrClient
rounded = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp)
contact
|> Radio.contact_path_points()
|> Enum.all?(fn {lat, lon} ->
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
Weather.has_hrrr_profile?(rlat, rlon, rounded)
end)
end
defp mark_enrichment_statuses(contact, types, jobs_by_type) do
ids = [contact.id]

View file

@ -656,6 +656,72 @@ defmodule Microwaveprop.WeatherTest do
end
end
describe "hrrr_data_fully_present?/1" do
# nearest_hrrr_hour rounds <:30 down; pick 18:15 so it maps to 18:00 to
# match the valid_time in @hrrr_profile_attrs.
@contact_time ~U[2026-03-28 18:15:00Z]
test "returns true when a pos1-only contact's snapped point has a profile" do
Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
contact = %{
pos1: %{"lat" => 32.9047, "lon" => -97.0382},
pos2: nil,
qso_timestamp: @contact_time
}
assert Weather.hrrr_data_fully_present?(contact)
end
test "returns false when a pos1-only contact's snapped point has no profile" do
contact = %{
pos1: %{"lat" => 40.0, "lon" => -80.0},
pos2: nil,
qso_timestamp: @contact_time
}
refute Weather.hrrr_data_fully_present?(contact)
end
test "returns true only when every path point (endpoint + midpoint + endpoint) has a profile" do
# Contact from (32.9, -97.04) to (33.1, -96.96) → midpoint ≈ (33.0, -97.0)
for {lat, lon} <- [{32.9, -97.04}, {33.0, -97.0}, {33.1, -96.96}] do
Weather.upsert_hrrr_profile(%{@hrrr_profile_attrs | lat: lat, lon: lon})
end
contact = %{
pos1: %{"lat" => 32.9, "lon" => -97.04},
pos2: %{"lat" => 33.1, "lon" => -96.96},
qso_timestamp: @contact_time
}
assert Weather.hrrr_data_fully_present?(contact)
end
test "returns false when the midpoint profile is missing" do
# Only endpoints present — midpoint (33.0, -97.0) missing
for {lat, lon} <- [{32.9, -97.04}, {33.1, -96.96}] do
Weather.upsert_hrrr_profile(%{@hrrr_profile_attrs | lat: lat, lon: lon})
end
contact = %{
pos1: %{"lat" => 32.9, "lon" => -97.04},
pos2: %{"lat" => 33.1, "lon" => -96.96},
qso_timestamp: @contact_time
}
refute Weather.hrrr_data_fully_present?(contact)
end
test "returns false when pos1 is nil" do
refute Weather.hrrr_data_fully_present?(%{
pos1: nil,
pos2: nil,
qso_timestamp: @contact_time
})
end
end
describe "round_to_hrrr_grid/2" do
test "rounds coordinates to 2 decimal places" do
assert Weather.round_to_hrrr_grid(32.9047, -97.0382) == {32.90, -97.04}