81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
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<string, string[]>();
|
|
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);
|
|
}
|