import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getSessionUser } from "@/lib/session"; import { listTenants } from "@/lib/k8s"; import { getOrgBilling } from "@/lib/db"; import { BackLink } from "@/components/ui/back-link"; import { NewInvoiceForm } from "@/components/admin/billing/new-invoice-form"; /** * /admin/billing/invoices/new — entry point for the custom-invoice * flow. The admin picks an org, clicks Continue, and lands on the * editor at /admin/billing/invoice-drafts/. * * Phase 8. Org list is built from tenant labels + each org's * billing config (we need the company name and the * has-billing-snapshot flag to gate the picker — orgs without a * snapshot can't be invoiced until they complete onboarding or * admin sets the billing info manually). */ export default async function NewInvoicePage() { const user = await getSessionUser(); if (!user) redirect("/login"); if (!user.isPlatform) redirect("/dashboard"); const t = await getTranslations("adminBilling"); // Tenants give us org membership; getOrgBilling per org gives us // the snapshot status. We dedupe by org id since one org can own // many tenants. const tenants = await listTenants(); const orgIds = new Set(); for (const tnt of tenants) { const oid = tnt.metadata.labels?.["pieced.ch/zitadel-org-id"]; if (oid) orgIds.add(oid); } const orgs = await Promise.all( Array.from(orgIds).map(async (oid) => { const billing = await getOrgBilling(oid).catch(() => null); return { zitadelOrgId: oid, companyName: billing?.companyName ?? null, country: billing?.country ?? null, hasBillingAddress: !!billing && !!billing.companyName, }; }) ); // Sort: orgs with billing first (admin's most likely target), // then alphabetically by company name. orgs.sort((a, b) => { if (a.hasBillingAddress !== b.hasBillingAddress) { return a.hasBillingAddress ? -1 : 1; } return (a.companyName ?? "").localeCompare(b.companyName ?? ""); }); return (

{t("newInvoicePageTitle")}

{t("newInvoicePageSubtitle")}

); }