- Added last_sync_message field to integrations schema - Gaiia sync: shows item counts on success, friendly error on failure - Preseem sync: shows AP/device counts on success, friendly error on failure - Error messages translated from technical errors to user-friendly text - Message displayed on integration card next to status badge - Exposed in REST API response
192 lines
6.1 KiB
Elixir
192 lines
6.1 KiB
Elixir
defmodule Towerops.Gaiia.Sync do
|
|
@moduledoc """
|
|
Orchestrates syncing data from the Gaiia GraphQL API into the local database.
|
|
|
|
Pulls accounts, network sites, inventory items, and billing subscriptions
|
|
from Gaiia, upserts them into local cache tables, and updates integration
|
|
sync status.
|
|
"""
|
|
|
|
alias Towerops.Gaiia
|
|
alias Towerops.Gaiia.Client
|
|
alias Towerops.Integrations
|
|
|
|
require Logger
|
|
|
|
@doc """
|
|
Main entry point: syncs all entity types for the given integration.
|
|
|
|
Returns `{:ok, %{accounts: n, network_sites: n, inventory_items: n, subscriptions: n}}`
|
|
or `{:error, reason}`.
|
|
"""
|
|
def sync_organization(%Integrations.Integration{} = integration) do
|
|
api_key = integration.credentials["api_key"]
|
|
org_id = integration.organization_id
|
|
opts = client_opts(integration)
|
|
|
|
with {:ok, accounts} <- Client.list_accounts(api_key, opts),
|
|
accounts_count = upsert_nodes(org_id, accounts, :account),
|
|
{:ok, sites} <- Client.list_network_sites(api_key, opts),
|
|
sites_count = upsert_nodes(org_id, sites, :network_site),
|
|
{:ok, items} <- Client.list_inventory_items(api_key, opts),
|
|
items_count = upsert_nodes(org_id, items, :inventory_item),
|
|
{:ok, subs} <- Client.list_billing_subscriptions(api_key, opts) do
|
|
subs_count = upsert_nodes(org_id, subs, :billing_subscription)
|
|
|
|
message =
|
|
"Synced #{accounts_count} accounts, #{sites_count} sites, #{items_count} inventory items, #{subs_count} subscriptions"
|
|
|
|
Integrations.update_sync_status(integration, "success", message)
|
|
|
|
{:ok,
|
|
%{
|
|
accounts: accounts_count,
|
|
network_sites: sites_count,
|
|
inventory_items: items_count,
|
|
subscriptions: subs_count
|
|
}}
|
|
else
|
|
{:error, reason} ->
|
|
Integrations.update_sync_status(integration, "failed", humanize_sync_error(reason))
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp upsert_nodes(org_id, nodes, type) do
|
|
Enum.reduce(nodes, 0, fn node, count ->
|
|
case upsert_node(org_id, node, type) do
|
|
{:ok, _} ->
|
|
count + 1
|
|
|
|
{:error, changeset} ->
|
|
Logger.warning("Failed to upsert Gaiia #{type}: #{inspect(changeset.errors)}")
|
|
count
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp upsert_node(org_id, node, :account) do
|
|
Gaiia.upsert_account(org_id, map_account(node))
|
|
end
|
|
|
|
defp upsert_node(org_id, node, :network_site) do
|
|
Gaiia.upsert_network_site(org_id, map_network_site(node))
|
|
end
|
|
|
|
defp upsert_node(org_id, node, :inventory_item) do
|
|
Gaiia.upsert_inventory_item(org_id, map_inventory_item(node))
|
|
end
|
|
|
|
defp upsert_node(org_id, node, :billing_subscription) do
|
|
Gaiia.upsert_billing_subscription(org_id, map_billing_subscription(node))
|
|
end
|
|
|
|
defp map_account(node) do
|
|
subs = get_in(node, ["billingSubscriptions", "edges"]) || []
|
|
active_count = Enum.count(subs, &(get_in(&1, ["node", "status", "name"]) == "Active"))
|
|
|
|
%{
|
|
gaiia_id: node["id"],
|
|
readable_id: node["readableId"],
|
|
name: node["name"],
|
|
status: get_in(node, ["status", "name"]),
|
|
account_type: get_in(node, ["type", "name"]),
|
|
address: map_address(node["physicalAddress"]),
|
|
subscription_count: active_count,
|
|
raw_data: node
|
|
}
|
|
end
|
|
|
|
defp map_network_site(node) do
|
|
ip_block_edges = get_in(node, ["ipBlocks", "edges"]) || []
|
|
ip_blocks = Enum.map(ip_block_edges, & &1["node"]["subnet"])
|
|
|
|
%{
|
|
gaiia_id: node["id"],
|
|
name: node["name"],
|
|
address: map_address(node["address"]),
|
|
ip_blocks: ip_blocks,
|
|
raw_data: node
|
|
}
|
|
end
|
|
|
|
defp map_inventory_item(node) do
|
|
assignation = node["assignation"]
|
|
|
|
{account_id, site_id} =
|
|
case assignation do
|
|
%{"assigneeType" => "Account", "assignee" => %{"id" => id}} -> {id, nil}
|
|
%{"assigneeType" => "NetworkSite", "assignee" => %{"id" => id}} -> {nil, id}
|
|
_ -> {nil, nil}
|
|
end
|
|
|
|
%{
|
|
gaiia_id: node["id"],
|
|
name: get_in(node, ["model", "name"]),
|
|
status: get_in(node, ["status", "name"]),
|
|
serial_number: nil,
|
|
mac_address: nil,
|
|
ip_address: node["ipAddressV4"],
|
|
model_name: get_in(node, ["model", "name"]),
|
|
manufacturer_name: get_in(node, ["model", "manufacturer", "name"]),
|
|
category: get_in(node, ["model", "category", "name"]),
|
|
assigned_account_gaiia_id: account_id,
|
|
assigned_network_site_gaiia_id: site_id,
|
|
raw_data: node
|
|
}
|
|
end
|
|
|
|
defp map_billing_subscription(node) do
|
|
price_cents = get_in(node, ["productVersion", "price"])
|
|
|
|
mrr_amount =
|
|
if is_integer(price_cents),
|
|
do: Decimal.div(Decimal.new(price_cents), 100)
|
|
|
|
%{
|
|
gaiia_id: node["id"],
|
|
account_gaiia_id: get_in(node, ["entity", "id"]),
|
|
status: get_in(node, ["status", "name"]),
|
|
product_name: get_in(node, ["productVersion", "product", "name"]),
|
|
mrr_amount: mrr_amount,
|
|
currency: get_in(node, ["productVersion", "currency"]),
|
|
raw_data: node
|
|
}
|
|
end
|
|
|
|
defp map_address(nil), do: nil
|
|
|
|
defp map_address(addr) do
|
|
%{
|
|
"line1" => addr["line1"],
|
|
"city" => addr["locality"],
|
|
"state" => addr["region"],
|
|
"zip" => addr["postalCode"],
|
|
"country" => addr["country"],
|
|
"latitude" => addr["latitude"],
|
|
"longitude" => addr["longitude"]
|
|
}
|
|
end
|
|
|
|
defp client_opts(integration) do
|
|
case integration.credentials["endpoint"] do
|
|
nil -> []
|
|
endpoint -> [endpoint: endpoint]
|
|
end
|
|
end
|
|
|
|
defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key"
|
|
|
|
defp humanize_sync_error(:forbidden), do: "Access denied — your API key may not have sufficient permissions"
|
|
|
|
defp humanize_sync_error(:not_found), do: "API endpoint not found — the Gaiia API may have changed"
|
|
|
|
defp humanize_sync_error(:timeout), do: "Connection timed out — Gaiia may be temporarily unavailable"
|
|
|
|
defp humanize_sync_error(%Req.TransportError{reason: :econnrefused}), do: "Connection refused — unable to reach Gaiia"
|
|
|
|
defp humanize_sync_error(%Req.TransportError{reason: reason}), do: "Connection error: #{reason}"
|
|
|
|
defp humanize_sync_error(reason) when is_binary(reason), do: reason
|
|
defp humanize_sync_error(_reason), do: "An unexpected error occurred during sync"
|
|
end
|