Phase 3: cost-vs-revenue dashboard

ArcadiaCloud.Analytics — the operator margin view. Revenue (tenant
invoices, ex-GST) vs COGS (DO cost lines), margin = revenue - COGS.

- margin_summary/1 — overall P&L + per-tenant + per-deployment margin
  for a month.
- by_kind/1 — revenue and COGS broken down by resource kind (separate
  axes; billing kinds and DO kinds don't 1:1).
- live_accrual/0 — current-month unbilled: runs the quote engine with
  partial metered usage per active subscription. "What tenants are
  racking up right now before the rollup."

COGS-to-tenant attribution uses COALESCE(deployment.tenant_id,
resource.tenant_id) — a resource in a deployment bills to the
deployment's tenant; standalone resources fall back to their own
tenant_id (skyai-internal infra).

API (platform_admin only):
- GET /api/v1/dashboard/margin?period=YYYY-MM-DD
- GET /api/v1/dashboard/accrual

Schema fix: cloud_resources.tenant_id and cloud_projects.tenant_id were
binary_id (UUID) while cloud_deployments / tenant_invoices use string.
Migrated both to text — a UUID is a valid string, arcadia also uses
non-UUID tenant slugs ("platform-admin"), and the type alignment lets
the analytics COALESCE join work. Side benefit: kills the phase-1 bug
where a non-UUID tenant_id claim crashed the inventory query.

Smoke verified against real ingested April DO COGS: pilot deployment on
Studio with 5 droplets, metered + rolled up — by_tenant shows
dashboard-pilot rev $71.12 / COGS $30.53 / margin $40.59 (57.1%);
overall P&L revenue $71.12 vs all-April COGS $86.92 = -$15.80 (the
pilot's revenue doesn't cover Sky AI's full April infra — correct, the
rest is internal/unattributed).

Phase 3 complete: catalog, deployments+subscriptions, quote engine,
metering, invoice rollup, cost-vs-revenue dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 15:57:08 +10:00
parent 3d54078c60
commit 6ec7e9b93a
6 changed files with 286 additions and 2 deletions

View File

@@ -0,0 +1,51 @@
defmodule ArcadiaCloudWeb.DashboardController do
@moduledoc """
Cost-vs-revenue dashboard. platform_admin only — this is the operator's
margin view across all tenants.
"""
use ArcadiaCloudWeb, :controller
alias ArcadiaCloud.Analytics
def margin(conn, params) do
with :ok <- require_platform_admin(conn) do
period = parse_period(params["period"])
summary = Analytics.margin_summary(period)
by_kind = Analytics.by_kind(period)
json(conn, Map.merge(summary, by_kind))
end
end
def accrual(conn, _params) do
with :ok <- require_platform_admin(conn) do
accrual = Analytics.live_accrual()
json(conn, %{
accrual: accrual,
total_accrued_cents: Enum.reduce(accrual, 0, &(&1.accrued_cents + &2))
})
end
end
defp require_platform_admin(conn) do
identity = conn.assigns.current_identity
if is_list(identity.roles) and "platform_admin" in identity.roles do
:ok
else
conn |> put_status(:forbidden) |> json(%{error: "platform_admin_required"}) |> halt()
end
end
# period defaults to the previous calendar month (what the last rollup invoiced)
defp parse_period(nil), do: Date.beginning_of_month(Date.add(Date.beginning_of_month(Date.utc_today()), -1))
defp parse_period(""), do: parse_period(nil)
defp parse_period(str) do
case Date.from_iso8601(str) do
{:ok, d} -> Date.beginning_of_month(d)
_ -> parse_period(nil)
end
end
end

View File

@@ -39,5 +39,8 @@ defmodule ArcadiaCloudWeb.Router do
get "/invoices", InvoiceController, :index
get "/invoices/:id", InvoiceController, :show
get "/dashboard/margin", DashboardController, :margin
get "/dashboard/accrual", DashboardController, :accrual
end
end