Files
arcadia-cloud/lib/arcadia_cloud_web/controllers/billing_controller.ex
Giuliano Silvestro c10b847324 Fix operator role gate: platform-admin (hyphen), not platform_admin
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>
2026-05-20 18:17:13 +10:00

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