Merge pull request 'gaiia' (#1) from gaiia into main
Reviewed-on: graham/towerops-web#1
This commit is contained in:
commit
6d678b6d17
17 changed files with 765 additions and 61 deletions
|
|
@ -441,6 +441,9 @@ defmodule SnmpKit.SnmpLib.Config do
|
|||
|
||||
## Private Implementation
|
||||
|
||||
# Capture Mix.env() at compile time since Mix is not available in releases
|
||||
@compile_env if Code.ensure_loaded?(Mix), do: Mix.env(), else: :prod
|
||||
|
||||
# Environment detection with multiple fallbacks
|
||||
defp detect_environment(opts) do
|
||||
cond do
|
||||
|
|
@ -448,7 +451,7 @@ defmodule SnmpKit.SnmpLib.Config do
|
|||
env = System.get_env("MIX_ENV") -> String.to_atom(env)
|
||||
env = System.get_env("SNMP_LIB_ENV") -> String.to_atom(env)
|
||||
env = Application.get_env(:snmp_lib, :environment) -> env
|
||||
true -> :dev
|
||||
true -> @compile_env
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Towerops.Dashboard do
|
|||
alias Towerops.Devices
|
||||
alias Towerops.Gaiia
|
||||
alias Towerops.Gaiia.ImpactAnalysis
|
||||
alias Towerops.Integrations
|
||||
alias Towerops.Preseem
|
||||
alias Towerops.Preseem.Insights
|
||||
alias Towerops.Sites
|
||||
|
|
@ -16,7 +17,7 @@ defmodule Towerops.Dashboard do
|
|||
status_counts = Devices.get_device_status_counts(organization_id)
|
||||
total_devices = status_counts |> Map.values() |> Enum.sum()
|
||||
active_alerts = Alerts.list_organization_active_alerts(organization_id)
|
||||
subscriber_totals = Gaiia.get_org_subscriber_totals(organization_id)
|
||||
subscriber_totals = Integrations.get_billing_summary(organization_id)
|
||||
insight_counts = Insights.count_active_by_urgency(organization_id)
|
||||
|
||||
%{
|
||||
|
|
@ -83,9 +84,9 @@ defmodule Towerops.Dashboard do
|
|||
end
|
||||
end
|
||||
|
||||
@doc "Returns subscriber summary for the org from Gaiia data."
|
||||
@doc "Returns subscriber summary for the org from billing integrations."
|
||||
def get_org_subscriber_summary(organization_id) do
|
||||
totals = Gaiia.get_org_subscriber_totals(organization_id)
|
||||
totals = Integrations.get_billing_summary(organization_id)
|
||||
|
||||
%{
|
||||
total: totals.total_subscribers,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,20 @@ defmodule Towerops.Gaiia do
|
|||
)
|
||||
end
|
||||
|
||||
def count_active_subscribers(organization_id) do
|
||||
Account
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([a], a.subscription_count > 0)
|
||||
|> Repo.aggregate(:count)
|
||||
end
|
||||
|
||||
def sum_active_mrr(organization_id) do
|
||||
BillingSubscription
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where(status: "ACTIVE")
|
||||
|> Repo.aggregate(:sum, :mrr_amount) || Decimal.new(0)
|
||||
end
|
||||
|
||||
# --- Network Sites ---
|
||||
|
||||
def list_network_sites(organization_id) do
|
||||
|
|
|
|||
104
lib/towerops/gaiia/site_aggregation.ex
Normal file
104
lib/towerops/gaiia/site_aggregation.ex
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
defmodule Towerops.Gaiia.SiteAggregation do
|
||||
@moduledoc """
|
||||
Computes per-site subscriber counts and MRR by matching inventory item
|
||||
IP addresses against network site CIDR blocks.
|
||||
"""
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias Towerops.Gaiia.Account
|
||||
alias Towerops.Gaiia.BillingSubscription
|
||||
alias Towerops.Gaiia.InventoryItem
|
||||
alias Towerops.Gaiia.NetworkSite
|
||||
alias Towerops.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
For each network site with IP blocks, finds inventory items whose IP addresses
|
||||
fall within those blocks, then counts unique accounts with active subscriptions
|
||||
and sums their MRR.
|
||||
|
||||
Updates each network site's `account_count` and `total_mrr` fields.
|
||||
"""
|
||||
def compute_and_store(organization_id) do
|
||||
sites = load_sites_with_ip_blocks(organization_id)
|
||||
items = load_assigned_items(organization_id)
|
||||
|
||||
Enum.each(sites, fn site ->
|
||||
cidrs = parse_cidrs(site.ip_blocks)
|
||||
matched_account_ids = match_accounts_to_cidrs(items, cidrs)
|
||||
{account_count, total_mrr} = compute_totals(organization_id, matched_account_ids)
|
||||
update_site_totals(site, account_count, total_mrr)
|
||||
end)
|
||||
end
|
||||
|
||||
defp load_sites_with_ip_blocks(organization_id) do
|
||||
NetworkSite
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([ns], fragment("cardinality(?) > 0", ns.ip_blocks))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp load_assigned_items(organization_id) do
|
||||
InventoryItem
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([ii], not is_nil(ii.assigned_account_gaiia_id))
|
||||
|> where([ii], not is_nil(ii.ip_address))
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
defp parse_cidrs(ip_blocks) do
|
||||
Enum.flat_map(ip_blocks, fn block ->
|
||||
case InetCidr.parse_cidr(block) do
|
||||
{:ok, cidr} -> [cidr]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp match_accounts_to_cidrs(items, cidrs) do
|
||||
items
|
||||
|> Enum.filter(fn item -> ip_in_any_cidr?(item.ip_address, cidrs) end)
|
||||
|> Enum.map(& &1.assigned_account_gaiia_id)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp ip_in_any_cidr?(ip_string, cidrs) do
|
||||
case :inet.parse_address(String.to_charlist(ip_string)) do
|
||||
{:ok, addr} ->
|
||||
Enum.any?(cidrs, fn cidr -> InetCidr.contains?(cidr, addr) end)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp compute_totals(_organization_id, []) do
|
||||
{0, Decimal.new("0")}
|
||||
end
|
||||
|
||||
defp compute_totals(organization_id, account_gaiia_ids) do
|
||||
account_count =
|
||||
Account
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([a], a.gaiia_id in ^account_gaiia_ids)
|
||||
|> where([a], a.subscription_count > 0)
|
||||
|> Repo.aggregate(:count)
|
||||
|
||||
total_mrr =
|
||||
BillingSubscription
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([bs], bs.account_gaiia_id in ^account_gaiia_ids)
|
||||
|> where(status: "ACTIVE")
|
||||
|> Repo.aggregate(:sum, :mrr_amount) || Decimal.new("0")
|
||||
|
||||
{account_count, total_mrr}
|
||||
end
|
||||
|
||||
defp update_site_totals(site, account_count, total_mrr) do
|
||||
site
|
||||
|> Ecto.Changeset.change(%{account_count: account_count, total_mrr: total_mrr})
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
|
@ -37,6 +37,8 @@ defmodule Towerops.Gaiia.Sync do
|
|||
"Synced #{accounts_count} accounts, #{sites_count} sites, #{items_count} inventory items, #{subs_count} subscriptions"
|
||||
|
||||
Integrations.update_sync_status(integration, "success", message)
|
||||
persist_billing_totals(integration)
|
||||
Gaiia.SiteAggregation.compute_and_store(org_id)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
|
|
@ -168,6 +170,13 @@ defmodule Towerops.Gaiia.Sync do
|
|||
}
|
||||
end
|
||||
|
||||
defp persist_billing_totals(integration) do
|
||||
org_id = integration.organization_id
|
||||
subscriber_count = Gaiia.count_active_subscribers(org_id)
|
||||
total_mrr = Gaiia.sum_active_mrr(org_id)
|
||||
Integrations.update_billing_totals(integration, subscriber_count, total_mrr)
|
||||
end
|
||||
|
||||
defp client_opts(integration) do
|
||||
case integration.credentials["endpoint"] do
|
||||
nil -> []
|
||||
|
|
|
|||
|
|
@ -62,6 +62,32 @@ defmodule Towerops.Integrations do
|
|||
end
|
||||
end
|
||||
|
||||
@billing_providers ~w(gaiia splynx visp sonar)
|
||||
|
||||
def update_billing_totals(%Integration{} = integration, subscriber_count, total_mrr) do
|
||||
integration
|
||||
|> Integration.changeset(%{subscriber_count: subscriber_count, total_mrr: total_mrr})
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def get_billing_summary(organization_id) do
|
||||
result =
|
||||
Integration
|
||||
|> where(organization_id: ^organization_id)
|
||||
|> where([i], i.provider in @billing_providers)
|
||||
|> where(enabled: true)
|
||||
|> select([i], %{
|
||||
total_subscribers: coalesce(sum(i.subscriber_count), 0),
|
||||
total_mrr: coalesce(sum(i.total_mrr), 0)
|
||||
})
|
||||
|> Repo.one()
|
||||
|
||||
%{
|
||||
total_subscribers: result.total_subscribers,
|
||||
total_mrr: result.total_mrr || Decimal.new(0)
|
||||
}
|
||||
end
|
||||
|
||||
def list_enabled_integrations(provider) do
|
||||
Integration
|
||||
|> where(provider: ^provider, enabled: true)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ defmodule Towerops.Integrations.Integration do
|
|||
field :last_synced_at, :utc_datetime
|
||||
field :last_sync_status, :string, default: "never"
|
||||
field :last_sync_message, :string
|
||||
field :subscriber_count, :integer, default: 0
|
||||
field :total_mrr, :decimal
|
||||
|
||||
belongs_to :organization, Towerops.Organizations.Organization
|
||||
|
||||
|
|
@ -41,7 +43,9 @@ defmodule Towerops.Integrations.Integration do
|
|||
:sync_interval_minutes,
|
||||
:last_synced_at,
|
||||
:last_sync_status,
|
||||
:last_sync_message
|
||||
:last_sync_message,
|
||||
:subscriber_count,
|
||||
:total_mrr
|
||||
])
|
||||
|> validate_required([:organization_id, :provider])
|
||||
|> validate_inclusion(:provider, @valid_providers)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ defmodule Towerops.Sonar.Sync do
|
|||
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,
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ defmodule Towerops.Splynx.Sync do
|
|||
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, active_count, Decimal.from_float(mrr))
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ defmodule Towerops.Visp.Sync do
|
|||
if(total_mrr > 0, do: " (MRR: $#{:erlang.float_to_binary(total_mrr / 1, decimals: 2)})", else: "")
|
||||
|
||||
Integrations.update_sync_status(integration, "success", message)
|
||||
Integrations.update_billing_totals(integration, subscribers_count, Decimal.from_float(total_mrr))
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
|
|
|
|||
|
|
@ -32,7 +32,10 @@
|
|||
|
||||
<%= if category.exclusive do %>
|
||||
<%!-- Exclusive category (billing): show as picker + single config panel --%>
|
||||
<% active_billing = Enum.find(category.providers, fn p -> @integrations[p.id] && @integrations[p.id].enabled end) %>
|
||||
<% active_billing =
|
||||
Enum.find(category.providers, fn p ->
|
||||
@integrations[p.id] && @integrations[p.id].enabled
|
||||
end) %>
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-white/5">
|
||||
<%= if active_billing do %>
|
||||
<%!-- Active billing provider --%>
|
||||
|
|
@ -40,7 +43,10 @@
|
|||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
|
||||
<.icon name={active_billing.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
|
||||
<.icon
|
||||
name={active_billing.icon}
|
||||
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
|
|
@ -71,10 +77,17 @@
|
|||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
case integration.last_sync_status do
|
||||
"success" -> "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
"partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
"failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
_ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
"success" ->
|
||||
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
|
||||
"partial" ->
|
||||
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
"failed" ->
|
||||
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
|
||||
_ ->
|
||||
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
end
|
||||
]}>
|
||||
{String.capitalize(integration.last_sync_status)}
|
||||
|
|
@ -83,19 +96,26 @@
|
|||
|
||||
<%= if integration.last_synced_at do %>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
· Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m
|
||||
· Next sync in ~{next_sync_minutes(
|
||||
integration.last_synced_at,
|
||||
integration.sync_interval_minutes
|
||||
)}m
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%= if active_billing.id == "gaiia" do %>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
|
||||
navigate={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
|
||||
}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Entity Mapping →
|
||||
</.link>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
|
||||
navigate={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
|
||||
}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Reconciliation →
|
||||
|
|
@ -147,13 +167,17 @@
|
|||
class={[
|
||||
"group relative flex flex-col items-center rounded-lg border-2 p-4 text-center transition-all hover:border-indigo-300 hover:bg-indigo-50/50 dark:hover:border-indigo-700 dark:hover:bg-indigo-900/10",
|
||||
if(@configuring == provider.id,
|
||||
do: "border-indigo-500 bg-indigo-50/50 dark:border-indigo-600 dark:bg-indigo-900/20",
|
||||
do:
|
||||
"border-indigo-500 bg-indigo-50/50 dark:border-indigo-600 dark:bg-indigo-900/20",
|
||||
else: "border-gray-200 dark:border-white/10"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-50 group-hover:bg-indigo-100 dark:bg-indigo-900/30">
|
||||
<.icon name={provider.icon} class="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
|
||||
<.icon
|
||||
name={provider.icon}
|
||||
class="h-5 w-5 text-indigo-600 dark:text-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<span class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{provider.name}
|
||||
|
|
@ -167,7 +191,7 @@
|
|||
|
||||
<%!-- Config form for whichever billing provider is being configured --%>
|
||||
<%= if @configuring && find_category_id(@configuring) == "billing" do %>
|
||||
<% provider = Enum.find(category.providers, & &1.id == @configuring) %>
|
||||
<% provider = Enum.find(category.providers, &(&1.id == @configuring)) %>
|
||||
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Configure {provider.name}
|
||||
|
|
@ -292,7 +316,9 @@
|
|||
{t("Webhook Configuration")}
|
||||
</h4>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{t("Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.")}
|
||||
{t(
|
||||
"Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change."
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 space-y-4">
|
||||
|
|
@ -347,7 +373,11 @@
|
|||
type="button"
|
||||
id="regenerate-webhook-secret"
|
||||
phx-click="regenerate_webhook_secret"
|
||||
data-confirm={t("Are you sure? Any existing Gaiia webhook using the current secret will stop working.")}
|
||||
data-confirm={
|
||||
t(
|
||||
"Are you sure? Any existing Gaiia webhook using the current secret will stop working."
|
||||
)
|
||||
}
|
||||
class="text-xs text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
{t("Regenerate Secret")}
|
||||
|
|
@ -383,7 +413,10 @@
|
|||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-50 dark:bg-indigo-900/30">
|
||||
<.icon name={provider.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
|
||||
<.icon
|
||||
name={provider.icon}
|
||||
class="h-6 w-6 text-indigo-600 dark:text-indigo-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
|
|
@ -398,7 +431,8 @@
|
|||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
|
||||
if(integration.enabled,
|
||||
do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
do:
|
||||
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
|
||||
else: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
)
|
||||
]}>
|
||||
|
|
@ -420,10 +454,17 @@
|
|||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
case integration.last_sync_status do
|
||||
"success" -> "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
"partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
"failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
_ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
"success" ->
|
||||
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
|
||||
"partial" ->
|
||||
"bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
|
||||
"failed" ->
|
||||
"bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
|
||||
_ ->
|
||||
"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
end
|
||||
]}>
|
||||
{String.capitalize(integration.last_sync_status)}
|
||||
|
|
@ -432,13 +473,18 @@
|
|||
|
||||
<%= if integration.last_synced_at do %>
|
||||
<span class="text-xs text-gray-400 dark:text-gray-500">
|
||||
· Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m
|
||||
· Next sync in ~{next_sync_minutes(
|
||||
integration.last_synced_at,
|
||||
integration.sync_interval_minutes
|
||||
)}m
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<%= if provider.id == "preseem" do %>
|
||||
<.link
|
||||
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"}
|
||||
navigate={
|
||||
~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"
|
||||
}
|
||||
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Manage Devices →
|
||||
|
|
@ -476,7 +522,10 @@
|
|||
phx-value-provider={provider.id}
|
||||
class={[
|
||||
"relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2",
|
||||
if(integration.enabled, do: "bg-indigo-600", else: "bg-gray-200 dark:bg-gray-700")
|
||||
if(integration.enabled,
|
||||
do: "bg-indigo-600",
|
||||
else: "bg-gray-200 dark:bg-gray-700"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class={[
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
defmodule Towerops.Repo.Migrations.AddBillingFieldsToIntegrations do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:integrations) do
|
||||
add :subscriber_count, :integer, default: 0
|
||||
add :total_mrr, :decimal, default: 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,7 +6,7 @@ defmodule Towerops.DashboardTest do
|
|||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Dashboard
|
||||
alias Towerops.Gaiia
|
||||
alias Towerops.Integrations
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
|
|
@ -53,21 +53,18 @@ defmodule Towerops.DashboardTest do
|
|||
assert summary.alerts.active_count == 1
|
||||
end
|
||||
|
||||
test "returns subscriber totals from Gaiia", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-dash",
|
||||
name: "Tower",
|
||||
account_count: 100,
|
||||
total_mrr: Decimal.new("5000.00")
|
||||
})
|
||||
test "returns subscriber totals from billing integrations", %{org: org} do
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00"))
|
||||
|
||||
summary = Dashboard.get_dashboard_summary(org.id)
|
||||
assert summary.subscribers.total == 100
|
||||
assert Decimal.equal?(summary.subscribers.total_mrr, Decimal.new("5000.00"))
|
||||
end
|
||||
|
||||
test "returns zero subscribers when no Gaiia data", %{org: org} do
|
||||
test "returns zero subscribers when no billing integrations", %{org: org} do
|
||||
summary = Dashboard.get_dashboard_summary(org.id)
|
||||
assert summary.subscribers.total == 0
|
||||
end
|
||||
|
|
@ -123,29 +120,23 @@ defmodule Towerops.DashboardTest do
|
|||
end
|
||||
|
||||
describe "get_org_subscriber_summary/1" do
|
||||
test "returns totals from Gaiia network sites", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-sub1",
|
||||
name: "Tower A",
|
||||
account_count: 50,
|
||||
total_mrr: Decimal.new("2500.00")
|
||||
})
|
||||
test "returns totals from billing integrations", %{org: org} do
|
||||
{:ok, i1} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-sub2",
|
||||
name: "Tower B",
|
||||
account_count: 30,
|
||||
total_mrr: Decimal.new("1500.00")
|
||||
})
|
||||
{:ok, _} = Integrations.update_billing_totals(i1, 50, Decimal.new("2500.00"))
|
||||
|
||||
{:ok, i2} =
|
||||
Integrations.create_integration(org.id, %{provider: "splynx", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i2, 30, Decimal.new("1500.00"))
|
||||
|
||||
result = Dashboard.get_org_subscriber_summary(org.id)
|
||||
assert result.total == 80
|
||||
assert Decimal.equal?(result.total_mrr, Decimal.new("4000.00"))
|
||||
end
|
||||
|
||||
test "returns zeros when no Gaiia data", %{org: org} do
|
||||
test "returns zeros when no billing integrations", %{org: org} do
|
||||
result = Dashboard.get_org_subscriber_summary(org.id)
|
||||
assert result.total == 0
|
||||
assert Decimal.equal?(result.total_mrr, Decimal.new(0))
|
||||
|
|
|
|||
391
test/towerops/gaiia/site_aggregation_test.exs
Normal file
391
test/towerops/gaiia/site_aggregation_test.exs
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
defmodule Towerops.Gaiia.SiteAggregationTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
import Towerops.OrganizationsFixtures
|
||||
|
||||
alias Towerops.Gaiia
|
||||
alias Towerops.Gaiia.SiteAggregation
|
||||
|
||||
setup do
|
||||
user = user_fixture()
|
||||
org = organization_fixture(user.id)
|
||||
%{org: org}
|
||||
end
|
||||
|
||||
describe "compute_and_store/1" do
|
||||
test "matches account to site via IP block", %{org: org} do
|
||||
{:ok, _site} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _account} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _item} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE",
|
||||
ip_address: "10.0.0.5",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _sub} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-1",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("79.99")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 1
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("79.99"))
|
||||
end
|
||||
|
||||
test "counts multiple accounts at one site", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-2",
|
||||
name: "Bob",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE-1",
|
||||
ip_address: "10.0.0.5",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-2",
|
||||
name: "CPE-2",
|
||||
ip_address: "10.0.0.6",
|
||||
assigned_account_gaiia_id: "acct-2"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-1",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("50.00")
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-2",
|
||||
account_gaiia_id: "acct-2",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("75.00")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 2
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("125.00"))
|
||||
end
|
||||
|
||||
test "excludes accounts with no active subscriptions", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-no-subs",
|
||||
name: "Charlie",
|
||||
subscription_count: 0
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE",
|
||||
ip_address: "10.0.0.10",
|
||||
assigned_account_gaiia_id: "acct-no-subs"
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 0
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("0"))
|
||||
end
|
||||
|
||||
test "ignores items assigned to sites (not accounts)", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-infra",
|
||||
name: "AP",
|
||||
ip_address: "10.0.0.1",
|
||||
assigned_network_site_gaiia_id: "ns-1",
|
||||
assigned_account_gaiia_id: nil
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 0
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("0"))
|
||||
end
|
||||
|
||||
test "matches across multiple IP blocks on one site", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24", "172.16.0.0/16"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-2",
|
||||
name: "Bob",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE-1",
|
||||
ip_address: "10.0.0.50",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-2",
|
||||
name: "CPE-2",
|
||||
ip_address: "172.16.5.10",
|
||||
assigned_account_gaiia_id: "acct-2"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-1",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("50.00")
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-2",
|
||||
account_gaiia_id: "acct-2",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("75.00")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 2
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("125.00"))
|
||||
end
|
||||
|
||||
test "skips sites with empty ip_blocks", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-empty",
|
||||
name: "No Blocks",
|
||||
ip_blocks: []
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-empty")
|
||||
# Site was skipped — values remain at DB defaults (not computed)
|
||||
assert site.account_count == 0
|
||||
assert is_nil(site.total_mrr)
|
||||
end
|
||||
|
||||
test "cross-site isolation — IP matches only the correct site", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-a",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-b",
|
||||
name: "Tower B",
|
||||
ip_blocks: ["192.168.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE",
|
||||
ip_address: "10.0.0.5",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-1",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("60.00")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site_a = Gaiia.get_network_site(org.id, "ns-a")
|
||||
assert site_a.account_count == 1
|
||||
assert Decimal.equal?(site_a.total_mrr, Decimal.new("60.00"))
|
||||
|
||||
site_b = Gaiia.get_network_site(org.id, "ns-b")
|
||||
assert site_b.account_count == 0
|
||||
assert Decimal.equal?(site_b.total_mrr, Decimal.new("0"))
|
||||
end
|
||||
|
||||
test "deduplicates accounts with multiple inventory items", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
# Two items for the same account
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE-Primary",
|
||||
ip_address: "10.0.0.5",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-2",
|
||||
name: "CPE-Secondary",
|
||||
ip_address: "10.0.0.6",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-1",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("99.99")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
# Account counted once despite two items
|
||||
assert site.account_count == 1
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("99.99"))
|
||||
end
|
||||
|
||||
test "only sums MRR from ACTIVE subscriptions", %{org: org} do
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_network_site(org.id, %{
|
||||
gaiia_id: "ns-1",
|
||||
name: "Tower A",
|
||||
ip_blocks: ["10.0.0.0/24"]
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_account(org.id, %{
|
||||
gaiia_id: "acct-1",
|
||||
name: "Alice",
|
||||
subscription_count: 1
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_inventory_item(org.id, %{
|
||||
gaiia_id: "item-1",
|
||||
name: "CPE",
|
||||
ip_address: "10.0.0.5",
|
||||
assigned_account_gaiia_id: "acct-1"
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-active",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "ACTIVE",
|
||||
mrr_amount: Decimal.new("50.00")
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Gaiia.upsert_billing_subscription(org.id, %{
|
||||
gaiia_id: "sub-cancelled",
|
||||
account_gaiia_id: "acct-1",
|
||||
status: "CANCELLED",
|
||||
mrr_amount: Decimal.new("25.00")
|
||||
})
|
||||
|
||||
SiteAggregation.compute_and_store(org.id)
|
||||
|
||||
site = Gaiia.get_network_site(org.id, "ns-1")
|
||||
assert site.account_count == 1
|
||||
# Only the active subscription's MRR
|
||||
assert Decimal.equal?(site.total_mrr, Decimal.new("50.00"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -150,6 +150,18 @@ defmodule Towerops.Gaiia.SyncTest do
|
|||
assert result.accounts == 0
|
||||
end
|
||||
|
||||
test "persists billing totals on the integration after sync", %{integration: integration} do
|
||||
stub_all_endpoints()
|
||||
|
||||
{:ok, _} = Sync.sync_organization(integration)
|
||||
|
||||
updated = Repo.reload!(integration)
|
||||
# 1 active account with subscription_count > 0
|
||||
assert updated.subscriber_count == 1
|
||||
# 1 active subscription at $79.99 (7999 cents)
|
||||
assert Decimal.equal?(updated.total_mrr, Decimal.new("79.99"))
|
||||
end
|
||||
|
||||
test "upserts existing records without duplicating", %{org: org, integration: integration} do
|
||||
stub_all_endpoints()
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,92 @@ defmodule Towerops.IntegrationsTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "update_billing_totals/3" do
|
||||
test "persists subscriber_count and total_mrr on integration", %{organization: org} do
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
assert {:ok, updated} =
|
||||
Integrations.update_billing_totals(integration, 42, Decimal.new("3500.00"))
|
||||
|
||||
assert updated.subscriber_count == 42
|
||||
assert Decimal.equal?(updated.total_mrr, Decimal.new("3500.00"))
|
||||
end
|
||||
|
||||
test "overwrites previous billing totals", %{organization: org} do
|
||||
{:ok, integration} =
|
||||
Integrations.create_integration(org.id, %{provider: "splynx", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(integration, 10, Decimal.new("500.00"))
|
||||
integration = Repo.reload!(integration)
|
||||
{:ok, updated} = Integrations.update_billing_totals(integration, 20, Decimal.new("1000.00"))
|
||||
|
||||
assert updated.subscriber_count == 20
|
||||
assert Decimal.equal?(updated.total_mrr, Decimal.new("1000.00"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_billing_summary/1" do
|
||||
test "sums across multiple billing integrations", %{organization: org} do
|
||||
{:ok, i1} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i1, 100, Decimal.new("5000.00"))
|
||||
|
||||
user2 = user_fixture()
|
||||
org2 = organization_fixture(user2.id)
|
||||
|
||||
{:ok, i2} =
|
||||
Integrations.create_integration(org2.id, %{provider: "splynx", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i2, 50, Decimal.new("2500.00"))
|
||||
|
||||
# Only org's integrations
|
||||
summary = Integrations.get_billing_summary(org.id)
|
||||
assert summary.total_subscribers == 100
|
||||
assert Decimal.equal?(summary.total_mrr, Decimal.new("5000.00"))
|
||||
end
|
||||
|
||||
test "excludes disabled integrations", %{organization: org} do
|
||||
{:ok, i1} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i1, 100, Decimal.new("5000.00"))
|
||||
|
||||
{:ok, i2} =
|
||||
Integrations.create_integration(org.id, %{provider: "splynx", enabled: false})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i2, 50, Decimal.new("2500.00"))
|
||||
|
||||
summary = Integrations.get_billing_summary(org.id)
|
||||
assert summary.total_subscribers == 100
|
||||
assert Decimal.equal?(summary.total_mrr, Decimal.new("5000.00"))
|
||||
end
|
||||
|
||||
test "excludes non-billing integrations", %{organization: org} do
|
||||
{:ok, i1} =
|
||||
Integrations.create_integration(org.id, %{provider: "gaiia", enabled: true})
|
||||
|
||||
{:ok, _} = Integrations.update_billing_totals(i1, 100, Decimal.new("5000.00"))
|
||||
|
||||
{:ok, _} =
|
||||
Integrations.create_integration(org.id, %{
|
||||
provider: "preseem",
|
||||
enabled: true
|
||||
})
|
||||
|
||||
summary = Integrations.get_billing_summary(org.id)
|
||||
assert summary.total_subscribers == 100
|
||||
assert Decimal.equal?(summary.total_mrr, Decimal.new("5000.00"))
|
||||
end
|
||||
|
||||
test "returns zeros when no billing integrations", %{organization: org} do
|
||||
summary = Integrations.get_billing_summary(org.id)
|
||||
assert summary.total_subscribers == 0
|
||||
assert Decimal.equal?(summary.total_mrr, Decimal.new(0))
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_enabled_integrations/1" do
|
||||
test "returns only enabled integrations for a provider", %{organization: org} do
|
||||
{:ok, _} = Integrations.create_integration(org.id, %{provider: "preseem", enabled: true})
|
||||
|
|
|
|||
|
|
@ -120,21 +120,22 @@ defmodule ToweropsWeb.DashboardLiveTest do
|
|||
refute html =~ "MRR"
|
||||
end
|
||||
|
||||
test "shows subscriber section when Gaiia data exists", %{
|
||||
test "shows subscriber section when billing data exists", %{
|
||||
conn: conn,
|
||||
organization: organization,
|
||||
site: site
|
||||
} do
|
||||
{:ok, _device} = create_device(organization, site)
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
||||
gaiia_id: "ns-dash",
|
||||
name: "Tower",
|
||||
account_count: 100,
|
||||
total_mrr: Decimal.new("5000.00")
|
||||
{:ok, integration} =
|
||||
Towerops.Integrations.create_integration(organization.id, %{
|
||||
provider: "gaiia",
|
||||
enabled: true
|
||||
})
|
||||
|
||||
{:ok, _} =
|
||||
Towerops.Integrations.update_billing_totals(integration, 100, Decimal.new("5000.00"))
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
||||
|
||||
assert html =~ "Subs"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue