Computes subscriber impact when device alerts fire. Three analysis layers: device-level (inventory item -> direct subscriber) and site-level (network site -> all subscribers at tower). Impact data stored as JSONB on alert records and displayed as badges on alert list.
128 lines
3.8 KiB
Elixir
128 lines
3.8 KiB
Elixir
defmodule Towerops.Gaiia.ImpactAnalysis do
|
|
@moduledoc """
|
|
Calculates subscriber impact when a Towerops device goes down.
|
|
|
|
Three layers of impact analysis (broadest to most precise):
|
|
1. Site-level: Device → Site → Gaiia Network Site → all subscribers at that site
|
|
2. Device-level: Device → Gaiia Inventory Item → directly assigned subscriber
|
|
"""
|
|
|
|
alias Towerops.Devices
|
|
alias Towerops.Gaiia
|
|
|
|
@doc """
|
|
Analyze the subscriber impact of a device going down.
|
|
|
|
Returns a map with:
|
|
- `:device_level` - direct subscriber impact (from inventory item assignment)
|
|
- `:site_level` - broader site-level impact (from network site mapping)
|
|
- `:total_subscribers` - broadest subscriber count (site > device)
|
|
- `:total_mrr` - broadest MRR estimate
|
|
- `:analyzed_at` - timestamp of analysis
|
|
"""
|
|
@spec analyze_device_impact(String.t(), String.t()) :: map()
|
|
def analyze_device_impact(organization_id, device_id) do
|
|
device = Devices.get_device(device_id)
|
|
device_level = analyze_device_level(organization_id, device_id)
|
|
site_level = analyze_site_level(organization_id, device)
|
|
|
|
{total_subscribers, total_mrr} = compute_totals(device_level, site_level)
|
|
|
|
%{
|
|
device_level: device_level,
|
|
site_level: site_level,
|
|
total_subscribers: total_subscribers,
|
|
total_mrr: total_mrr,
|
|
analyzed_at: DateTime.truncate(DateTime.utc_now(), :second)
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Convert impact analysis result to a JSON-serializable map for storage.
|
|
"""
|
|
@spec to_json(map()) :: map()
|
|
def to_json(impact) do
|
|
%{
|
|
"device_level" => encode_level(impact.device_level),
|
|
"site_level" => encode_level(impact.site_level),
|
|
"total_subscribers" => impact.total_subscribers,
|
|
"total_mrr" => to_string(impact.total_mrr),
|
|
"analyzed_at" => DateTime.to_iso8601(impact.analyzed_at)
|
|
}
|
|
end
|
|
|
|
defp analyze_device_level(organization_id, device_id) do
|
|
case Gaiia.get_inventory_item_for_device(device_id) do
|
|
nil ->
|
|
nil
|
|
|
|
item ->
|
|
{subscribers, mrr} = get_item_subscriber_impact(organization_id, item)
|
|
|
|
if subscribers > 0 do
|
|
%{
|
|
subscriber_count: subscribers,
|
|
mrr: mrr,
|
|
inventory_item_name: item.name,
|
|
confidence: :device_level
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp analyze_site_level(_organization_id, nil), do: nil
|
|
|
|
defp analyze_site_level(_organization_id, %{site_id: nil}), do: nil
|
|
|
|
defp analyze_site_level(_organization_id, device) do
|
|
case Gaiia.get_network_site_for_site(device.site_id) do
|
|
nil ->
|
|
nil
|
|
|
|
network_site ->
|
|
count = network_site.account_count || 0
|
|
mrr = network_site.total_mrr || Decimal.new("0")
|
|
|
|
%{
|
|
subscriber_count: count,
|
|
mrr: mrr,
|
|
site_name: network_site.name,
|
|
confidence: :site_level
|
|
}
|
|
end
|
|
end
|
|
|
|
defp get_item_subscriber_impact(organization_id, item) do
|
|
case item.assigned_account_gaiia_id do
|
|
nil ->
|
|
{0, Decimal.new("0")}
|
|
|
|
account_gaiia_id ->
|
|
case Gaiia.get_account(organization_id, account_gaiia_id) do
|
|
nil -> {1, Decimal.new("0")}
|
|
account -> {1, account.mrr || Decimal.new("0")}
|
|
end
|
|
end
|
|
end
|
|
|
|
defp compute_totals(nil, nil), do: {0, Decimal.new("0")}
|
|
defp compute_totals(nil, site), do: {site.subscriber_count, site.mrr}
|
|
defp compute_totals(device, nil), do: {device.subscriber_count, device.mrr}
|
|
|
|
defp compute_totals(_device, site) do
|
|
# Site-level is the broadest impact estimate
|
|
{site.subscriber_count, site.mrr}
|
|
end
|
|
|
|
defp encode_level(nil), do: nil
|
|
|
|
defp encode_level(level) do
|
|
level
|
|
|> maybe_from_struct()
|
|
|> Map.update(:mrr, "0", &to_string/1)
|
|
|> Map.update(:confidence, nil, &to_string/1)
|
|
end
|
|
|
|
defp maybe_from_struct(%_{} = struct), do: Map.from_struct(struct)
|
|
defp maybe_from_struct(map) when is_map(map), do: map
|
|
end
|