67 lines
2.0 KiB
TypeScript
67 lines
2.0 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { requirePlatformRole } from "@/lib/session";
|
|
import { generateInvoice } from "@/lib/billing";
|
|
import { safeError } from "@/lib/errors";
|
|
|
|
/**
|
|
* POST /api/admin/billing/generate
|
|
*
|
|
* Compute (and optionally commit) an invoice for an (org, year,
|
|
* month). Platform-only — this is the testing/admin tool.
|
|
*
|
|
* Body:
|
|
* {
|
|
* zitadelOrgId: string,
|
|
* year: number (e.g. 2026),
|
|
* month: number (1-12),
|
|
* locale?: 'de' | 'en' | 'fr' | 'it', // default: from country
|
|
* dryRun?: boolean // default: false
|
|
* }
|
|
*
|
|
* Response on success:
|
|
* {
|
|
* draft: InvoiceDraft, // line breakdown + warnings
|
|
* invoice: Invoice | null, // null when dryRun=true
|
|
* }
|
|
*
|
|
* If an invoice for that (org, period) already exists, returns
|
|
* 409 with a clear message. Use the delete endpoint first to
|
|
* regenerate.
|
|
*/
|
|
|
|
const bodySchema = z.object({
|
|
zitadelOrgId: z.string().min(1),
|
|
year: z.number().int().min(2020).max(2100),
|
|
month: z.number().int().min(1).max(12),
|
|
locale: z.enum(["de", "en", "fr", "it"]).optional(),
|
|
dryRun: z.boolean().optional().default(false),
|
|
});
|
|
|
|
export async function POST(request: Request) {
|
|
try {
|
|
await requirePlatformRole();
|
|
} catch {
|
|
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
}
|
|
const body = await request.json().catch(() => ({}));
|
|
const parsed = bodySchema.safeParse(body);
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid request", details: parsed.error.flatten() },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
try {
|
|
const result = await generateInvoice(parsed.data);
|
|
return NextResponse.json(result);
|
|
} catch (e: any) {
|
|
console.error("Invoice generation failed:", e);
|
|
const msg = safeError(e, "Generation failed");
|
|
// Specific 409 for the "already exists" case so the UI can
|
|
// show a "delete first" link.
|
|
const status = /already exists/i.test(msg) ? 409 : 500;
|
|
return NextResponse.json({ error: msg }, { status });
|
|
}
|
|
}
|