- 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>
156 lines
7.4 KiB
TypeScript
156 lines
7.4 KiB
TypeScript
/**
|
||
* 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}`);
|
||
}
|
||
}
|