defmodule ArcadiaCloud.Analytics do @moduledoc """ Cost-vs-revenue analytics — the operator's margin view. Revenue side: tenant_invoice_lines / tenant_invoices (what tenants owe Sky AI, ex-GST). COGS side: cloud_cost_lines (what DO bills Sky AI), attributed to a tenant/deployment via cloud_resources. margin = revenue (ex-GST) - COGS. GST is pass-through, excluded both sides. Phase 3 runs these queries directly. A monthly_margin_rollup materialized view is the optimization for when the dashboard is slow — deliberately deferred. """ import Ecto.Query, warn: false alias ArcadiaCloud.Repo alias ArcadiaCloud.Cloud.CloudResource alias ArcadiaCloud.Billing.{CloudCostLine, TenantInvoice, TenantInvoiceLine} alias ArcadiaCloud.Deployments.CloudDeployment alias ArcadiaCloud.{Catalog, Metering, Quoting, Subscriptions} @doc """ Margin summary for a month. `period` is the first-of-month date. Returns overall P&L plus per-tenant and per-deployment breakdowns. """ def margin_summary(%Date{} = period) do revenue_overall = total_revenue(period) cogs_overall = total_cogs(period) %{ period: period, overall: margin_row(revenue_overall, cogs_overall), by_tenant: by_tenant(period), by_deployment: by_deployment(period) } end # ---- overall -------------------------------------------------------------- defp total_revenue(period) do from(i in TenantInvoice, where: i.period_start == ^period, select: coalesce(sum(i.subtotal_cents), 0) ) |> Repo.one() end defp total_cogs(period) do from(cl in CloudCostLine, where: cl.invoice_period == ^period, select: coalesce(sum(cl.amount_cents), 0) ) |> Repo.one() end # ---- per tenant ----------------------------------------------------------- defp by_tenant(period) do revenue = from(i in TenantInvoice, where: i.period_start == ^period, group_by: i.tenant_id, select: {i.tenant_id, sum(i.subtotal_cents)} ) |> Repo.all() |> Map.new() # A resource's billing tenant is its deployment's tenant if it's in a # deployment, else its own tenant_id (skyai-internal infra). cogs = from(cl in CloudCostLine, join: r in CloudResource, on: r.id == cl.resource_id, left_join: d in CloudDeployment, on: d.id == r.deployment_id, where: cl.invoice_period == ^period, group_by: fragment("COALESCE(?, ?)", d.tenant_id, r.tenant_id), select: {fragment("COALESCE(?, ?)", d.tenant_id, r.tenant_id), sum(cl.amount_cents)} ) |> Repo.all() |> Enum.reject(fn {tenant_id, _} -> is_nil(tenant_id) end) |> Map.new() merge_keys(revenue, cogs) |> Enum.map(fn tenant_id -> Map.put( margin_row(Map.get(revenue, tenant_id, 0), Map.get(cogs, tenant_id, 0)), :tenant_id, tenant_id ) end) |> Enum.sort_by(& &1.margin_cents) end # ---- per deployment ------------------------------------------------------- defp by_deployment(period) do revenue = from(l in TenantInvoiceLine, join: i in TenantInvoice, on: i.id == l.invoice_id, where: i.period_start == ^period and not is_nil(l.deployment_id) and l.kind != "tax", group_by: l.deployment_id, select: {l.deployment_id, sum(l.amount_cents)} ) |> Repo.all() |> Map.new() cogs = from(cl in CloudCostLine, join: r in CloudResource, on: r.id == cl.resource_id, where: cl.invoice_period == ^period and not is_nil(r.deployment_id), group_by: r.deployment_id, select: {r.deployment_id, sum(cl.amount_cents)} ) |> Repo.all() |> Map.new() names = from(d in CloudDeployment, select: {d.id, d.slug}) |> Repo.all() |> Map.new() merge_keys(revenue, cogs) |> Enum.map(fn dep_id -> margin_row(Map.get(revenue, dep_id, 0), Map.get(cogs, dep_id, 0)) |> Map.put(:deployment_id, dep_id) |> Map.put(:deployment_slug, Map.get(names, dep_id)) end) |> Enum.sort_by(& &1.margin_cents) end # ---- by kind -------------------------------------------------------------- @doc "Revenue and COGS broken down by resource kind (separate axes — DO kinds and billing kinds don't 1:1)." def by_kind(%Date{} = period) do revenue = from(l in TenantInvoiceLine, join: i in TenantInvoice, on: i.id == l.invoice_id, where: i.period_start == ^period and not is_nil(l.resource_kind), group_by: l.resource_kind, select: {l.resource_kind, sum(l.amount_cents)} ) |> Repo.all() |> Enum.map(fn {k, v} -> %{kind: k, amount_cents: v} end) cogs = from(cl in CloudCostLine, where: cl.invoice_period == ^period and not is_nil(cl.kind), group_by: cl.kind, select: {cl.kind, sum(cl.amount_cents)} ) |> Repo.all() |> Enum.map(fn {k, v} -> %{kind: k, amount_cents: v} end) %{revenue_by_kind: revenue, cogs_by_kind: cogs} end # ---- live accrual --------------------------------------------------------- @doc """ Current-month unbilled accrual per tenant — runs the quote engine with partial metered usage. Answers "what are tenants racking up right now, before the month-end rollup". """ def live_accrual do today = Date.utc_today() month_start = Date.beginning_of_month(today) Subscriptions.list_active_subscriptions() |> Enum.map(fn sub -> deployment = Repo.get(CloudDeployment, sub.deployment_id) plan_version = Catalog.get_version(sub.plan_version_id) addons = Subscriptions.list_addons(sub.id) usage = Metering.usage_for_period(sub.deployment_id, month_start, today) quote = Quoting.quote(plan_version, addons, usage: usage) %{ tenant_id: deployment && deployment.tenant_id, deployment_id: sub.deployment_id, deployment_slug: deployment && deployment.slug, accrued_cents: quote.all_in_monthly_cents, recurring_cents: quote.recurring.monthly_total_cents, overage_cents: quote.overage.monthly_total_cents } end) end # ---- helpers -------------------------------------------------------------- defp margin_row(revenue, cogs) do revenue = revenue || 0 cogs = cogs || 0 margin = revenue - cogs %{ revenue_cents: revenue, cogs_cents: cogs, margin_cents: margin, margin_pct: if(revenue > 0, do: Float.round(margin / revenue * 100, 1), else: nil) } end defp merge_keys(map_a, map_b) do (Map.keys(map_a) ++ Map.keys(map_b)) |> Enum.uniq() end end