76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { getSessionUser } from "@/lib/session";
|
|
import { computeInvoiceDraft } from "@/lib/billing";
|
|
import { listInvoices } from "@/lib/db";
|
|
|
|
/**
|
|
* GET /api/billing/current
|
|
*
|
|
* Running total for the current calendar month — what the
|
|
* customer will be billed if no further activity happens. Uses
|
|
* the same compute pipeline as the final invoice (LiteLLM spend,
|
|
* Threema usage, skill day-counting, proration) so the number
|
|
* the customer sees matches what they'll eventually receive
|
|
* within the limits of intra-month drift.
|
|
*
|
|
* If an invoice has ALREADY been issued for the current month
|
|
* (e.g. cron ran early, admin manually generated), we return
|
|
* that issued invoice instead — no point showing a draft that
|
|
* duplicates a real invoice.
|
|
*
|
|
* Returns:
|
|
* { issued: Invoice } // current-month invoice exists
|
|
* { draft: InvoiceDraft } // still accruing
|
|
* { error: ... } // org missing billing config
|
|
*
|
|
* Cost: 1 LiteLLM HTTP call + 1 Threema HTTP call + a handful of
|
|
* DB queries per skill. Sub-second typically. No caching; called
|
|
* on demand from the customer billing page.
|
|
*/
|
|
export async function GET() {
|
|
const user = await getSessionUser();
|
|
if (!user) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
}
|
|
// Resolve current calendar month from UTC. Billing is UTC-day
|
|
// based throughout (see billing.ts iterDays comment), so the
|
|
// running total inherits that same semantics.
|
|
const now = new Date();
|
|
const year = now.getUTCFullYear();
|
|
const month = now.getUTCMonth() + 1; // 1-12
|
|
const periodMonth = `${year}-${String(month).padStart(2, "0")}`;
|
|
|
|
// 1. Has the current month already been invoiced?
|
|
const existing = await listInvoices({
|
|
zitadelOrgId: user.orgId,
|
|
periodMonth,
|
|
limit: 1,
|
|
});
|
|
if (existing.length > 0) {
|
|
return NextResponse.json({ issued: existing[0] });
|
|
}
|
|
|
|
// 2. Otherwise compute the draft. Falls through to error if the
|
|
// org doesn't have a billing config yet (no Address on file).
|
|
try {
|
|
const draft = await computeInvoiceDraft({
|
|
zitadelOrgId: user.orgId,
|
|
year,
|
|
month,
|
|
});
|
|
return NextResponse.json({ draft });
|
|
} catch (e: any) {
|
|
// Most likely: org_billing row missing. We surface a 200 with a
|
|
// soft error code rather than 500 — the customer-side widget
|
|
// displays a helpful "complete your billing details" message
|
|
// instead of a stack trace.
|
|
return NextResponse.json(
|
|
{
|
|
error: e?.message ?? "Could not compute running total.",
|
|
code: e?.code ?? "COMPUTE_FAILED",
|
|
},
|
|
{ status: 200 }
|
|
);
|
|
}
|
|
}
|