71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requirePlatformRole } from "@/lib/session";
|
|
import { listTenants } from "@/lib/k8s";
|
|
import { backfillTenantBillingLifecycle } from "@/lib/db";
|
|
import { safeError } from "@/lib/errors";
|
|
|
|
/**
|
|
* POST /api/admin/billing/backfill
|
|
*
|
|
* One-off bootstrap that reads every live PiecedTenant CR and
|
|
* mirrors it into the Phase 1 billing tables:
|
|
* - tenant_billing_lifecycle.created_at ← CR's creationTimestamp
|
|
* - tenant_skill_events: one 'enabled' event per package in
|
|
* spec.packages, anchored at the CR's creationTimestamp
|
|
* - tenant_suspension_events: one 'suspended' event if the CR is
|
|
* currently suspended (anchored at status.suspendedAt)
|
|
*
|
|
* Idempotent — re-running is safe. The helper only inserts rows
|
|
* for tenants that have no lifecycle row / no events yet; running
|
|
* twice produces zero additional rows.
|
|
*
|
|
* Authorization: platform role only. The body of the request is
|
|
* ignored.
|
|
*
|
|
* Response: counts of rows inserted, mostly for sanity-checking
|
|
* (expect non-zero on first run, zero on subsequent runs).
|
|
*
|
|
* Phase 2 will surface this behind an admin UI button.
|
|
*/
|
|
export async function POST() {
|
|
try {
|
|
await requirePlatformRole();
|
|
} catch {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
|
|
try {
|
|
const tenants = await listTenants();
|
|
const result = await backfillTenantBillingLifecycle(
|
|
tenants.map((t) => ({
|
|
name: t.metadata.name,
|
|
// Tenants without the org label exist as a pre-Slice-3
|
|
// artifact; we still record them but with 'unknown' as the
|
|
// org id, which surfaces them in admin reports for manual
|
|
// labelling. Per-org billing computation skips rows with
|
|
// org id = 'unknown'.
|
|
zitadelOrgId:
|
|
t.metadata.labels?.["pieced.ch/zitadel-org-id"] ?? "unknown",
|
|
createdAt: t.metadata.creationTimestamp
|
|
? new Date(t.metadata.creationTimestamp)
|
|
: new Date(),
|
|
packages: t.spec.packages ?? [],
|
|
suspendedAt: t.status?.suspendedAt
|
|
? new Date(t.status.suspendedAt)
|
|
: null,
|
|
}))
|
|
);
|
|
return NextResponse.json({
|
|
message: "Backfill complete.",
|
|
tenantsExamined: tenants.length,
|
|
...result,
|
|
});
|
|
} catch (e: any) {
|
|
console.error("Backfill failed:", e);
|
|
return NextResponse.json(
|
|
{ error: safeError(e, "Backfill failed") },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|