towerops/lib/towerops/sonar/sync.ex

87 lines
2.9 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)
{: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
defp calculate_total_mrr(accounts) do
Enum.reduce(accounts, 0.0, fn account, total ->
services = account["account_services"] || []
service_mrr =
Enum.reduce(services, 0.0, fn service, acc ->
price = service["price_override"]
if is_number(price) do
acc + price
else
acc
end
end)
total + service_mrr
end)
end
defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API token"
defp humanize_sync_error(:forbidden), do: "Access denied — your API token may not have sufficient permissions"
defp humanize_sync_error({:rate_limited, retry_after}), do: "Rate limited by Sonar — retry after #{retry_after}s"
defp humanize_sync_error({:unexpected_status, status}), do: "Sonar returned unexpected HTTP #{status}"
defp humanize_sync_error({:graphql_errors, errors}) when is_list(errors) do
messages = Enum.map_join(errors, "; ", &(&1["message"] || inspect(&1)))
"GraphQL errors: #{messages}"
end
defp humanize_sync_error(reason) when is_binary(reason), do: reason
defp humanize_sync_error(reason) do
Logger.warning("Sonar sync failed with unexpected error: #{inspect(reason)}")
"An unexpected error occurred during sync: #{inspect(reason)}"
end
end