diff --git a/lib/microwaveprop/radio.ex b/lib/microwaveprop/radio.ex index e2bdc392..a731fb7b 100644 --- a/lib/microwaveprop/radio.ex +++ b/lib/microwaveprop/radio.ex @@ -100,6 +100,33 @@ defmodule Microwaveprop.Radio do |> Repo.update_all(set: [iemre_queued: true]) end + @doc """ + Returns a list of {lat, lon} points along the QSO path: pos1, midpoint, pos2. + Returns [pos1] if pos2 is nil, or [] if pos1 is nil. + """ + def qso_path_points(%{pos1: nil}), do: [] + + def qso_path_points(%{pos1: pos1, pos2: nil}) do + lat = pos1["lat"] + lon = pos1["lon"] || pos1["lng"] + if lat && lon, do: [{lat, lon}], else: [] + end + + def qso_path_points(%{pos1: pos1, pos2: pos2}) do + lat1 = pos1["lat"] + lon1 = pos1["lon"] || pos1["lng"] + lat2 = pos2["lat"] + lon2 = pos2["lon"] || pos2["lng"] + + if lat1 && lon1 && lat2 && lon2 do + mid_lat = (lat1 + lat2) / 2 + mid_lon = (lon1 + lon2) / 2 + [{lat1, lon1}, {mid_lat, mid_lon}, {lat2, lon2}] + else + [{lat1, lon1}] + end + end + @earth_radius_km 6371.0 def haversine_km(lat1, lon1, lat2, lon2) do diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 3c698e89..19d76e16 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -192,36 +192,48 @@ defmodule Microwaveprop.Weather do lon = qso.pos1["lon"] || qso.pos1["lng"] if lat && lon do - # Search within ~0.05 degrees (~5km) and 1 hour - dlat = 0.05 - dlon = 0.05 - time_start = DateTime.add(qso.qso_timestamp, -3600, :second) - time_end = DateTime.add(qso.qso_timestamp, 3600, :second) - - HrrrProfile - |> where( - [h], - h.lat >= ^(lat - dlat) and h.lat <= ^(lat + dlat) and - h.lon >= ^(lon - dlon) and h.lon <= ^(lon + dlon) and - h.valid_time >= ^time_start and h.valid_time <= ^time_end - ) - |> order_by([h], - asc: - fragment( - "ABS(? - ?) + ABS(? - ?) + ABS(EXTRACT(EPOCH FROM ? - ?))", - h.lat, - ^lat, - h.lon, - ^lon, - h.valid_time, - ^qso.qso_timestamp - ) - ) - |> limit(1) - |> Repo.one() + find_nearest_hrrr(lat, lon, qso.qso_timestamp) end end + def find_nearest_hrrr(lat, lon, timestamp) do + dlat = 0.05 + dlon = 0.05 + time_start = DateTime.add(timestamp, -3600, :second) + time_end = DateTime.add(timestamp, 3600, :second) + + HrrrProfile + |> where( + [h], + h.lat >= ^(lat - dlat) and h.lat <= ^(lat + dlat) and + h.lon >= ^(lon - dlon) and h.lon <= ^(lon + dlon) and + h.valid_time >= ^time_start and h.valid_time <= ^time_end + ) + |> order_by([h], + asc: + fragment( + "ABS(? - ?) + ABS(? - ?) + ABS(EXTRACT(EPOCH FROM ? - ?))", + h.lat, + ^lat, + h.lon, + ^lon, + h.valid_time, + ^timestamp + ) + ) + |> limit(1) + |> Repo.one() + end + + def hrrr_profiles_for_path(%{pos1: nil}), do: [] + + def hrrr_profiles_for_path(qso) do + qso + |> Microwaveprop.Radio.qso_path_points() + |> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, qso.qso_timestamp) end) + |> Enum.reject(&is_nil/1) + end + def round_to_hrrr_grid(lat, lon) do {Float.round(lat / 1.0, 2), Float.round(lon / 1.0, 2)} end @@ -253,12 +265,25 @@ defmodule Microwaveprop.Weather do lon = qso.pos1["lon"] || qso.pos1["lng"] if lat && lon do - {rlat, rlon} = round_to_iemre_grid(lat, lon) - date = DateTime.to_date(qso.qso_timestamp) - - IemreObservation - |> where([i], i.lat == ^rlat and i.lon == ^rlon and i.date == ^date) - |> Repo.one() + find_nearest_iemre(lat, lon, qso.qso_timestamp) end end + + def find_nearest_iemre(lat, lon, timestamp) do + {rlat, rlon} = round_to_iemre_grid(lat, lon) + date = DateTime.to_date(timestamp) + + IemreObservation + |> where([i], i.lat == ^rlat and i.lon == ^rlon and i.date == ^date) + |> Repo.one() + end + + def iemre_for_path(%{pos1: nil}), do: [] + + def iemre_for_path(qso) do + qso + |> Microwaveprop.Radio.qso_path_points() + |> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, qso.qso_timestamp) end) + |> Enum.reject(&is_nil/1) + end end diff --git a/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex b/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex index 2c8846ca..e9bb6c06 100644 --- a/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex +++ b/lib/microwaveprop/workers/qso_weather_enqueue_worker.ex @@ -112,13 +112,12 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do end defp jobs_for_qso(qso) do - lat = qso.pos1["lat"] - lon = qso.pos1["lon"] || qso.pos1["lng"] - - asos_jobs = build_asos_jobs(lat, lon, qso.qso_timestamp) - raob_jobs = build_raob_jobs(lat, lon, qso.qso_timestamp) - - asos_jobs ++ raob_jobs + qso + |> Radio.qso_path_points() + |> Enum.flat_map(fn {lat, lon} -> + build_asos_jobs(lat, lon, qso.qso_timestamp) ++ + build_raob_jobs(lat, lon, qso.qso_timestamp) + end) end defp enqueue_iemre_jobs do @@ -143,12 +142,12 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do defp hrrr_job_for_qso(%{pos1: nil}), do: [] defp hrrr_job_for_qso(qso) do - lat = qso.pos1["lat"] - lon = qso.pos1["lon"] || qso.pos1["lng"] + rounded_time = HrrrClient.nearest_hrrr_hour(qso.qso_timestamp) - if lat && lon do + qso + |> Radio.qso_path_points() + |> Enum.flat_map(fn {lat, lon} -> {rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon) - rounded_time = HrrrClient.nearest_hrrr_hour(qso.qso_timestamp) if Weather.has_hrrr_profile?(rlat, rlon, rounded_time) do [] @@ -161,9 +160,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do }) ] end - else - [] - end + end) end defp build_asos_jobs(lat, lon, timestamp) do @@ -189,12 +186,12 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do defp iemre_job_for_qso(%{pos1: nil}), do: [] defp iemre_job_for_qso(qso) do - lat = qso.pos1["lat"] - lon = qso.pos1["lon"] || qso.pos1["lng"] + date = DateTime.to_date(qso.qso_timestamp) - if lat && lon do + qso + |> Radio.qso_path_points() + |> Enum.flat_map(fn {lat, lon} -> {rlat, rlon} = Weather.round_to_iemre_grid(lat, lon) - date = DateTime.to_date(qso.qso_timestamp) if Weather.has_iemre_observation?(rlat, rlon, date) do [] @@ -207,9 +204,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do }) ] end - else - [] - end + end) end defp build_raob_jobs(lat, lon, timestamp) do diff --git a/lib/mix/tasks/reset_enrichment.ex b/lib/mix/tasks/reset_enrichment.ex new file mode 100644 index 00000000..d7b56a33 --- /dev/null +++ b/lib/mix/tasks/reset_enrichment.ex @@ -0,0 +1,33 @@ +defmodule Mix.Tasks.ResetEnrichment do + @shortdoc "Reset weather/HRRR/IEMRE enrichment flags for re-processing" + + @moduledoc """ + Resets weather, HRRR, and IEMRE enrichment flags on all QSOs so the + next enqueue worker run re-processes them with multi-point path data. + + Does NOT reset terrain_queued since terrain profiles are path-based (pos1→pos2) + and don't change with multi-point enrichment. + + mix reset_enrichment + """ + + use Mix.Task + + import Ecto.Query + + alias Microwaveprop.Radio.Qso + alias Microwaveprop.Repo + + @impl Mix.Task + def run(_args) do + Mix.Task.run("app.start") + + {count, _} = + Qso + |> where([q], q.weather_queued == true or q.hrrr_queued == true or q.iemre_queued == true) + |> Repo.update_all(set: [weather_queued: false, hrrr_queued: false, iemre_queued: false]) + + Mix.shell().info("Reset enrichment flags on #{count} QSOs") + :ok + end +end diff --git a/test/microwaveprop/radio_test.exs b/test/microwaveprop/radio_test.exs index a93e690c..1f84ea87 100644 --- a/test/microwaveprop/radio_test.exs +++ b/test/microwaveprop/radio_test.exs @@ -346,6 +346,52 @@ defmodule Microwaveprop.RadioTest do end end + describe "qso_path_points/1" do + test "returns pos1, midpoint, and pos2 when both positions exist" do + qso = + create_qso(%{ + pos1: %{"lat" => 32.9, "lon" => -97.0}, + pos2: %{"lat" => 30.3, "lon" => -97.7} + }) + + points = Radio.qso_path_points(qso) + + assert length(points) == 3 + [{lat1, lon1}, {mid_lat, mid_lon}, {lat2, lon2}] = points + assert lat1 == 32.9 + assert lon1 == -97.0 + assert_in_delta mid_lat, 31.6, 0.01 + assert_in_delta mid_lon, -97.35, 0.01 + assert lat2 == 30.3 + assert lon2 == -97.7 + end + + test "returns only pos1 when pos2 is nil" do + qso = create_qso(%{pos2: nil}) + + points = Radio.qso_path_points(qso) + + assert points == [{32.9, -97.0}] + end + + test "returns empty list when pos1 is nil" do + qso = create_qso(%{pos1: nil}) + + assert Radio.qso_path_points(qso) == [] + end + + test "handles lng key in position maps" do + qso = + create_qso(%{ + pos1: %{"lat" => 32.9, "lng" => -97.0}, + pos2: %{"lat" => 30.3, "lng" => -97.7} + }) + + points = Radio.qso_path_points(qso) + assert length(points) == 3 + end + end + describe "change_qso/2" do test "returns a submission changeset" do changeset = Radio.change_qso(%Qso{}) diff --git a/test/microwaveprop/weather_test.exs b/test/microwaveprop/weather_test.exs index 0325dc5d..84a4ad11 100644 --- a/test/microwaveprop/weather_test.exs +++ b/test/microwaveprop/weather_test.exs @@ -578,6 +578,112 @@ defmodule Microwaveprop.WeatherTest do end end + describe "hrrr_profiles_for_path/1" do + test "returns HRRR profiles for all path points" do + # Profile at pos1 + Weather.upsert_hrrr_profile(@hrrr_profile_attrs) + + # Profile near pos2 + Weather.upsert_hrrr_profile(%{ + @hrrr_profile_attrs + | lat: 30.30, + lon: -97.70 + }) + + qso = %{ + pos1: %{"lat" => 32.91, "lon" => -97.05}, + pos2: %{"lat" => 30.31, "lon" => -97.71}, + qso_timestamp: ~U[2026-03-28 18:30:00Z] + } + + profiles = Weather.hrrr_profiles_for_path(qso) + assert length(profiles) >= 2 + end + + test "returns empty list when no HRRR profiles exist nearby" do + qso = %{ + pos1: %{"lat" => 40.0, "lon" => -80.0}, + pos2: %{"lat" => 41.0, "lon" => -81.0}, + qso_timestamp: ~U[2026-03-28 18:00:00Z] + } + + assert Weather.hrrr_profiles_for_path(qso) == [] + end + + test "returns profiles for pos1 only when pos2 is nil" do + Weather.upsert_hrrr_profile(@hrrr_profile_attrs) + + qso = %{ + pos1: %{"lat" => 32.91, "lon" => -97.05}, + pos2: nil, + qso_timestamp: ~U[2026-03-28 18:30:00Z] + } + + profiles = Weather.hrrr_profiles_for_path(qso) + assert length(profiles) == 1 + end + end + + describe "iemre_for_path/1" do + test "returns IEMRE observations for all path points" do + Weather.upsert_iemre_observation(@iemre_attrs) + + # Observation near pos2 + Weather.upsert_iemre_observation(%{ + lat: 30.25, + lon: -97.75, + date: ~D[2026-03-28], + hourly: [%{"hour" => 0, "p01m_mm" => 1.0}] + }) + + qso = %{ + pos1: %{"lat" => 32.9, "lon" => -97.0}, + pos2: %{"lat" => 30.3, "lon" => -97.7}, + qso_timestamp: ~U[2026-03-28 18:00:00Z] + } + + observations = Weather.iemre_for_path(qso) + assert length(observations) >= 2 + end + + test "returns empty list when no observations exist" do + qso = %{ + pos1: %{"lat" => 40.0, "lon" => -80.0}, + pos2: %{"lat" => 41.0, "lon" => -81.0}, + qso_timestamp: ~U[2026-03-28 18:00:00Z] + } + + assert Weather.iemre_for_path(qso) == [] + end + + test "returns observation for pos1 only when pos2 is nil" do + Weather.upsert_iemre_observation(@iemre_attrs) + + qso = %{ + pos1: %{"lat" => 32.9, "lon" => -97.0}, + pos2: nil, + qso_timestamp: ~U[2026-03-28 18:00:00Z] + } + + observations = Weather.iemre_for_path(qso) + assert length(observations) == 1 + end + end + + describe "find_nearest_hrrr/3" do + test "returns nearest HRRR profile for given lat, lon, and timestamp" do + Weather.upsert_hrrr_profile(@hrrr_profile_attrs) + + profile = Weather.find_nearest_hrrr(32.91, -97.05, ~U[2026-03-28 18:30:00Z]) + assert profile + assert profile.lat == 32.90 + end + + test "returns nil when no HRRR profiles exist nearby" do + assert Weather.find_nearest_hrrr(40.0, -80.0, ~U[2026-03-28 18:00:00Z]) == nil + end + end + describe "iemre_for_qso/1" do test "returns IEMRE observation matching QSO pos1 and date" do Weather.upsert_iemre_observation(@iemre_attrs) diff --git a/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs b/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs index 379fa8fd..1a46932f 100644 --- a/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs +++ b/test/microwaveprop/workers/qso_weather_enqueue_worker_test.exs @@ -113,8 +113,19 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert length(asos_jobs) == 1 end + test "builds ASOS jobs for stations near pos2 and midpoint" do + # Station near pos2 (30.3, -97.7) but NOT near pos1 (32.9, -97.0) + station_near_pos2 = create_asos_station("KAUS", 30.20, -97.68) + qso = create_qso() + + jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso]) + + station_ids = Enum.map(jobs, & &1.changes.args["station_id"]) + assert station_near_pos2.id in station_ids + end + test "returns empty list when no stations nearby" do - qso = create_qso(%{pos1: %{"lat" => 60.0, "lon" => -150.0}}) + qso = create_qso(%{pos1: %{"lat" => 60.0, "lon" => -150.0}, pos2: nil}) assert QsoWeatherEnqueueWorker.build_weather_jobs([qso]) == [] end @@ -244,11 +255,22 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do end describe "build_hrrr_jobs/1" do - test "builds one HRRR job per QSO with rounded lat/lon" do + test "builds HRRR jobs for all path points (pos1, midpoint, pos2)" do qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}}) jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) + # pos1, midpoint, pos2 = 3 distinct grid points + assert length(jobs) == 3 + lats = jobs |> Enum.map(& &1.changes.args["lat"]) |> Enum.sort() + assert 32.91 in lats + end + + test "builds only one HRRR job when pos2 is nil" do + qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil}) + + jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) + assert length(jobs) == 1 job = hd(jobs) assert job.changes.args["lat"] == 32.91 @@ -256,7 +278,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do end test "rounds valid_time to nearest hour" do - qso = create_qso(%{qso_timestamp: ~U[2026-03-28 18:45:00Z]}) + qso = create_qso(%{qso_timestamp: ~U[2026-03-28 18:45:00Z], pos2: nil}) jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) @@ -264,19 +286,21 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert job.changes.args["valid_time"] == "2026-03-28T19:00:00Z" end - test "deduplicates jobs with identical args" do + test "deduplicates jobs across QSOs with identical path points" do q1 = create_qso(%{ station1: "A1", qso_timestamp: ~U[2026-03-28 18:10:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) q2 = create_qso(%{ station1: "A2", qso_timestamp: ~U[2026-03-28 18:20:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([q1, q2]) @@ -285,16 +309,30 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert length(jobs) == 1 end + test "deduplicates path points that round to the same HRRR grid cell" do + # pos1 and pos2 only 0.001° apart — all 3 points round to same HRRR cell + qso = + create_qso(%{ + pos1: %{"lat" => 32.901, "lon" => -97.041}, + pos2: %{"lat" => 32.902, "lon" => -97.042} + }) + + jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) + + # All 3 path points round to {32.90, -97.04} → 1 job + assert length(jobs) == 1 + end + test "skips QSOs without pos1" do qso = create_qso(%{pos1: nil}) assert QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) == [] end - test "skips HRRR job when profile already exists for that location and time" do + test "skips only path points where HRRR profile already exists" do qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}}) - # Insert a profile at the rounded grid point and nearest hour + # Insert a profile at the pos1 rounded grid point only Weather.upsert_hrrr_profile(%{ valid_time: ~U[2026-03-28 18:00:00Z], lat: 32.91, @@ -308,7 +346,12 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do surface_pressure_mb: 1013.0 }) - assert QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) == [] + jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) + + # pos1 skipped (exists), midpoint + pos2 still need fetching + assert length(jobs) == 2 + lats = Enum.map(jobs, & &1.changes.args["lat"]) + refute 32.91 in lats end end @@ -380,11 +423,22 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do end describe "build_iemre_jobs/1" do - test "builds one IEMRE job per QSO with rounded lat/lon" do + test "builds IEMRE jobs for all path points (pos1, midpoint, pos2)" do qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}}) jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) + # pos1, midpoint, pos2 may have distinct IEMRE grid cells + assert length(jobs) >= 2 + lats = Enum.map(jobs, & &1.changes.args["lat"]) + assert 32.875 in lats + end + + test "builds only one IEMRE job when pos2 is nil" do + qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil}) + + jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) + assert length(jobs) == 1 job = hd(jobs) assert job.changes.args["lat"] == 32.875 @@ -392,19 +446,21 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert job.changes.args["date"] == "2026-03-28" end - test "deduplicates jobs with identical args" do + test "deduplicates jobs across QSOs with identical path points" do q1 = create_qso(%{ station1: "A1", qso_timestamp: ~U[2026-03-28 18:10:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) q2 = create_qso(%{ station1: "A2", qso_timestamp: ~U[2026-03-28 20:20:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2]) @@ -418,14 +474,16 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do create_qso(%{ station1: "A1", qso_timestamp: ~U[2026-03-28 23:00:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) q2 = create_qso(%{ station1: "A2", qso_timestamp: ~U[2026-03-29 01:00:00Z], - pos1: %{"lat" => 32.90, "lon" => -97.04} + pos1: %{"lat" => 32.90, "lon" => -97.04}, + pos2: nil }) jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2]) @@ -437,10 +495,10 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do assert QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) == [] end - test "skips IEMRE job when observation already exists for that location and date" do + test "skips only path points where IEMRE observation already exists" do qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}}) - # Insert an IEMRE observation at the rounded grid point and date + # Insert an IEMRE observation at the pos1 rounded grid point only Weather.upsert_iemre_observation(%{ lat: 32.875, lon: -97.0, @@ -448,7 +506,12 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do hourly: [%{"hour" => 0, "p01m_mm" => 0.0}] }) - assert QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) == [] + jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) + + # pos1 skipped, midpoint + pos2 still need fetching + assert length(jobs) >= 1 + lats = Enum.map(jobs, & &1.changes.args["lat"]) + refute 32.875 in lats end end diff --git a/test/mix/tasks/reset_enrichment_test.exs b/test/mix/tasks/reset_enrichment_test.exs new file mode 100644 index 00000000..cb6f8d7c --- /dev/null +++ b/test/mix/tasks/reset_enrichment_test.exs @@ -0,0 +1,65 @@ +defmodule Mix.Tasks.ResetEnrichmentTest do + use Microwaveprop.DataCase, async: true + + alias Microwaveprop.Radio.Qso + alias Mix.Tasks.ResetEnrichment + + defp create_queued_qso do + import Ecto.Query + + default = %{ + station1: "W5XD", + station2: "K5TR", + qso_timestamp: ~U[2026-03-28 18:00:00Z], + mode: "CW", + band: Decimal.new("1296"), + grid1: "EM12", + grid2: "EM00", + pos1: %{"lat" => 32.9, "lon" => -97.0}, + pos2: %{"lat" => 30.3, "lon" => -97.7}, + distance_km: Decimal.new("295") + } + + {:ok, qso} = + %Qso{} + |> Qso.changeset(default) + |> Repo.insert() + + # Set all queued flags to true (not in changeset cast) + Qso + |> where([q], q.id == ^qso.id) + |> Repo.update_all(set: [weather_queued: true, hrrr_queued: true, iemre_queued: true, terrain_queued: true]) + + Repo.get!(Qso, qso.id) + end + + describe "run/1" do + test "resets weather_queued, hrrr_queued, and iemre_queued to false" do + qso = create_queued_qso() + + assert qso.weather_queued == true + assert qso.hrrr_queued == true + assert qso.iemre_queued == true + + ResetEnrichment.run([]) + + updated = Repo.get!(Qso, qso.id) + assert updated.weather_queued == false + assert updated.hrrr_queued == false + assert updated.iemre_queued == false + end + + test "does not reset terrain_queued" do + qso = create_queued_qso() + + ResetEnrichment.run([]) + + updated = Repo.get!(Qso, qso.id) + assert updated.terrain_queued == true + end + + test "handles empty database" do + assert :ok = ResetEnrichment.run([]) + end + end +end