Files
Wabenhain/src/app/api/erp/rechnungen/[invoiceNumber]/pdf/route.ts
T
Nick SzaunigandClaude Opus 5.5 b246c9ff4e feat(erp): read-only invoice export for ERPNext
ERPNext (collab.vycept.com) now picks up every shop invoice with its PDF,
books it as a Wabenhain sales invoice and matches it to the Stripe payment
via the PaymentIntent, so fees, revenue and the invoice document sit on one
booking.

- GET /api/erp/rechnungen?seit=<ISO>&limit=<1-200>: invoices oldest first,
  with order, payment intent, customer address (no phone, no notes), items
  and the VAT split per rate exactly as printed (lib/vat.ts), including the
  shipping and discount share per rate, so ERPNext books the same cents.
- GET /api/erp/rechnungen/<RE-YYYY-NNNN>/pdf: the stored invoice PDF.
- Bearer token ERP_EXPORT_TOKEN (min. 32 chars, timing-safe compare);
  without it the export is off. Failed attempts are rate limited.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-10 04:39:11 +02:00

28 lines
1.2 KiB
TypeScript

import { NextResponse } from "next/server";
import { erpExportAuthorized, erpInvoicePdf, isInvoiceNumber } from "@/lib/erp-export";
import { rateLimit, getClientIp } from "@/lib/rate-limit";
const FAILED_ATTEMPTS_PER_WINDOW = 20;
const FAILED_WINDOW_MS = 15 * 60 * 1000;
/** Rechnungs-PDF fuer ERPNext (Anhang an der Ausgangsrechnung). Nur mit ERP_EXPORT_TOKEN. */
export async function GET(request: Request, { params }: { params: Promise<{ invoiceNumber: string }> }) {
if (!erpExportAuthorized(request.headers.get("authorization"))) {
const ip = await getClientIp();
const status = rateLimit(`erp-export:${ip}`, FAILED_ATTEMPTS_PER_WINDOW, FAILED_WINDOW_MS) ? 401 : 429;
return NextResponse.json({ error: "Nicht berechtigt." }, { status });
}
const { invoiceNumber } = await params;
const pdf = isInvoiceNumber(invoiceNumber) ? await erpInvoicePdf(invoiceNumber) : null;
if (!pdf) {
return NextResponse.json({ error: "Rechnung nicht gefunden." }, { status: 404 });
}
return new NextResponse(new Uint8Array(pdf), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${invoiceNumber}.pdf"`,
"Cache-Control": "no-store",
},
});
}