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 import Ecto.Query
alias Microwaveprop.Propagation.ProfilesFile alias Microwaveprop.Propagation.ProfilesFile
alias Microwaveprop.Radio
alias Microwaveprop.Repo alias Microwaveprop.Repo
alias Microwaveprop.Weather.GefsProfile alias Microwaveprop.Weather.GefsProfile
alias Microwaveprop.Weather.GridCache alias Microwaveprop.Weather.GridCache
alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.HrrrNativeProfile alias Microwaveprop.Weather.HrrrNativeProfile
alias Microwaveprop.Weather.HrrrProfile alias Microwaveprop.Weather.HrrrProfile
alias Microwaveprop.Weather.IemClient alias Microwaveprop.Weather.IemClient
@ -865,6 +867,33 @@ defmodule Microwaveprop.Weather do
|> Repo.exists?() |> Repo.exists?()
end 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 @spec hrrr_for_contact(map()) :: HrrrProfile.t() | nil
def hrrr_for_contact(%{pos1: nil}), do: nil def hrrr_for_contact(%{pos1: nil}), do: nil
@ -1018,7 +1047,7 @@ defmodule Microwaveprop.Weather do
def hrrr_profiles_for_path(contact) do def hrrr_profiles_for_path(contact) do
contact 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.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
end end
@ -1093,7 +1122,7 @@ defmodule Microwaveprop.Weather do
def narr_profiles_for_path(contact) do def narr_profiles_for_path(contact) do
contact 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.map(fn {lat, lon} -> find_nearest_narr(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
end end
@ -1261,7 +1290,7 @@ defmodule Microwaveprop.Weather do
def iemre_for_path(contact) do def iemre_for_path(contact) do
contact 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.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1) |> Enum.reject(&is_nil/1)
end end

View file

@ -88,25 +88,12 @@ defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
cond do cond do
is_nil(contact.pos1) -> [] is_nil(contact.pos1) -> []
NarrClient.in_coverage?(contact.qso_timestamp) -> [] NarrClient.in_coverage?(contact.qso_timestamp) -> []
hrrr_data_fully_present?(contact) -> [] Weather.hrrr_data_fully_present?(contact) -> []
true -> [contact.id] true -> [contact.id]
end end
end) 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 defp mark_enrichment_statuses(contact, types, jobs_by_type) do
ids = [contact.id] ids = [contact.id]

View file

@ -656,6 +656,72 @@ defmodule Microwaveprop.WeatherTest do
end end
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 describe "round_to_hrrr_grid/2" do
test "rounds coordinates to 2 decimal places" do test "rounds coordinates to 2 decimal places" do
assert Weather.round_to_hrrr_grid(32.9047, -97.0382) == {32.90, -97.04} assert Weather.round_to_hrrr_grid(32.9047, -97.0382) == {32.90, -97.04}