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