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