Store billing subscriber/MRR data on integrations for dashboard
Each billing sync (Gaiia, Splynx, VISP, Sonar) already computes subscriber counts and MRR but only stored them in sync messages. Now persists them on the integrations table so the dashboard can aggregate across all billing providers instead of only querying Gaiia network sites (which were never populated). Also fixes SnmpKit Config environment detection to use Mix.env() as fallback when MIX_ENV env var is not set.
This commit is contained in:
parent
6f48171e55
commit
de075ea8d9
15 changed files with 266 additions and 60 deletions
|
|
@ -448,6 +448,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
|
||||
Code.ensure_loaded?(Mix) -> Mix.env()
|
||||
true -> :dev
|
||||
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
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ 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)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
|
|
@ -168,6 +169,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))
|
||||
|
|
|
|||
|
|
@ -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