towerops/lib/towerops/uisp/site_sync.ex

101 lines
2.9 KiB
Elixir

defmodule Towerops.Uisp.SiteSync do
@moduledoc """
Syncs UISP sites into the Towerops sites table.
Fetches all sites from the UISP API and upserts them by name within
the organization. GPS coordinates are mapped from the UISP response.
"""
import Ecto.Query
alias Towerops.Integrations.Integration
alias Towerops.Repo
alias Towerops.Sites.Site
alias Towerops.Uisp.Client
require Logger
@doc """
Fetches sites from UISP and upserts them into the local sites table.
Returns `{:ok, %{synced: count, site_map: map}}` where `site_map` maps
UISP site IDs to local Site records (used by device sync for site assignment).
"""
def sync_sites(%Integration{} = integration) do
base_url = integration.credentials["url"]
api_key = integration.credentials["api_key"]
org_id = integration.organization_id
case Client.list_sites(base_url, api_key) do
{:ok, sites} ->
{synced, site_map} = upsert_sites(org_id, sites)
{:ok, %{synced: synced, site_map: site_map}}
{:error, reason} ->
{:error, reason}
end
end
defp upsert_sites(org_id, sites) do
Enum.reduce(sites, {0, %{}}, fn site_data, {count, acc} ->
uisp_id = get_in(site_data, ["identification", "id"]) || site_data["id"]
name = get_in(site_data, ["identification", "name"]) || site_data["name"]
latitude = get_in(site_data, ["description", "gps", "latitude"])
longitude = get_in(site_data, ["description", "gps", "longitude"])
if is_nil(name) or name == "" do
{count, acc}
else
attrs = %{
organization_id: org_id,
name: name,
latitude: to_float(latitude),
longitude: to_float(longitude)
}
case upsert_site(attrs) do
{:ok, site} ->
new_acc = if uisp_id, do: Map.put(acc, uisp_id, site), else: acc
{count + 1, new_acc}
{:error, changeset} ->
Logger.warning("UISP site sync: failed to upsert site #{name}: #{inspect(changeset.errors)}")
{count, acc}
end
end
end)
end
defp upsert_site(attrs) do
%Site{}
|> Site.changeset(attrs)
|> Repo.insert(
on_conflict: {:replace, [:latitude, :longitude, :updated_at]},
conflict_target: [:organization_id, :name],
returning: true
)
end
@doc """
Builds a site_map from existing local sites, keyed by name.
Used after sync to allow device_sync to match UISP site names to local sites.
"""
def build_site_map_by_name(org_id) do
Site
|> where(organization_id: ^org_id)
|> Repo.all()
|> Map.new(fn site -> {site.name, site} end)
end
defp to_float(nil), do: nil
defp to_float(v) when is_float(v), do: v
defp to_float(v) when is_integer(v), do: v / 1
defp to_float(v) when is_binary(v) do
case Float.parse(v) do
{f, _} -> f
:error -> nil
end
end
end