Files
pieced-portal/src/app/api/admin/billing/orgs/route.ts
admin c8ed27157f
Some checks failed
Build and Push / build (push) Failing after 28s
Phase2: Invoicecomputation/AdminpricingUI/Ainvoicemgnt
2026-05-24 13:51:38 +02:00

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);
}