Files
pieced-portal/src/app/[locale]/admin/billing/orgs/page.tsx
admin ee6bb89fb6
Some checks failed
Build and Push / build (push) Failing after 42s
Phase8: Auto bill credit card
2026-05-27 22:06:32 +02:00

84 lines
3.0 KiB
TypeScript

import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { getSessionUser } from "@/lib/session";
import { getOrgBilling, getOrgBillingConfig } from "@/lib/db";
import { listTenants } from "@/lib/k8s";
import { BackLink } from "@/components/ui/back-link";
import { OrgPaymentModeList } from "@/components/admin/billing/org-payment-mode-list";
/**
* /admin/billing/orgs — list of orgs with their payment mode
* settings.
*
* Phase 9b-2. The customer's /settings/billing only exposes the
* saved-card flow (auto-pay). Bank-transfer mode is admin-only —
* customer must contact support to request it, admin flips the
* pay_by_invoice flag here. Also exposes the auto_charge_enabled
* pause-switch for support situations.
*
* The page is intentionally minimal: org name, country, current
* mode, has-saved-card indicator, and toggles. Detail-level work
* (open balances, invoice list) is on the existing pages
* (/admin/billing, /admin/billing/invoices).
*/
export default async function AdminOrgsPaymentModePage() {
const user = await getSessionUser();
if (!user) redirect("/login");
if (!user.isPlatform) redirect("/dashboard");
const t = await getTranslations("adminBilling");
// Same org-discovery pattern as /api/admin/billing/orgs: tenant
// labels are the source of truth for org membership. We dedupe by
// org id since one org can own many tenants.
const tenants = await listTenants().catch(() => []);
const orgIds = new Set<string>();
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, cfg] = await Promise.all([
getOrgBilling(oid).catch(() => null),
getOrgBillingConfig(oid),
]);
return {
zitadelOrgId: oid,
companyName: billing?.companyName ?? null,
country: billing?.country ?? null,
hasSavedCard: !!cfg.stripeDefaultPaymentMethodId,
cardLabel:
cfg.stripePmBrand && cfg.stripePmLast4
? `${cfg.stripePmBrand} •••• ${cfg.stripePmLast4}`
: null,
payByInvoice: !!cfg.payByInvoice,
autoChargeEnabled: cfg.autoChargeEnabled !== false,
};
})
);
// Sort: orgs with billing first (most actionable), then by name.
orgs.sort((a, b) => {
if (!!a.companyName !== !!b.companyName) {
return a.companyName ? -1 : 1;
}
return (a.companyName ?? a.zitadelOrgId).localeCompare(
b.companyName ?? b.zitadelOrgId
);
});
return (
<main className="max-w-6xl mx-auto px-6 py-8">
<BackLink href="/admin/billing" label={t("backToBilling")} />
<div className="mb-6">
<h1 className="font-display text-2xl font-semibold accent-rule">
{t("orgsPageTitle")}
</h1>
<p className="text-sm text-text-secondary mt-3">
{t("orgsPageSubtitle")}
</p>
</div>
<OrgPaymentModeList orgs={orgs} />
</main>
);
}