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.
99 lines
3.4 KiB
Elixir
99 lines
3.4 KiB
Elixir
defmodule Towerops.Sonar.Sync do
|
|
@moduledoc """
|
|
Orchestrates syncing data from the Sonar GraphQL API into the local database.
|
|
|
|
Pulls accounts, network sites, and inventory items from Sonar
|
|
and updates integration sync status.
|
|
"""
|
|
|
|
alias Towerops.Integrations
|
|
alias Towerops.Sonar.Client
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Main entry point: syncs all entity types for the given integration.
|
|
|
|
Returns `{:ok, %{accounts: n, network_sites: n, inventory_items: n}}`
|
|
or `{:error, reason}`.
|
|
"""
|
|
def sync_organization(%Integrations.Integration{} = integration) do
|
|
instance_url = integration.credentials["instance_url"]
|
|
api_token = integration.credentials["api_token"]
|
|
|
|
with {:ok, accounts} <- Client.list_accounts(instance_url, api_token),
|
|
{:ok, sites} <- Client.list_network_sites(instance_url, api_token),
|
|
{:ok, items} <- Client.list_inventory_items(instance_url, api_token) do
|
|
accounts_count = length(accounts)
|
|
sites_count = length(sites)
|
|
items_count = length(items)
|
|
|
|
mrr = calculate_total_mrr(accounts)
|
|
|
|
message =
|
|
"Synced #{accounts_count} accounts, #{sites_count} sites, #{items_count} inventory items" <>
|
|
if(mrr > 0, do: " (MRR: $#{:erlang.float_to_binary(mrr / 1, decimals: 2)})", else: "")
|
|
|
|
Integrations.update_sync_status(integration, "success", message)
|
|
Integrations.update_billing_totals(integration, accounts_count, Decimal.from_float(mrr))
|
|
|
|
{:ok,
|
|
%{
|
|
accounts: accounts_count,
|
|
network_sites: sites_count,
|
|
inventory_items: items_count
|
|
}}
|
|
else
|
|
{:error, reason} ->
|
|
Integrations.update_sync_status(integration, "failed", humanize_sync_error(reason))
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def calculate_total_mrr(accounts) do
|
|
Enum.reduce(accounts, 0.0, fn account, total ->
|
|
services = account["account_services"] || []
|
|
total + sum_service_prices(services)
|
|
end)
|
|
end
|
|
|
|
@doc false
|
|
def sum_service_prices(services) do
|
|
Enum.reduce(services, 0.0, fn service, acc -> add_price(acc, service["price_override"]) end)
|
|
end
|
|
|
|
@doc false
|
|
def add_price(acc, price) when is_number(price), do: acc + price
|
|
def add_price(acc, _price), do: acc
|
|
|
|
@doc false
|
|
def humanize_sync_error(:unauthorized), do: "Authentication failed — check your API token"
|
|
|
|
def humanize_sync_error(:forbidden), do: "Access denied — your API token may not have sufficient permissions"
|
|
|
|
def humanize_sync_error({:rate_limited, retry_after}), do: "Rate limited by Sonar — retry after #{retry_after}s"
|
|
|
|
def humanize_sync_error({:unexpected_status, status}), do: "Sonar returned unexpected HTTP #{status}"
|
|
|
|
def humanize_sync_error({:graphql_errors, errors}) when is_list(errors) do
|
|
messages = Enum.map_join(errors, "; ", &(&1["message"] || inspect(&1)))
|
|
"GraphQL errors: #{messages}"
|
|
end
|
|
|
|
def humanize_sync_error(reason) when is_binary(reason) do
|
|
if ssl_failure?(reason) do
|
|
"SSL certificate verification failed — unable to establish a secure connection"
|
|
else
|
|
Logger.warning("Sync failed with raw error: #{reason}")
|
|
"An unexpected error occurred during sync"
|
|
end
|
|
end
|
|
|
|
def humanize_sync_error(reason) do
|
|
Logger.warning("Sonar sync failed with unexpected error: #{inspect(reason)}")
|
|
"An unexpected error occurred during sync"
|
|
end
|
|
|
|
defp ssl_failure?(reason), do: String.contains?(reason, ["CA trust store", "cacert", "certificate"])
|
|
end
|