Merge lane/import in feature/craftvia-mvp

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:22:07 +02:00
co-authored by Claude Opus 5
38 changed files with 4204 additions and 4 deletions
+106
View File
@@ -0,0 +1,106 @@
# Lane L3 – Auftragsimport
Branch `lane/import` (von `feature/craftvia-mvp` @ `bf44567`). Spec §9 komplett, §7.3, §31, US-002, US-003, ARCHITEKTUR §4.4/§4.5.
## Umfang / erfüllte Spec-Punkte
| Spec | Umsetzung |
|---|---|
| §9.2 Dateien | PDF (Text und Scan: Claude liest PDF nativ als `document`-Block, inkl. Seitenbild → kein separates OCR), JPG, PNG. Allowlist + Magic Bytes + Größenlimit (PDF 25 MB, Bild 15 MB) |
| §9.3 1–2 Upload, Typprüfung | `services/imports/upload.ts#createImport` → Dokument (Kategorie `order_confirmation`, Sichtbarkeit `backoffice_only`, SHA-256) → `ImportJob uploaded` → `dispatchJob("import-extraction")` |
| §9.3 3–5 Texterkennung, Analyse, Extraktion | `ai/extraction/anthropic.ts` (Anthropic SDK 0.115, Modell `ANTHROPIC_MODEL`, Default `claude-opus-5`): Streaming + `finalMessage()`, adaptive thinking, Structured Output (`output_config.format` JSON-Schema aus `lib/imports/extraction.ts`), Volltext `text` + 22 Felder mit `value/confidence/source`. Prompt: nichts erfinden, unsichere Felder < 0.8, deutsche Datums-/Zahlenformate normalisieren, Kunde ≠ Briefkopf, Objekt ≠ Kundenadresse. Refusal/`max_tokens` → Fehler; bei Opus 5 serverseitiger Fallback (`fallbacks: "default"`, Beta `server-side-fallback-2026-07-01`) |
| §9.3 6 Plausibilität | `lib/imports/plausibility.ts`: Datum gültig und plausibel (Auftrags-/Dokumentdatum ≤ +1 Monat, Ausführung −2…+3 Jahre), PLZ 5-stellig (DE), E-Mail-/Telefonformat, Ende ≥ Beginn. Verstoß: Konfidenz ≤ 0.4 + Hinweis |
| §9.3 7 / §7.3 / US-003 Dubletten | Kandidaten über Kundennummer, Firmenname (Rechtsform-normalisiert), Personenname, E-Mail, Telefon, Adresse (Straße/Str./Strasse) mit Score + Gründen; Objekt-Kandidaten = Objekte der Kandidaten an gleicher Adresse. Keine automatische Zuordnung/Zusammenführung |
| §9.5 Vertrauenswerte | je Feld gespeichert (`ImportJob.extraction.fields`), Prüfmaske markiert < 0.8 mit Warnfarbe + Icon + Text „unsicher · Sicherheit n %“, Quelle als Tooltip und Hinweistext |
| §9.6 Manuelle Prüfung | `/imports/[id]`: links Original (PDF-iframe / Bild), rechts Formular Kunde · Objekt · Ansprechpartner · Auftrag · Positionen/Material; Kundenentscheidung (Kandidat mit Score/Gründen · Suche · neu anlegen · Link „Dubletten im Kundenstamm zusammenführen“ → L1), Objekt analog (keins/bestehend/neu), Positionen editierbar mit „als Materialvorgabe übernehmen“. **Kein Auftrag ohne Bestätigung** |
| §9.3 10 Bestätigung | `services/imports/confirm.ts#confirmImport`: eine Transaktion – atomarer Statuswechsel `review_required → confirmed` (verhindert Doppelaufträge), Kunde/Kontakt/Objekt anlegen oder zuordnen, `createWorkOrder` (Status-Historie `review_required → planned`, `sourceImportId`, Materialvorgabe), Originaldokument an Auftrag/Kunde/Objekt verknüpft, `corrections` (Diff Extraktion ↔ bestätigt + Entscheidungen). Audit: `import_job` (import), `work_order`/`customer`/`site`/`contact` (create) |
| §9.7 Originaldokument | bleibt dauerhaft (auch bei Verwerfen); gespeichert: Importdatum, importierender Nutzer, erkannter Text, Extraktion, Extraktionsversion, Modell, Provider, Korrekturen, `AiGeneration` |
| §31 Provider-Abstraktion | `DocumentExtractionProvider` (ARCHITEKTUR §4.5), `getExtractionProvider()` → `null` ohne Key bzw. bei `AI_EXTRACTION_PROVIDER≠anthropic`; `FakeExtractionProvider` für Tests |
| Graceful degradation | ohne Provider: `review_required` mit leerer Extraktion + Hinweis „manuell erfassen“ |
| Fehler | Provider-/Dateifehler → `failed` + `errorMessage` + Audit + Event `import.failed`; „Neu verarbeiten“ (`failed → uploaded` + Dispatch). Erfolg → Event `import.ready_for_review` |
## Routen / Screens
| Route | Inhalt |
|---|---|
| `/imports` | Upload (Drag & Drop + Dateiauswahl, Fortschrittsbalken per XHR), Liste mit Status (Pill = Farbe + Icon + Text), Aktionen Prüfen / Neu verarbeiten / Auftrag, Auto-Refresh während Verarbeitung |
| `/imports/[id]` | Prüfmaske bzw. Statusansicht (Verarbeitung, fehlgeschlagen + Neu verarbeiten/Verwerfen, bestätigt + Link zum Auftrag, verworfen), erkannter Text aufklappbar |
| `/imports/[id]/file` | Original für die Vorschau (inline, nur eigener Origin als Frame; `?download=1`) – Session + `import:write` (DB-autoritativ) + Modul + Dokument-Sichtbarkeit |
| `POST /api/v1/work-orders/import` | multipart `file` → `201 { id, status }` |
| `GET /api/v1/imports/[id]` | Status, Extraktion inkl. Konfidenzen, Kandidaten |
| `POST /api/v1/imports/[id]/confirm` | JSON = Prüfformular (`lib/imports/review.ts#reviewFormSchema`) → `{ workOrderId, workOrderNumber, customerId, siteId, contactId }` |
Fehler-Mapping API: `not_found` 404, `forbidden` 403, `invalid` 400 (mit Feldpfaden), `conflict` 409, ohne Session 401.
## Dateien
- `src/lib/imports/` – `extraction.ts` (Felder, Zod, JSON-Schema, gespeicherte Form), `plausibility.ts`, `review.ts` (Formularschema, Mapping, Feld-Konfidenzen, Korrektur-Diff), `status.ts`
- `src/server/ai/extraction/` – `anthropic.ts` (Provider + `getExtractionProvider`), `fake.ts`
- `src/server/services/imports/` – `upload.ts`, `process.ts`, `confirm.ts` (inkl. Verwerfen/Neu verarbeiten), `queries.ts`, Stubs `document-store-stub.ts`, `duplicates-stub.ts`, `work-orders-stub.ts`
- `src/server/jobs/processors/import-extraction.ts`
- `src/server/actions/imports/imports.ts` (confirm, discard, retry, Kundensuche – je `moduleGuard("imports")` + `await guard(...)`)
- `src/app/(app)/imports/page.tsx`, `src/app/(app)/imports/[id]/page.tsx`, `src/app/(app)/imports/[id]/file/route.ts`
- `src/app/api/v1/imports/_context.ts`, `src/app/api/v1/imports/[id]/route.ts`, `src/app/api/v1/imports/[id]/confirm/route.ts`, `src/app/api/v1/work-orders/import/route.ts`
- `src/components/imports/` – `uploader.tsx`, `review-form.tsx`, `status-pill.tsx`, `job-actions.tsx`, `auto-refresh.tsx`
- `messages/de/imports.json`, `messages/en/imports.json`
- `scripts/make-sample-pdfs.ts` → `docs/craftvia/samples/01-musterbau-auftragsbestaetigung.pdf`, `02-elbblick-wartungsauftrag.pdf`, `03-privatkunde-reparatur.pdf` (fiktiv; Nr. 3 enthält absichtlich PLZ „2148“, E-Mail „(at)“ und Ende vor Beginn für die Plausibilitätshinweise)
- Tests: `scripts/test-import-rules.ts`, `scripts/test-import-flow.ts`, `scripts/test-import-live.ts`
Fremd-Einzeiler: `src/server/jobs/processors/index.ts` (Processor-Registrierung). `src/lib/nav.ts` enthielt `/imports` bereits.
## Tests
| Skript | Prüfungen | Inhalt |
|---|---|---|
| `test-import-rules.ts` | 55 | Plausibilitätsregeln, Parsing der Provider-Ausgabe, JSON-Schema (strict-tauglich), Mapping Extraktion → Formular, Formularvalidierung, Korrektur-Diff, Normalisierung Dubletten, Magic Bytes/Dateiname, PDF-Generator |
| `test-import-flow.ts` | 75 | Upload/Validierung, Verarbeitung mit Fake-Provider, Dubletten-/Objektkandidaten, AiGeneration, kein Auftrag ohne Bestätigung, Idempotenz; **Rollen** (Monteur/Teamleiter → `forbidden`); **Mandantentrennung** (B kann A weder lesen, laden, bestätigen, verwerfen, neu verarbeiten noch A-Kunden zuordnen; Kandidaten/Suche nur im Mandanten); Bestätigung bestehender vs. neuer Kunde inkl. Dokumentverknüpfung, Korrekturen, Audit; Doppelbestätigung → conflict; vergebene Kundennummer → conflict ohne Teilanlage; Verwerfen; Provider-Fehler/Datei fehlt/Dispatch-Fehler → failed + Neu verarbeiten; ohne Provider → manuell |
| `test-import-live.ts` | 6 | nur mit `ANTHROPIC_API_KEY` (sonst übersprungen): echte Extraktion einer generierten Auftragsbestätigung |
## Gate
`npm run gate` grün (Lane-DB `craftvia_import`, `RLS_DATABASE_URL` auf dieselbe DB): prisma generate, `tsc` ohne Fehler, Lint 0 Fehler (2 Warnungen im Fundament-Platzhalter `services/notifications/handle-event.ts`), Build inkl. Modul-Guard-Check (14 Action-Dateien), **25/25 Testskripte grün**. `test-import-live.ts` ohne `ANTHROPIC_API_KEY` übersprungen (Exit 0) – die Live-Extraktion gegen Claude ist damit **nicht** verifiziert.
## Smoke (Dev-Server :3103, curl mit Seed-Logins)
| Prüfung | Ergebnis |
|---|---|
| ohne Session: `/imports`, `/imports/[id]`, `/imports/[id]/file`, `/api/v1/**` | 307 → `/login` |
| Backoffice `GET /imports` | 200, Upload-Bereich gerendert |
| Backoffice `POST /api/v1/work-orders/import` (Beispiel-PDF 01) | 201, ohne API-Key direkt `review_required` mit Hinweis „manuell erfassen“ |
| Upload `package.json` | 400 |
| `GET /api/v1/imports/[id]` / unbekannte ID | 200 / 404 |
| `GET /imports/[id]` | 200, Prüfmaske + Originaldokument gerendert |
| `GET /imports/[id]/file` | 200, `inline`, **aber `X-Frame-Options: DENY`** (siehe Bedarf Punkt 2) |
| `POST /api/v1/imports/[id]/confirm` mit `{}` | 400 mit Feldpfaden |
| Monteur: API / Datei / Upload | 403 / 404 / 403 |
Keine visuelle Browser-Prüfung (Login-Maske würde Passworteingabe durch den Agenten erfordern) – Layout 1024/768/375 px noch manuell prüfen. Der Smoke hinterlässt einen Import im Mandanten `demo` der Lane-DB.
## Stubs / Abhängigkeiten zu anderen Lanes
| Stub (L3-Pfad) | Vertrag | Ablösen |
|---|---|---|
| `services/imports/document-store-stub.ts#storeFile` | ARCHITEKTUR §4.3 `services/documents/store.ts#storeFile` (auf dem Basis-Commit nicht vorhanden; keiner Lane zugeordnet) | Import in `upload.ts` umstellen; `readDocumentBytes` durch Service-Funktion ersetzen |
| `services/imports/duplicates-stub.ts#findDuplicateCustomers` | L1 `lib/customers/duplicates.ts#findDuplicateCustomers(db, candidate) → { customerId, score, reasons[] }[]` | Import in `process.ts` umstellen (`findSiteCandidates` bleibt L3) |
| `services/imports/work-orders-stub.ts#createWorkOrder` + `CreateWorkOrderInput` | L2 `createWorkOrder(ctx, input)`; Stub schreibt Status-Historie direkt statt über `transitionWorkOrder` | Import in `confirm.ts` umstellen; L2-Service muss einen Transaktions-Client in `ctx.db` akzeptieren und `sourceImportId`, Status `planned` (Historie ab `review_required`) und Materialvorgabe unterstützen |
| `app/api/v1/imports/_context.ts#importsApiContext` | gemeinsames `requireApiContext` (in `services/context.ts` erwähnt, nicht vorhanden) | durch zentrale Funktion ersetzen |
Links in andere Lanes: `/work-orders/[id]` (L2), `/customers/[id]` für das Zusammenführen (L1, Merge mit Bestätigung).
Hinweis Pfad: `src/app/api/v1/work-orders/import/route.ts` liegt im L2-Ordner `api/v1/work-orders` (vom Auftrag so gefordert) – neue Datei, kein Konflikt mit L2-Dateien zu erwarten.
## Bedarf an Fundament / Architektur (nicht geändert)
1. **Upload > 10 MB:** `src/proxy.ts` puffert Request-Bodies standardmäßig nur bis 10 MB (`proxyClientMaxBodySize`, danach wird der Body still abgeschnitten). Für die geforderten 25 MB in `next.config.ts` `experimental.proxyClientMaxBodySize: "26mb"` setzen. Server Actions werden für Uploads bewusst nicht genutzt (1-MB-Limit).
2. **PDF-Vorschau im iframe:** globale Header in `next.config.ts` setzen `X-Frame-Options: DENY` und `frame-ancestors 'none'` für alle Pfade. Die Datei-Route setzt `SAMEORIGIN`/`frame-ancestors 'self'`, **im Smoke verifiziert: der globale Header gewinnt (`X-Frame-Options: DENY`) → die iframe-Vorschau bleibt im Browser leer**; „In neuem Tab öffnen“ funktioniert. `object`/`embed` unterliegen derselben Regel, daher kein Umweg in L3. Fix im Fundament: in `next.config.ts` für `/imports/:id/file` `X-Frame-Options: SAMEORIGIN` und `frame-ancestors 'self'` statt `DENY`/`'none'` ausliefern. Außerdem braucht die Vorschau echte Bytes → `S3_*` muss gesetzt sein (ohne S3 speichert der Storage-Stub keine Bytes; Verarbeitung mit Provider endet dann mit `file_unavailable`).
3. **RLS_ENFORCED=true:** `dbForTenant` öffnet je Operation eine eigene Transaktion; innerhalb von `ctx.db.$transaction` (Bestätigung) ist das nicht atomar. Betrifft alle Lanes mit Transaktionen – zentral in `db.ts` lösen.
4. `documents/store.ts` (§4.3) ist keiner Lane zugeordnet – Zuständigkeit klären.
5. **Eigene Lane-Datenbank und RLS-Test:** `scripts/test-rls-enforcement.ts` legt Fixtures über `DATABASE_URL` an, verbindet `craftvia_app` aber ohne `RLS_DATABASE_URL` fest auf die DB `craftvia`. Mit `DATABASE_URL=…/craftvia_import` schlägt der Test deshalb fehl (0 Zeilen, FK-Verletzung) – kein Codefehler. Gate daher mit `RLS_DATABASE_URL=postgresql://craftvia_app:craftvia_app_local@localhost:5432/craftvia_import?schema=public` ausgeführt; der Test ist damit grün. Vorschlag Fundament: Default-URL aus `DATABASE_URL` ableiten.
## Bekannte Lücken
- Keine Migration nötig: Hinweise und Objektkandidaten liegen in `ImportJob.extraction` (`{ fields, hints, siteCandidates }`), Dublettenkandidaten in `duplicate_candidates`.
- Malware-Scan nur Magic-Byte-/Typprüfung (ClamAV-Hook gehört zum Dokumentservice §4.3).
- Kostenlimit/Region/Aufbewahrung je Mandant (§31) nicht umgesetzt; Konfiguration nur per Env.
- Kundensuche in der Prüfmaske: Top 10 nach Name/Nummer/Ort/E-Mail; kein Paging.
- Keine Teamzuweisung/Auftragsart in der Prüfmaske (erfolgt danach im Auftrag, L2).
- Worker: mit `REDIS_URL` läuft die Extraktion in `npm run worker:craftvia`; ohne laufenden Worker bleibt ein Import auf „Hochgeladen“.
@@ -0,0 +1,61 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 2221 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Musterbau GmbH) Tj ET
BT /F1 10 Tf 56 722 Td (z. Hd. Frau Jana K\366hler) Tj ET
BT /F1 10 Tf 56 707 Td (Hafenstra\337e 12) Tj ET
BT /F1 10 Tf 56 692 Td (20457 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftragsbest\344tigung) Tj ET
BT /F1 10 Tf 56 636 Td (Auftragsnummer: AB-2026-0815 Datum: 02.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Ihr Angebot: ANG-2026-0342 Kundennummer: K-10042) Tj ET
BT /F1 10 Tf 56 606 Td (Bauvorhaben: Neubau B\374rogeb\344ude Speicherhof, Am Kaiserkai 30, 20457 Hamburg) Tj ET
BT /F1 10 Tf 56 591 Td (Ansprechpartnerin vor Ort: Jana K\366hler, Tel. 040 0000 2233, j.koehler@musterbau.example.org) Tj ET
BT /F2 10 Tf 56 576 Td (Ausf\374hrungszeitraum: 12.10.2026 bis 16.10.2026) Tj ET
BT /F1 10 Tf 56 552 Td (Sehr geehrte Frau K\366hler,) Tj ET
BT /F1 10 Tf 56 537 Td (vielen Dank f\374r Ihren Auftrag. Wir best\344tigen die Ausf\374hrung folgender Leistungen:) Tj ET
BT /F2 10 Tf 56 513 Td (Pos. Bezeichnung Art.-Nr. Menge Einheit) Tj ET
BT /F1 10 Tf 56 498 Td (1 Montage W\344rmepumpe Luft/Wasser 12 kW 1 Stk) Tj ET
BT /F1 10 Tf 56 483 Td (2 W\344rmepumpe Aerotherm 12 kW WP-AT-12 1 Stk) Tj ET
BT /F1 10 Tf 56 468 Td (3 Pufferspeicher 500 l PS-500 1 Stk) Tj ET
BT /F1 10 Tf 56 453 Td (4 Kupferrohr 22 mm CU-22 24 m) Tj ET
BT /F1 10 Tf 56 438 Td (5 Inbetriebnahme und Einweisung 4 Std) Tj ET
BT /F2 10 Tf 56 414 Td (Gesamtbetrag netto: 18.450,00 \200 zzgl. 19 % MwSt.: 3.505,50 \200 Gesamt: 21.955,50 \200) Tj ET
BT /F1 10 Tf 56 390 Td (Hinweise: Zufahrt \374ber Tor 2, Anmeldung beim Bauleiter. Kran ist bauseits zu stellen.) Tj ET
BT /F1 10 Tf 56 375 Td (Referenz: Ihre Bestellung BE-7781 vom 28.08.2026) Tj ET
BT /F1 10 Tf 56 345 Td (Mit freundlichen Gr\374\337en) Tj ET
BT /F1 10 Tf 56 330 Td (Kranich Haustechnik GmbH) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000002593 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
2729
%%EOF
@@ -0,0 +1,54 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 1425 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Elbblick Wohnen eG) Tj ET
BT /F1 10 Tf 56 722 Td (Hausverwaltung) Tj ET
BT /F1 10 Tf 56 707 Td (Neum\374hlen 7) Tj ET
BT /F1 10 Tf 56 692 Td (22763 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftrag Wartung Heizungsanlagen) Tj ET
BT /F1 10 Tf 56 636 Td (Auftrag Nr. W-26-117 \267 Kunden-Nr. K-20017 \267 Hamburg, den 01.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Objekt: Wohnanlage Elbhang, \326velg\366nne 45, 22605 Hamburg) Tj ET
BT /F1 10 Tf 56 606 Td (Ansprechpartner: Herr Timo Brandt, Hausmeister, Tel. +49 40 0000 4545) Tj ET
BT /F1 10 Tf 56 586 Td (Termin: KW 41 nach Absprache mit dem Hausmeister) Tj ET
BT /F2 10 Tf 56 564 Td (Leistungsumfang:) Tj ET
BT /F1 10 Tf 56 549 Td (J\344hrliche Wartung von 2 Gas-Brennwertkesseln inkl. Abgasmessung und Protokoll.) Tj ET
BT /F1 10 Tf 56 534 Td (1 Wartung Gas-Brennwertkessel 2 Stk) Tj ET
BT /F1 10 Tf 56 519 Td (2 Wartungsset Dichtungen 2 Satz) Tj ET
BT /F1 10 Tf 56 504 Td (3 Abgasmessung mit Protokoll 2 Stk) Tj ET
BT /F1 10 Tf 56 482 Td (Besondere Hinweise: Heizungsraum im Keller, Schl\374ssel beim Hausmeister.) Tj ET
BT /F1 10 Tf 56 467 Td (R\374ckfragen bitte an verwaltung@elbblick.example.org) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000001797 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
1933
%%EOF
@@ -0,0 +1,51 @@
%PDF-1.4
%âãÏÓ
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [6 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>
endobj
4 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>
endobj
5 0 obj
<< /Length 1061 >>
stream
BT /F2 16 Tf 56 790 Td (Kranich Haustechnik GmbH) Tj ET
BT /F1 8 Tf 56 777 Td (Deichweg 4 \267 21029 Hamburg \267 Tel. 040 0000 1000 \267 info@kranich-haustechnik.example.org) Tj ET
BT /F1 10 Tf 56 737 Td (Frau) Tj ET
BT /F1 10 Tf 56 722 Td (Petra Sommer) Tj ET
BT /F1 10 Tf 56 707 Td (Lindenallee 9a) Tj ET
BT /F1 10 Tf 56 692 Td (2148 Hamburg) Tj ET
BT /F2 14 Tf 56 656 Td (Auftragsbest\344tigung Reparatur) Tj ET
BT /F1 10 Tf 56 636 Td (Auftrag: AB-2026-0901 Datum: 03.09.2026) Tj ET
BT /F1 10 Tf 56 621 Td (Einsatzort: wie oben) Tj ET
BT /F1 10 Tf 56 606 Td (Telefon: 0170 0000 987 E-Mail: petra.sommer\(at\)example.org) Tj ET
BT /F1 10 Tf 56 591 Td (Geplanter Termin: 18.09.2026, Ende 17.09.2026) Tj ET
BT /F1 10 Tf 56 569 Td (Leistung: Austausch defekter Thermostatkopf im Bad, Pr\374fung Heizk\366rperventile.) Tj ET
BT /F1 10 Tf 56 554 Td (1 Thermostatkopf Standard TK-100 1 Stk) Tj ET
BT /F1 10 Tf 56 539 Td (2 Arbeitszeit Monteur 1,5 Std) Tj ET
BT /F2 10 Tf 56 517 Td (Gesamt brutto: 189,40 \200) Tj ET
endstream
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> /Contents 5 0 R >>
endobj
xref
0 7
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000218 00000 n
0000000320 00000 n
0000001433 00000 n
trailer
<< /Size 7 /Root 1 0 R >>
startxref
1569
%%EOF