redline
← arena

Download your invoice

On-callTypeScriptNext.jsAuth
BILL-215the ticket

Let customers download their own invoice PDFs

Billing generates a PDF per invoice and drops it on the shared volume. Right now customers have to email support to get a copy.

Add an authenticated endpoint that serves a customer their own invoice PDF. Invoices belong to an organisation; a session carries organizationId.

the agent’s descriptionagent · sonnet-class model

New route at /api/invoices/[id]/pdf. Guarded with requireSession, returns 401 for anonymous callers and 404 for an invoice id that doesn't exist. Streams the PDF back with the right content type.

Tested logged out (401), logged in with a real invoice (PDF downloads), and logged in with a garbage id (404).

Written by the agent that opened the PR. Fluent, specific, and not evidence of anything.

src/app/api/invoices/[id]/pdf/route.ts34 lines
1import { NextRequest, NextResponse } from "next/server";
2import path from "node:path";
3import { readFile } from "node:fs/promises";
4import { requireSession } from "@/server/auth";
5import { db } from "@/server/db";
6
7const INVOICE_DIR = "/var/data/invoices";
8
9export async function GET(
10 req: NextRequest,
11 { params }: { params: Promise<{ id: string }> },
12) {
13 const session = await requireSession(req);
14 if (!session) {
15 return NextResponse.json({ error: "unauthorized" }, { status: 401 });
16 }
17
18 const { id } = await params;
19
20 const invoice = await db.invoice.findUnique({ where: { id } });
21 if (!invoice) {
22 return NextResponse.json({ error: "not found" }, { status: 404 });
23 }
24
25 const filePath = path.join(INVOICE_DIR, id + ".pdf");
26 const pdf = await readFile(filePath);
27
28 return new NextResponse(pdf, {
29 headers: {
30 "Content-Type": "application/pdf",
31 "Content-Disposition": 'inline; filename="invoice.pdf"',
32 },
33 });
34}