import PDFDocument from "pdfkit"; import { formatPrice, formatDate } from "@/lib/format"; import { getCompanyInfo } from "@/lib/company-info"; import { getDictionary } from "@/i18n/get-dictionary"; import type { Locale } from "@/i18n/config"; interface InvoicePdfInput { locale: Locale; invoiceNumber: string; issuedAt: Date; totalCents: number; order: { orderNumber: string; firstName: string; lastName: string; street: string; zip: string; city: string; currency: string; subtotalCents: number; shippingCents: number; discountCents: number; discountCode?: { code: string } | null; items: { productName: string; weightGrams: number; quantity: number; unitPriceCents: number; totalCents: number }[]; }; } /** * Erzeugt die Rechnung als PDF (pdfkit — reine JS-Bibliothek ohne * Headless-Browser/native Abhaengigkeiten, haelt das Docker-Image schlank). * Wird on-the-fly beim Download erzeugt statt dauerhaft gespeichert. Nutzt * dieselben Uebersetzungen wie die HTML-Rechnungsansicht (t.invoicePage), * damit beide Darstellungen derselben Rechnung konsistent bleiben. */ export async function generateInvoicePdf(input: InvoicePdfInput): Promise { const t = getDictionary(input.locale).invoicePage; const company = await getCompanyInfo(); const doc = new PDFDocument({ margin: 50, size: "A4" }); const chunks: Buffer[] = []; doc.on("data", (chunk: Buffer) => chunks.push(chunk)); const done = new Promise((resolve, reject) => { doc.on("end", () => resolve(Buffer.concat(chunks))); doc.on("error", reject); }); doc.fontSize(20).fillColor("#24352B").text(company.name.toUpperCase(), { continued: false }); doc .fontSize(9) .fillColor("#666666") .text(`${company.address.street}, ${company.address.zip} ${company.address.city}`); doc.moveDown(1.5); doc.fontSize(14).fillColor("#24352B").text(t.invoiceLabel(input.invoiceNumber)); doc .fontSize(9) .fillColor("#666666") .text(t.orderLabel(input.order.orderNumber)) .text(`${t.dateLabel} ${formatDate(input.issuedAt)}`); doc.moveDown(1); doc .fontSize(10) .fillColor("#242424") .text(`${input.order.firstName} ${input.order.lastName}`) .text(input.order.street) .text(`${input.order.zip} ${input.order.city}`); doc.moveDown(1.5); const tableTop = doc.y; const columns = { name: 50, qty: 320, price: 380, total: 470 }; doc .fontSize(9) .fillColor("#666666") .text(t.itemHeader, columns.name, tableTop) .text(t.quantityHeader, columns.qty, tableTop) .text(t.priceHeader, columns.price, tableTop) .text(t.totalHeader, columns.total, tableTop); doc .moveTo(50, tableTop + 14) .lineTo(545, tableTop + 14) .strokeColor("#dddddd") .stroke(); let y = tableTop + 22; doc.fontSize(10).fillColor("#242424"); for (const item of input.order.items) { doc.text(`${item.productName} (${item.weightGrams} g)`, columns.name, y, { width: 260 }); doc.text(String(item.quantity), columns.qty, y); doc.text(formatPrice(item.unitPriceCents, input.order.currency), columns.price, y); doc.text(formatPrice(item.totalCents, input.order.currency), columns.total, y); y += 20; } y += 10; doc.moveTo(350, y).lineTo(545, y).strokeColor("#dddddd").stroke(); y += 10; doc.fontSize(10).fillColor("#666666").text(t.subtotalLabel, 350, y); doc.fillColor("#242424").text(formatPrice(input.order.subtotalCents, input.order.currency), columns.total, y); y += 16; doc.fillColor("#666666").text(t.shippingLabel, 350, y); doc .fillColor("#242424") .text( input.order.shippingCents === 0 ? t.freeShipping : formatPrice(input.order.shippingCents, input.order.currency), columns.total, y ); if (input.order.discountCents > 0) { y += 16; doc .fillColor("#666666") .text(input.order.discountCode ? `${t.discountLabel} (${input.order.discountCode.code})` : t.discountLabel, 350, y); doc .fillColor("#242424") .text(`−${formatPrice(input.order.discountCents, input.order.currency)}`, columns.total, y); } y += 20; doc.fontSize(11).fillColor("#24352B").text(t.totalLabel, 350, y); doc.text(formatPrice(input.totalCents, input.order.currency), columns.total, y); doc .fontSize(8) .fillColor("#999999") .text(t.legalNote, 50, 760, { width: 495 }); doc.end(); return done; }