prop/lib/microwaveprop/qrz.ex
Graham McIntire 581955bd69
Some checks failed
Build and Push / Build and Push Docker Image (push) Has been cancelled
fix: prevent contacts_dedup_idx collisions in parallel async tests
Create shared ContactsFixtures module with globally-unique qso_timestamp
seconds to prevent unique-constraint violations when async test modules
run in parallel and insert contacts with identical dedup-key columns.
The qso_timestamp column is timestamp(0), so microsecond offsets were
truncated — use System.unique_integer monotonic seconds instead.

Also make count-asserting tests resilient to sandbox-leaked contacts
from prior tests by using >= assertions or status-based checks rather
than exact counts.

Includes automated DateTime.add → DateTime.shift migration from
mix format.
2026-08-04 17:05:16 -05:00

72 lines
2 KiB
Elixir

defmodule Microwaveprop.Qrz do
@moduledoc """
Callsign-lookup facade around the QRZ.com XML API.
Results are cached in the `qrz_callsigns` table keyed by uppercased
callsign with a configurable TTL (`cache_ttl_hours`, default 168 / 7
days). Entries that are still fresh return from the DB without
touching the upstream.
"""
import Ecto.Query
alias Microwaveprop.Qrz.Callsign
alias Microwaveprop.Qrz.Client
alias Microwaveprop.Qrz.Record
alias Microwaveprop.Repo
@spec lookup_callsign(String.t()) :: {:ok, Record.t()} | {:error, String.t()}
def lookup_callsign(callsign) do
callsign = String.upcase(callsign)
case get_fresh_cache(callsign) do
%Callsign{data: data} ->
{:ok, Record.from_map(data)}
nil ->
fetch_and_upsert(callsign)
end
end
@spec get_callsign(String.t()) :: Callsign.t() | nil
def get_callsign(callsign) do
callsign = String.upcase(callsign)
Repo.get_by(Callsign, callsign: callsign)
end
defp get_fresh_cache(callsign) do
ttl_hours = config()[:cache_ttl_hours] || 168
cutoff = DateTime.shift(DateTime.utc_now(), hour: -ttl_hours)
Callsign
|> where([c], c.callsign == ^callsign and c.updated_at > ^cutoff)
|> Repo.one()
end
defp fetch_and_upsert(callsign) do
case Client.lookup(callsign) do
{:ok, data} ->
now = DateTime.utc_now(:second)
result =
%Callsign{}
|> Callsign.changeset(%{callsign: callsign, data: data})
|> Repo.insert(
on_conflict: [set: [data: data, updated_at: now]],
conflict_target: :callsign,
returning: true
)
case result do
{:ok, %Callsign{data: data}} -> {:ok, Record.from_map(data)}
{:error, changeset} -> {:error, "Failed to save: #{inspect(changeset.errors)}"}
end
{:error, _} = error ->
error
end
end
defp config do
Application.get_env(:microwaveprop, __MODULE__, [])
end
end