arcadia-app issues the role slug "platform-admin" (hyphen) — confirmed from a live arcadia-dev JWT (roles: ["admin","platform-admin"]). Every authorization check here tested for "platform_admin" (underscore), so real operator tokens got 403 on billing / dashboard / drift and an empty tenant-scoped result on inventory. The smoke tests missed it because Guardian.mint_dev_token hardcoded the underscore form — fixed there too, so the dev helper now matches what arcadia-app actually emits. Replaced the string literal "platform_admin" -> "platform-admin" in all six controllers + guardian.ex. The platform_admin?/1 function names keep underscores (Elixir identifiers can't contain hyphens) — only the role string changed. Verified: with a platform-admin token, /inventory, /billing/balance, /dashboard/margin and /drift all return 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
2.5 KiB
Elixir
102 lines
2.5 KiB
Elixir
defmodule ArcadiaCloudWeb.BillingController do
|
|
@moduledoc """
|
|
Cost ingestion read endpoints. Phase 1 is operator-only (platform_admin).
|
|
Tenant-scoped per-deployment cost queries land in phase 3 with the
|
|
monthly_margin_rollup view.
|
|
"""
|
|
|
|
use ArcadiaCloudWeb, :controller
|
|
|
|
alias ArcadiaCloud.Billing
|
|
|
|
def balance(conn, _params) do
|
|
with :ok <- require_platform_admin(conn) do
|
|
snap = Billing.latest_balance()
|
|
json(conn, %{balance: shape_balance(snap)})
|
|
end
|
|
end
|
|
|
|
def cost_lines(conn, params) do
|
|
with :ok <- require_platform_admin(conn) do
|
|
opts =
|
|
[]
|
|
|> maybe_put(:period, parse_date(params["period"]))
|
|
|> maybe_put(:kind, params["kind"])
|
|
|> maybe_put(:limit, parse_int(params["limit"]) || 200)
|
|
|
|
lines =
|
|
Billing.list_cost_lines(opts)
|
|
|> Enum.map(&shape_line/1)
|
|
|
|
json(conn, %{cost_lines: lines, count: length(lines)})
|
|
end
|
|
end
|
|
|
|
# ---- helpers --------------------------------------------------------------
|
|
|
|
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
|
|
|
|
defp shape_balance(nil), do: nil
|
|
|
|
defp shape_balance(s) do
|
|
%{
|
|
provider: s.provider,
|
|
month_to_date_balance_cents: s.month_to_date_balance_cents,
|
|
account_balance_cents: s.account_balance_cents,
|
|
month_to_date_usage_cents: s.month_to_date_usage_cents,
|
|
generated_at: s.generated_at
|
|
}
|
|
end
|
|
|
|
defp shape_line(l) do
|
|
%{
|
|
id: l.id,
|
|
invoice_id: l.invoice_id,
|
|
resource_id: l.resource_id,
|
|
invoice_period: l.invoice_period,
|
|
kind: l.kind,
|
|
description: l.description,
|
|
qty: l.qty,
|
|
unit: l.unit,
|
|
amount_cents: l.amount_cents,
|
|
project_name: l.project_name,
|
|
category: l.category,
|
|
matched_at: l.matched_at
|
|
}
|
|
end
|
|
|
|
defp parse_date(nil), do: nil
|
|
defp parse_date(""), do: nil
|
|
|
|
defp parse_date(str) do
|
|
case Date.from_iso8601(str) do
|
|
{:ok, d} -> d
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
defp parse_int(nil), do: nil
|
|
defp parse_int(""), do: nil
|
|
|
|
defp parse_int(str) when is_binary(str) do
|
|
case Integer.parse(str) do
|
|
{n, _} -> n
|
|
:error -> nil
|
|
end
|
|
end
|
|
|
|
defp maybe_put(opts, _key, nil), do: opts
|
|
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
|
|
end
|