Phase3: Billing Customerpage/Mailings
Some checks failed
Build and Push / build (push) Failing after 46s
Some checks failed
Build and Push / build (push) Failing after 46s
This commit is contained in:
43
src/app/api/billing/invoices/[invoiceNumber]/pdf/route.ts
Normal file
43
src/app/api/billing/invoices/[invoiceNumber]/pdf/route.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
}
|
||||
27
src/app/api/billing/invoices/[invoiceNumber]/route.ts
Normal file
27
src/app/api/billing/invoices/[invoiceNumber]/route.ts
Normal 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);
|
||||
}
|
||||
39
src/app/api/billing/invoices/route.ts
Normal file
39
src/app/api/billing/invoices/route.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user