From 38f4c3243e8151d20e84f98ae49964d06322ac6a Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 26 May 2026 23:08:07 +0200 Subject: [PATCH] Phase7b: Manual Invoice --- .../admin/billing/invoice-drafts/page.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/app/[locale]/admin/billing/invoice-drafts/page.tsx b/src/app/[locale]/admin/billing/invoice-drafts/page.tsx index f15c66d..465a2e4 100644 --- a/src/app/[locale]/admin/billing/invoice-drafts/page.tsx +++ b/src/app/[locale]/admin/billing/invoice-drafts/page.tsx @@ -1,7 +1,7 @@ import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getSessionUser } from "@/lib/session"; -import { listAllInvoiceDrafts } from "@/lib/db"; +import { getOrgBilling, listAllInvoiceDrafts } from "@/lib/db"; import { listTenants } from "@/lib/k8s"; import { BackLink } from "@/components/ui/back-link"; import { DraftList } from "@/components/admin/billing/draft-list"; @@ -14,9 +14,9 @@ import { DraftList } from "@/components/admin/billing/draft-list"; * an invoice; visible only to platform admins. From here the admin * can resume editing or discard. * - * Building an org-name map by reading tenant labels (same approach - * as the existing /admin/billing/orgs endpoint) so the table can - * show "Customer X" instead of a raw ZITADEL org id. + * Building an org-name map by reading tenant labels (for the set of + * known orgs) + getOrgBilling per org (for the actual company name) + * so the table can show "Customer X" instead of a raw ZITADEL org id. */ export default async function AdminInvoiceDraftsPage() { const user = await getSessionUser(); @@ -29,16 +29,30 @@ export default async function AdminInvoiceDraftsPage() { listTenants().catch(() => []), ]); - // Build org-id → company-name map from tenant labels. Same shape - // the existing /api/admin/billing/orgs uses. Falls back to the - // raw org id when we don't have a tenant label match. + // Build the set of distinct ZITADEL org ids from tenant labels, + // PLUS the set referenced by any current draft. Drafts may target + // orgs that don't have tenants yet (rare but possible), so we + // union both sources before fetching billing rows. + const orgIds = new Set(); + for (const tnt of tenants) { + const oid = tnt.metadata.labels?.["pieced.ch/zitadel-org-id"]; + if (oid) orgIds.add(oid); + } + for (const d of drafts) { + orgIds.add(d.zitadelOrgId); + } + // Look up billing in parallel — same pattern as + // /api/admin/billing/orgs uses. Failure for any single org is + // non-fatal (falls back to the raw id in the table). + const orgNamePairs = await Promise.all( + Array.from(orgIds).map(async (oid) => { + const billing = await getOrgBilling(oid).catch(() => null); + return [oid, billing?.companyName ?? null] as const; + }) + ); const orgNameMap: Record = {}; - for (const t of tenants) { - const oid = t.metadata.labels?.["pieced.ch/zitadel-org-id"]; - const company = t.spec?.billing?.companyName; - if (oid && company && !orgNameMap[oid]) { - orgNameMap[oid] = company; - } + for (const [oid, name] of orgNamePairs) { + if (name) orgNameMap[oid] = name; } return (