Phase3: Billing Customerpage/Mailings
Some checks failed
Build and Push / build (push) Failing after 46s

This commit is contained in:
2026-05-24 21:44:10 +02:00
parent a3b080f542
commit cf190e5ac5
17 changed files with 1057 additions and 4 deletions

View File

@@ -0,0 +1,43 @@
import { NextResponse } from "next/server";
import { getSessionUser } from "@/lib/session";
import { getInvoiceByNumberForOrg, getInvoicePdf } from "@/lib/db";
/**
* GET /api/billing/invoices/[invoiceNumber]/pdf
*
* Customer-facing PDF download. Same Uint8Array.from() variance
* fix as the admin route — see /api/admin/billing/invoices/[id]/pdf
* for the rationale.
*
* Authorization: looks up the invoice by number with org scope
* baked into the query, then re-fetches the PDF blob by id. A
* customer can't probe another org's invoice numbers — they get
* 404 either way.
*/
export async function GET(
_request: Request,
{ params }: { params: Promise<{ invoiceNumber: string }> }
) {
const user = await getSessionUser();
if (!user) {
return new NextResponse("Unauthorized", { status: 401 });
}
const { invoiceNumber } = await params;
const detail = await getInvoiceByNumberForOrg(invoiceNumber, user.orgId);
if (!detail) {
return new NextResponse("Not found", { status: 404 });
}
const pdf = await getInvoicePdf(detail.invoice.id);
if (!pdf) {
return new NextResponse("PDF not available", { status: 404 });
}
const body = Uint8Array.from(pdf.data);
return new NextResponse(body, {
status: 200,
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `inline; filename="${pdf.filename}"`,
"Cache-Control": "private, max-age=0, must-revalidate",
},
});
}

View File

@@ -0,0 +1,27 @@
import { NextResponse } from "next/server";
import { getSessionUser } from "@/lib/session";
import { getInvoiceByNumberForOrg } from "@/lib/db";
/**
* GET /api/billing/invoices/[invoiceNumber]
*
* Customer-scoped detail lookup by invoice number (the human-
* readable YYYY-NNNNN format the customer sees on the PDF). The
* org filter is part of the DB query — a customer probing another
* org's invoice number gets the same 404 as a non-existent one.
*/
export async function GET(
_request: Request,
{ params }: { params: Promise<{ invoiceNumber: string }> }
) {
const user = await getSessionUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { invoiceNumber } = await params;
const detail = await getInvoiceByNumberForOrg(invoiceNumber, user.orgId);
if (!detail) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(detail);
}

View File

@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { getSessionUser } from "@/lib/session";
import { listInvoices, syncOverdueInvoices } from "@/lib/db";
/**
* GET /api/billing/invoices
*
* Customer-scoped list of invoices for the caller's org. Returns
* a flat array of Invoice headers (no line items — those are
* fetched separately by /[invoiceNumber]).
*
* Status filter is implicit: we return every invoice the
* customer's org has, all statuses (issued/paid/overdue/void)
* because the customer wants a single billing-history view.
*
* Before returning we run syncOverdueInvoices() so the displayed
* status reflects the current date — issued invoices past their
* due_at flip to 'overdue'. Cheap, idempotent, and avoids needing
* a separate cron for this transition.
*/
export async function GET() {
const user = await getSessionUser();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Personal accounts have an org too — they share the same shape;
// their invoices show up under that synthetic org id.
try {
await syncOverdueInvoices();
} catch (e) {
// Non-fatal — display stale status rather than 500.
console.warn("syncOverdueInvoices failed in /api/billing/invoices:", e);
}
const invoices = await listInvoices({
zitadelOrgId: user.orgId,
limit: 200,
});
return NextResponse.json(invoices);
}