towerops/lib/towerops/uisp/gps_sync.ex
Graham McIntire 3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00

92 lines
2.7 KiB
Elixir

defmodule Towerops.Uisp.GpsSync do
@moduledoc """
Enriches local devices with GPS coordinates from UISP.
When UISP provides device GPS data, updates the local device
and its associated site with coordinates for map enrichment.
"""
alias Towerops.Devices.Device
alias Towerops.Repo
alias Towerops.Sites.Site
require Logger
@doc """
Updates GPS coordinates for devices and sites from UISP device data.
Expects a list of `{uisp_device_data, local_device}` tuples.
Returns `{:ok, %{devices_updated: count, sites_updated: count}}`.
"""
def sync_gps(device_pairs) do
result =
Enum.reduce(device_pairs, %{devices_updated: 0, sites_updated: 0}, fn {uisp_data, local_device}, acc ->
lat = get_in(uisp_data, ["location", "latitude"]) || get_in(uisp_data, ["gps", "latitude"])
lon = get_in(uisp_data, ["location", "longitude"]) || get_in(uisp_data, ["gps", "longitude"])
if valid_coords?(lat, lon) do
acc
|> maybe_update_device_gps(local_device, lat, lon)
|> maybe_update_site_gps(local_device, lat, lon)
else
acc
end
end)
{:ok, result}
end
defp maybe_update_device_gps(acc, device, lat, lon) do
# Update device metadata with GPS if not already set
metadata = device.metadata || %{}
if is_nil(metadata["latitude"]) or is_nil(metadata["longitude"]) do
new_metadata =
Map.merge(metadata, %{"latitude" => to_float(lat), "longitude" => to_float(lon), "gps_source" => "uisp"})
case device
|> Device.changeset(%{metadata: new_metadata})
|> Repo.update() do
{:ok, _} -> Map.update!(acc, :devices_updated, &(&1 + 1))
{:error, _} -> acc
end
else
acc
end
end
defp maybe_update_site_gps(acc, device, lat, lon) do
with site_id when not is_nil(site_id) <- device.site_id,
%Site{} = site <- Repo.get(Site, site_id),
true <- is_nil(site.latitude) or is_nil(site.longitude),
{:ok, _} <-
site
|> Site.changeset(%{latitude: to_float(lat), longitude: to_float(lon)})
|> Repo.update() do
Map.update!(acc, :sites_updated, &(&1 + 1))
else
_ -> acc
end
end
@doc false
def valid_coords?(lat, lon), do: check_coords(to_float(lat), to_float(lon))
defp check_coords(lat, lon) when is_number(lat) and is_number(lon) and lat != 0.0 and lon != 0.0, do: true
defp check_coords(_lat, _lon), do: false
@doc false
def to_float(nil), do: nil
def to_float(v) when is_float(v), do: v
def to_float(v) when is_integer(v), do: v / 1
def to_float(v) when is_binary(v) do
case Float.parse(v) do
{f, _} -> f
:error -> nil
end
end
def to_float(_), do: nil
end