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.add(DateTime.utc_now(), -ttl_hours, :hour) 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