import { NextResponse } from "next/server"; import { requirePlatformRole } from "@/lib/session"; import { listTenants } from "@/lib/k8s"; import { getOrgBilling, getOrgOpenBalances } from "@/lib/db"; /** * GET /api/admin/billing/orgs * * Returns the orgs known to the platform via tenant labels, with * their billing-address-on-file status and open balance summary. * Powers the generate form's org dropdown and the billing landing * page's open-balance table. * * Each entry: * { * zitadelOrgId: string, * tenantCount: number, * hasBillingAddress: boolean, * companyName: string | null, * openCount: number, * overdueCount: number, * totalOpenChf: number * } */ export async function GET() { try { await requirePlatformRole(); } catch { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } // Org membership is derived from tenant labels — there's no // separate "orgs" table on the portal. listTenants reads from // K8s, which is the source of truth. const tenants = await listTenants(); const orgIdToTenants = new Map(); for (const t of tenants) { const oid = t.metadata.labels?.["pieced.ch/zitadel-org-id"]; if (!oid) continue; if (!orgIdToTenants.has(oid)) orgIdToTenants.set(oid, []); orgIdToTenants.get(oid)!.push(t.metadata.name); } const balances = await getOrgOpenBalances(); const balanceMap = new Map(balances.map((b) => [b.zitadelOrgId, b])); // Hydrate billing-address presence + company name per org. const results = await Promise.all( [...orgIdToTenants.entries()].map(async ([orgId, tenantNames]) => { const billing = await getOrgBilling(orgId).catch(() => null); const bal = balanceMap.get(orgId); return { zitadelOrgId: orgId, tenantCount: tenantNames.length, tenantNames, hasBillingAddress: !!billing, companyName: billing?.companyName ?? null, country: billing?.country ?? null, openCount: bal?.openCount ?? 0, overdueCount: bal?.overdueCount ?? 0, totalOpenChf: bal?.totalOpenChf ?? 0, }; }) ); // Sort: orgs with overdue first, then open, then by name. results.sort((a, b) => { if (a.overdueCount !== b.overdueCount) { return b.overdueCount - a.overdueCount; } if (a.openCount !== b.openCount) { return b.openCount - a.openCount; } return (a.companyName ?? a.zitadelOrgId).localeCompare( b.companyName ?? b.zitadelOrgId ); }); return NextResponse.json(results); }