/**
* Server-side PDF rendering. Every renderer here goes through
* `renderToBuffer()` from `@react-pdf/renderer`.
*
* ## Why not `pdf().toString()`
*
* Every renderer used to funnel through
*
* const instance = pdf();
* const pdfString = await instance.toString();
* return Buffer.from(pdfString, "binary");
*
* on the belief that `toBuffer()` was unusable under jsdom (Vitest's
* environment). That belief was half right and led somewhere much
* worse than a test-only problem.
*
* Three things are going on, and only the first was ever real:
*
* 1. `pdf(el).toBuffer()` is NOT the buffer API. It resolves to the
* PDFKit *stream* (react-pdf carries a TODO to rename it
* `toStream`), and under jsdom it resolves to a bare
* `PDFDocument` handle. That is the jsdom breakage the old
* comment described — but it applies to that method, not to the
* module-level `renderToBuffer()` helper, which collects the
* stream into a real Node Buffer and works fine under jsdom.
*
* 2. `toString()` renders with `compress: false`. react-pdf's own
* source says why: "when rendering to string if compress=true
* the document is blank". So every PDF this app produced was
* uncompressed — larger than necessary — and the method is
* flagged upstream as buggy (react-pdf issues #2112, #2095).
*
* 3. The round-trip through a JS string corrupts binary streams.
* A raster image (PNG/JPEG) arrives as a Flate-compressed binary
* blob that does not survive being stringified and re-encoded as
* latin1. The image object ends up embedded in the file but is
* never painted. Text is ASCII in the content stream, so text
* renders fine and the corruption is invisible until something
* raster goes in — which is why this sat latent for so long. The
* letterhead logo hid it completely: that logo is an SVG, and
* SVG is text, so it round-tripped perfectly.
*
* `renderToBuffer()` fixes all three at once and needs no jsdom
* fallback. `render-image-pipeline.test.ts` is the regression guard —
* it renders a real raster image and asserts the bytes come back
* intact, so this cannot silently come back.
*/
import { renderToBuffer } from "@react-pdf/renderer";
import { buildQrMatrix, type QrMatrix } from "./qr";
import { QuoteDocument, type QuoteDocumentProps } from "./quote-document";
import {
PurchaseOrderDocument,
type PurchaseOrderDocumentProps,
} from "./purchase-order-document";
import {
StockPurchaseDocument,
type StockPurchaseDocumentProps,
} from "./stock-purchase-document";
import {
DeliveryNoteDocument,
type DeliveryNoteDocumentProps,
} from "./delivery-note-document";
import {
StockPurchaseListDocument,
type StockPurchaseListDocumentProps,
} from "./stock-purchase-list-document";
import {
StockItemStocktakeDocument,
type StockItemStocktakeDocumentProps,
} from "./stock-item-stocktake-document";
import {
RepairRequestDocument,
type RepairRequestDocumentProps,
} from "./repair-request-document";
import {
RepPerformanceDetailDocument,
RepPerformanceTeamDocument,
type RepPerformanceDetailDocumentProps,
type RepPerformanceTeamDocumentProps,
} from "./rep-performance-detail-document";
/**
* Server-side render of a `` to a PDF Buffer.
*
* Runs entirely in-process via `renderToBuffer()` — no headless
* Chromium, no Playwright/Puppeteer, no browser binary in the Docker
* image. See the note at the top of this file for why that specific
* API and not `pdf().toString()`.
*
* Always returns a fresh Buffer; never cached. Reads of letterhead,
* quote, items, and customer happen in the API route before this is
* called, so any edit in PB propagates on the next generation.
*/
export async function renderQuotePdf(props: QuoteDocumentProps): Promise {
return renderToBuffer();
}
/**
* Server-side render of a `` to a PDF Buffer.
*
* Same in-process Node pipeline as `renderQuotePdf` — no headless
* Chromium, no browser binary. The PO PDF document component is a
* pure function of its props, so the API route resolves the PO +
* items + supplier + order + customer + letterhead before calling
* this, then uploads the resulting Buffer to
* `purchase_orders.documents` (the embedded file, multi field on
* the PO record itself — replaced 2026-07-06 the separate
* `purchase_order_pdfs` collection).
*
* Same `renderToBuffer()` pipeline as every other renderer here.
*/
export async function renderPurchaseOrderPdf(
props: PurchaseOrderDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of a `` to a PDF Buffer.
*
* Stock POs are pure stock-replenishment orders with no customer in
* the loop — the document layout is a slimmed-down version of the
* customer-PO PDF (no Order Ref line, no "For: "). Same
* `renderToBuffer()` pipeline as the rest of this file.
*
* The route drops the resulting Buffer into
* `stock_purchases.documents` (file, multi) + appends a metadata
* entry to `stock_purchases.documents_history` (json) — same pattern
* as the customer-PO PDF route. Mirroring the storage shape lets
* the existing `/api/v1/files/...` proxy serve stock PDFs the same
* way it serves customer POs.
*/
export async function renderStockPurchasePdf(
props: StockPurchaseDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of a `` to a PDF Buffer.
*
* Same in-process Node pipeline as the quote + PO renderers. The
* `delivery-note` API route calls this right after it stamps the
* status flip on the items, then uploads the resulting Buffer to
* each item's `order_item_attachements` field — a multi-file field
* (`maxSelect: 10`) already on the `order_items` collection. The
* PDF is named `DN{YY}{MM}{DD}{NN}.pdf` (e.g. `DN26071401.pdf`) so
* the retrieval side can disambiguate it from other attachments on
* the same item.
*/
export async function renderDeliveryNotePdf(props: DeliveryNoteDocumentProps): Promise {
// Every delivery note carries a QR of its own number so the
// warehouse/driver scanner app can resolve the note straight off
// the printed page. Built here rather than in the document so the
// QR encoder stays out of the presentational component.
//
// Non-fatal: a QR that fails to encode must never cost the operator
// their delivery note. The document renders without it.
let qr: QrMatrix | null = null;
try {
qr = buildQrMatrix(props.header.deliveryNoteNumber);
} catch (err) {
console.error("[renderDeliveryNotePdf] QR build failed:", err);
}
return renderToBuffer();
}
/**
* Server-side render of a `` to a PDF Buffer.
*
* Same in-process Node pipeline as the other renderers. The
* `repair-request` API route calls this right after it resolves the
* repair + supplier + letterhead, then uploads the resulting Buffer
* to the repair's `repair_request` file field (multi, maxSelect 10).
*
* The PDF is named `RR-{repairNumber}.pdf` (e.g. `RR-REP-2026-08-003.pdf`)
* so the operator can find it on the repair detail page by name
* (PB's file URLs include the encoded name).
*/
export async function renderRepairRequestPdf(
props: RepairRequestDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of the stock-orders *list* PDF — every
* purchase in one document, not one PDF per PO. Sibling of
* `renderStockPurchasePdf` (which is the single-PO, supplier-facing
* PDF). Both use the same `renderToBuffer()` pipeline, which needs no
* browser binary.
*/
export async function renderStockPurchaseListPdf(
props: StockPurchaseListDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of the rep-performance drill-down PDF. Same
* in-process pipeline as the other list-style reports.
*/
export async function renderRepPerformanceDetailPdf(
props: RepPerformanceDetailDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of the team-wide rep-performance PDF (all reps,
* ranked). Same pipeline as the per-rep variant.
*/
export async function renderRepPerformanceTeamPdf(
props: RepPerformanceTeamDocumentProps
): Promise {
return renderToBuffer();
}
/**
* Server-side render of the stocktake PDF — every active stock
* item in one document. Same in-process Node pipeline as the other
* renderers. The blank "Actual" column is intentional — the operator
* fills it in on the printed page during the walk-through.
*/
export async function renderStockItemStocktakePdf(
props: StockItemStocktakeDocumentProps
): Promise {
return renderToBuffer();
}