import { NextResponse } from "next/server"; import { z } from "zod"; import { getSessionUser, requirePlatformRole } from "@/lib/session"; import { runMonthlyIssuance } from "@/lib/cron"; import { safeError } from "@/lib/errors"; /** * POST /api/admin/cron/issue-monthly * * Admin-side manual trigger for the issuance sweep — same business * logic as /api/cron/issue-monthly, different auth (session-based * platform role check) and the option to override the target * year/month from the request body. * * Body (all optional): * { year?: number, month?: number } * * Default target is the previous local month — matching what the * automated cron would do. Override is useful for catching up after * a failed run or re-billing a past month after fixing data. */ const bodySchema = z.object({ year: z.number().int().min(2000).max(3000).optional(), month: z.number().int().min(1).max(12).optional(), }); export async function POST(request: Request) { let user; try { await requirePlatformRole(); user = await getSessionUser(); } catch { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } 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 } ); } if ( (parsed.data.year && !parsed.data.month) || (parsed.data.month && !parsed.data.year) ) { return NextResponse.json( { error: "year and month must both be provided, or neither" }, { status: 400 } ); } try { const { runId, summary } = await runMonthlyIssuance({ triggeredBy: user.id, year: parsed.data.year, month: parsed.data.month, }); return NextResponse.json({ runId, ...summary }); } catch (e) { return NextResponse.json( { error: safeError(e, "Issuance sweep failed.") }, { status: 500 } ); } }