Files
craftvia/scripts/make-sample-pdfs.ts
T
msolarczekandClaude Opus 5 12a764786a L3 Auftragsimport: Extraktion, Plausibilität, Dubletten, Bestätigung (Services + Tests)
- Claude-Extraktion (PDF nativ/Bild, Structured Output, Konfidenzen, Volltext), FakeProvider
- Plausibilitätsprüfung, Mapping Extraktion → Formular, Korrektur-Diff
- Services Upload/Verarbeitung/Bestätigung/Verwerfen/Neu verarbeiten, Processor import-extraction
- Stubs: storeFile (§4.3), findDuplicateCustomers (L1), createWorkOrder (L2)
- Tests test-import-rules/-flow/-live, Beispiel-PDFs + Generator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 12:17:13 +02:00

156 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Generates fictitious sample order confirmations for the import lane (L3) as text PDFs —
* handwritten minimal PDF writer (Helvetica, WinAnsiEncoding), no dependencies.
*
* Lauf: npx tsx scripts/make-sample-pdfs.ts → docs/craftvia/samples/*.pdf
* All companies, people, addresses and numbers are invented (example.org, 0000-numbers).
*/
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "docs", "craftvia", "samples");
type Line = { text: string; size?: number; bold?: boolean; x?: number; gap?: number };
/** Latin-1/WinAnsi string literal for a PDF content stream. */
function pdfString(s: string): string {
let out = "";
for (const ch of s) {
if (ch === "€") out += "\\200";
else if (ch === "–") out += "\\226";
else if (ch === "(" || ch === ")" || ch === "\\") out += `\\${ch}`;
else {
const code = ch.charCodeAt(0);
out += code < 128 ? ch : `\\${code.toString(8).padStart(3, "0")}`;
}
}
return `(${out})`;
}
export function buildPdf(pages: Line[][]): Buffer {
const objects: string[] = [];
const add = (body: string) => {
objects.push(body);
return objects.length;
};
const catalogId = add("");
const pagesId = add("");
const fontId = add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>");
const boldId = add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>");
const pageIds: number[] = [];
for (const lines of pages) {
let y = 800;
const ops: string[] = [];
for (const l of lines) {
const size = l.size ?? 10;
y -= l.gap ?? size + 5;
ops.push(`BT /${l.bold ? "F2" : "F1"} ${size} Tf ${l.x ?? 56} ${y} Td ${pdfString(l.text)} Tj ET`);
}
const stream = ops.join("\n");
const contentId = add(`<< /Length ${Buffer.byteLength(stream, "latin1")} >>\nstream\n${stream}\nendstream`);
pageIds.push(
add(`<< /Type /Page /Parent ${pagesId} 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 ${fontId} 0 R /F2 ${boldId} 0 R >> >> /Contents ${contentId} 0 R >>`),
);
}
objects[catalogId - 1] = `<< /Type /Catalog /Pages ${pagesId} 0 R >>`;
objects[pagesId - 1] = `<< /Type /Pages /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] /Count ${pageIds.length} >>`;
let pdf = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n";
const offsets: number[] = [];
objects.forEach((body, i) => {
offsets.push(Buffer.byteLength(pdf, "latin1"));
pdf += `${i + 1} 0 obj\n${body}\nendobj\n`;
});
const xref = Buffer.byteLength(pdf, "latin1");
pdf += `xref\n0 ${objects.length + 1}\n0000000000 65535 f \n`;
for (const o of offsets) pdf += `${String(o).padStart(10, "0")} 00000 n \n`;
pdf += `trailer\n<< /Size ${objects.length + 1} /Root ${catalogId} 0 R >>\nstartxref\n${xref}\n%%EOF\n`;
return Buffer.from(pdf, "latin1");
}
const letterhead = (): Line[] => [
{ text: "Kranich Haustechnik GmbH", size: 16, bold: true, gap: 10 },
{ text: "Deichweg 4 · 21029 Hamburg · Tel. 040 0000 1000 · info@kranich-haustechnik.example.org", size: 8 },
];
const SAMPLES: Record<string, Line[][]> = {
"01-musterbau-auftragsbestaetigung.pdf": [
[
...letterhead(),
{ text: "Musterbau GmbH", gap: 40 },
{ text: "z. Hd. Frau Jana Köhler" },
{ text: "Hafenstraße 12" },
{ text: "20457 Hamburg" },
{ text: "Auftragsbestätigung", size: 14, bold: true, gap: 36 },
{ text: "Auftragsnummer: AB-2026-0815 Datum: 02.09.2026", gap: 20 },
{ text: "Ihr Angebot: ANG-2026-0342 Kundennummer: K-10042" },
{ text: "Bauvorhaben: Neubau Bürogebäude Speicherhof, Am Kaiserkai 30, 20457 Hamburg" },
{ text: "Ansprechpartnerin vor Ort: Jana Köhler, Tel. 040 0000 2233, j.koehler@musterbau.example.org" },
{ text: "Ausführungszeitraum: 12.10.2026 bis 16.10.2026", bold: true },
{ text: "Sehr geehrte Frau Köhler,", gap: 24 },
{ text: "vielen Dank für Ihren Auftrag. Wir bestätigen die Ausführung folgender Leistungen:" },
{ text: "Pos. Bezeichnung Art.-Nr. Menge Einheit", bold: true, gap: 24 },
{ text: "1 Montage Wärmepumpe Luft/Wasser 12 kW 1 Stk" },
{ text: "2 Wärmepumpe Aerotherm 12 kW WP-AT-12 1 Stk" },
{ text: "3 Pufferspeicher 500 l PS-500 1 Stk" },
{ text: "4 Kupferrohr 22 mm CU-22 24 m" },
{ text: "5 Inbetriebnahme und Einweisung 4 Std" },
{ text: "Gesamtbetrag netto: 18.450,00 € zzgl. 19 % MwSt.: 3.505,50 € Gesamt: 21.955,50 €", bold: true, gap: 24 },
{ text: "Hinweise: Zufahrt über Tor 2, Anmeldung beim Bauleiter. Kran ist bauseits zu stellen.", gap: 24 },
{ text: "Referenz: Ihre Bestellung BE-7781 vom 28.08.2026" },
{ text: "Mit freundlichen Grüßen", gap: 30 },
{ text: "Kranich Haustechnik GmbH" },
],
],
"02-elbblick-wartungsauftrag.pdf": [
[
...letterhead(),
{ text: "Elbblick Wohnen eG", gap: 40 },
{ text: "Hausverwaltung" },
{ text: "Neumühlen 7" },
{ text: "22763 Hamburg" },
{ text: "Auftrag Wartung Heizungsanlagen", size: 14, bold: true, gap: 36 },
{ text: "Auftrag Nr. W-26-117 · Kunden-Nr. K-20017 · Hamburg, den 01.09.2026", gap: 20 },
{ text: "Objekt: Wohnanlage Elbhang, Övelgönne 45, 22605 Hamburg" },
{ text: "Ansprechpartner: Herr Timo Brandt, Hausmeister, Tel. +49 40 0000 4545" },
{ text: "Termin: KW 41 nach Absprache mit dem Hausmeister", gap: 20 },
{ text: "Leistungsumfang:", bold: true, gap: 22 },
{ text: "Jährliche Wartung von 2 Gas-Brennwertkesseln inkl. Abgasmessung und Protokoll." },
{ text: "1 Wartung Gas-Brennwertkessel 2 Stk" },
{ text: "2 Wartungsset Dichtungen 2 Satz" },
{ text: "3 Abgasmessung mit Protokoll 2 Stk" },
{ text: "Besondere Hinweise: Heizungsraum im Keller, Schlüssel beim Hausmeister.", gap: 22 },
{ text: "Rückfragen bitte an verwaltung@elbblick.example.org" },
],
],
"03-privatkunde-reparatur.pdf": [
[
...letterhead(),
{ text: "Frau", gap: 40 },
{ text: "Petra Sommer" },
{ text: "Lindenallee 9a" },
{ text: "2148 Hamburg" },
{ text: "Auftragsbestätigung Reparatur", size: 14, bold: true, gap: 36 },
{ text: "Auftrag: AB-2026-0901 Datum: 03.09.2026", gap: 20 },
{ text: "Einsatzort: wie oben" },
{ text: "Telefon: 0170 0000 987 E-Mail: petra.sommer(at)example.org" },
{ text: "Geplanter Termin: 18.09.2026, Ende 17.09.2026" },
{ text: "Leistung: Austausch defekter Thermostatkopf im Bad, Prüfung Heizkörperventile.", gap: 22 },
{ text: "1 Thermostatkopf Standard TK-100 1 Stk" },
{ text: "2 Arbeitszeit Monteur 1,5 Std" },
{ text: "Gesamt brutto: 189,40 €", bold: true, gap: 22 },
],
],
};
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
mkdirSync(OUT_DIR, { recursive: true });
for (const [name, pages] of Object.entries(SAMPLES)) {
const file = join(OUT_DIR, name);
writeFileSync(file, buildPdf(pages));
console.log(`✔ ${file}`);
}
}