Link to existing contact on duplicate submission

Return the conflicting contact from create_contact so the UI can redirect
to it with a flash message instead of just showing a form error.
This commit is contained in:
Graham McIntire 2026-04-11 17:30:37 -05:00
parent 231f1d9a14
commit 25c703401b
3 changed files with 27 additions and 20 deletions

View file

@ -338,11 +338,8 @@ defmodule Microwaveprop.Radio do
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
grid2 = Ecto.Changeset.get_change(changeset, :grid2)
if duplicate_contact_exists?(changeset) do
changeset =
Ecto.Changeset.add_error(changeset, :station1, "a matching contact already exists within the last hour")
{:error, %{changeset | action: :insert}}
if existing = find_duplicate_contact(changeset) do
{:error, :duplicate, existing}
else
case {Maidenhead.to_latlon(grid1), Maidenhead.to_latlon(grid2)} do
{{:ok, {lat1, lon1}}, {:ok, {lat2, lon2}}} ->
@ -365,7 +362,7 @@ defmodule Microwaveprop.Radio do
end
end
defp duplicate_contact_exists?(changeset) do
defp find_duplicate_contact(changeset) do
s1 = Ecto.Changeset.get_field(changeset, :station1) |> to_string() |> String.upcase()
s2 = Ecto.Changeset.get_field(changeset, :station2) |> to_string() |> String.upcase()
g1 = Ecto.Changeset.get_field(changeset, :grid1) |> to_string() |> String.upcase()
@ -377,8 +374,8 @@ defmodule Microwaveprop.Radio do
min_ts = DateTime.add(ts, -@dedup_window_seconds, :second)
max_ts = DateTime.add(ts, @dedup_window_seconds, :second)
# Query for contacts with same band in the time window, then check
# station/grid pair match (order-independent) in Elixir
expected_pair = Enum.sort([{s1, g1}, {s2, g2}])
from(c in Contact,
where:
c.band == ^band_decimal and
@ -387,11 +384,14 @@ defmodule Microwaveprop.Radio do
c.flagged_invalid == false
)
|> Repo.all()
|> Enum.any?(fn c ->
a = {String.upcase(c.station1), String.upcase(c.grid1)}
b = {String.upcase(c.station2), String.upcase(c.grid2)}
pair = Enum.sort([a, b])
pair == Enum.sort([{s1, g1}, {s2, g2}])
|> Enum.find(fn c ->
pair =
Enum.sort([
{String.upcase(c.station1), String.upcase(c.grid1)},
{String.upcase(c.station2), String.upcase(c.grid2)}
])
pair == expected_pair
end)
end

View file

@ -80,6 +80,15 @@ defmodule MicrowavepropWeb.SubmitLive do
|> put_flash(:info, "Contact submitted successfully!")
|> push_navigate(to: ~p"/contacts/#{contact.id}")}
{:error, :duplicate, existing} ->
{:noreply,
socket
|> put_flash(
:error,
"A matching contact already exists: #{existing.station1}#{existing.station2} on #{existing.band} MHz"
)
|> push_navigate(to: ~p"/contacts/#{existing.id}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end

View file

@ -96,11 +96,10 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do
describe "create_contact/1 deduplication" do
alias Microwaveprop.Radio
test "rejects duplicate contact within 1-hour window" do
assert {:ok, _contact} = Radio.create_contact(@valid_attrs)
test "rejects duplicate and returns the existing contact" do
assert {:ok, original} = Radio.create_contact(@valid_attrs)
assert {:error, changeset} = Radio.create_contact(@valid_attrs)
assert "a matching contact already exists within the last hour" in errors_on(changeset).station1
assert {:error, :duplicate, ^original} = Radio.create_contact(@valid_attrs)
end
test "allows same stations on a different band" do
@ -121,11 +120,10 @@ defmodule Microwaveprop.Radio.ContactSubmissionTest do
end
test "detects duplicates regardless of station order" do
assert {:ok, _} = Radio.create_contact(@valid_attrs)
assert {:ok, original} = Radio.create_contact(@valid_attrs)
swapped = %{@valid_attrs | station1: "K5TR", grid1: "EM00", station2: "W5XD", grid2: "EM12"}
assert {:error, changeset} = Radio.create_contact(swapped)
assert "a matching contact already exists within the last hour" in errors_on(changeset).station1
assert {:error, :duplicate, ^original} = Radio.create_contact(swapped)
end
end
end