Phase5: Automate bill creation
All checks were successful
Build and Push / build (push) Successful in 1m43s
All checks were successful
Build and Push / build (push) Successful in 1m43s
This commit is contained in:
68
src/app/api/admin/cron/issue-monthly/route.ts
Normal file
68
src/app/api/admin/cron/issue-monthly/route.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user