0017_rechnung_doku_current.sql
Pfad: migrations/0017_rechnung_doku_current.sql
Ext: sql
Größe: 13521 Bytes
Geändert: 2026-07-16 17:56:36+02
UPDATE dokumentation
SET body = $rechnung$
# Rechnungs-Download und Buchhaltung
**Stand 2026-07-16:** Offline und lokal verifiziert; noch nicht fuer einen unbeaufsichtigten externen Lauf freigegeben. Wetell-Zugangsdaten fehlen im vorgesehenen Pfad, und `api.lexware.io` ist aus dieser Laufzeit aktuell nicht erreichbar. Credentials sind gemaess Scope nicht Gegenstand dieser Umsetzung.
## Orientierung
Das System holt Eingangsrechnungen aus dem Wetell-Kundenportal, prueft und archiviert die Original-PDF, liest die Rechnungsdaten aus und uebergibt den Beleg an Lexware Office. Der lokale Zustand verhindert Parallel- und Wiederholungsfehler. Zusaetzlich sucht Lexware vor jedem Create nach derselben Rechnungsnummer, damit auch ein verlorener lokaler State keinen zweiten wirtschaftlichen Beleg erzeugt.
## 1. Lauf vorbereiten und Anwendung zusammensetzen
`run-rechnung.php` liest nur Konfiguration, erzeugt typisierte Laufzeitobjekte und startet den Use Case. Die Factory verdrahtet Ports und Adapter. Fehlende Pflichtwerte brechen vor Netzwerkmutationen ab.
**Schritte:** Lauf-ID und Pfade bestimmen; Wetell-/Lexware-Konfiguration validieren; Logger, State, Storage, Extraktoren und Remote-Adapter bauen; exklusiven Ingestionslauf starten.
**Symbole:** [run-rechnung.php](/code/105), [InvoiceApplicationFactory::create](/code/167), [WetellRuntimeConfig::fromFile](/code/174), [IngestInvoicesUseCase::execute](/code/109).
## 2. Wetell anmelden und Rechnungen entdecken
Der bevorzugte lokale Node-/Playwright-Dienst startet genau einen Browser fuer seine gesamte Lebensdauer. Jeder Auftrag erhaelt einen isolierten Context. Ein Discover-Auftrag meldet sich im selben Context an und liest danach die Rechnungsseite. Der PHP-Fallback bindet den gesamten Lauf an genau den Adapter, dessen Login erfolgreich war.
Nur HTTPS-URLs des exakt konfigurierten Wetell-Hosts sind erlaubt. Fremde Hosts, Loopback-Ziele, `file:`-URLs und ungepruefte Redirects werden abgewiesen. Der Dienst verlangt immer ein Secret, akzeptiert maximal 64 KiB Request-Body und zwei parallele Jobs.
**Schritte:** aktiven Adapter waehlen; Loginseite laden; Formular mit derselben Identitaet absenden; Login-Erfolg pruefen; Rechnungsseite laden; PDF-Links normalisieren; `SourceInvoice`-Objekte erzeugen.
**Symbole:** [FallbackWetellSourceAdapter](/code/131), [HeadlessWetellSourceAdapter](/code/132), [HttpWetellSourceAdapter](/code/133), [WetellUrlPolicy](/code/175), [browser-service.js](/code/182), [server-core.js](/code/186), [policy.js](/code/184).
## 3. PDF herunterladen und Original sichern
Der Wetell-Port liefert PDF-Bytes, keinen fremden Dateipfad. Der Download-Use-Case verlangt `%PDF-` am Anfang und `%%EOF` am Ende. Adapter und Storage begrenzen die Datei auf 5 MB. Erst danach wird sie atomar unter `rechnungen/originale/Jahr/Monat/` gespeichert und per SHA-256 identifiziert. Eine bestehende Zieldatei wird nur bei identischem Hash wiederverwendet.
**Schritte:** URL erneut erlauben; Bytes laden; HTTP-/PDF-/Groessenvertrag pruefen; Zielpfad bilden; atomar persistieren; SHA-256 bilden.
**Symbole:** [DownloadInvoiceUseCase::execute](/code/107), [DateBasedInvoiceStorage](/code/129), [HeadlessWetellSourceAdapter::downloadInvoicePdf](/code/132), [HttpWetellSourceAdapter::downloadInvoicePdf](/code/133).
## 4. Text und Rechnungsdaten extrahieren
Zuerst liest `pdftotext` den vorhandenen Textlayer. Reicht dessen Konfidenz nicht, rastert der OCR-Fallback PDF-Seiten und uebergibt Bilder an Tesseract. Ein gemeinsamer Parser liest Lieferant, strukturierte Anschrift, echte Rechnungsnummer, Rechnungsdatum sowie Netto, Steuer und Brutto.
Geld wird als exakte Dezimalzeichenfolge verarbeitet, nicht als binaerer Float. Brutto muss positiv sein; Netto plus Steuer muss innerhalb der konfigurierten Toleranz Brutto ergeben. Fehlt ein expliziter Steuerbetrag, wird er exakt aus Brutto minus Netto abgeleitet.
**Symbole:** [ExtractInvoiceDataUseCase::execute](/code/108), [InvoiceTextParser::parse](/code/148), [PdftotextExtractor](/code/137), [TesseractInvoiceExtractor](/code/139), [Amount](/code/114).
## 5. Identitaet, Duplikate und State absichern
Eine Rechnung wird ueber Quelle, URL-unabhaengigen fachlichen Schluessel, echte Rechnungsnummer und PDF-Hash erkannt. Der komplette Read-Modify-Remote-Write-Zyklus laeuft unter einem exklusiven Prozesslock. Ungueltiges JSON fuehrt zum Abbruch statt zu einem leeren Zustand; Schreiben erfolgt ueber eindeutige Tempdatei, `fsync` und atomisches Rename.
Fehlerstatus duerfen spaeter erneut verarbeitet werden. Erfolgreiche Treffer mit gleichem Schluessel oder Hash werden uebersprungen. Ein Test mit geaenderter URL bleibt bei genau einem Posting.
**Symbole:** [SourceInvoice::rechnungsschluessel](/code/126), [IngestInvoicesUseCase](/code/109), [ProcessInvoiceUseCase](/code/164), [InvoiceState](/code/121), [InvoiceRecord](/code/120), [JsonInvoiceStateAdapter](/code/134).
## 6. Lieferantenkontakt abgleichen
Der Lieferant ist nicht fest kodiert. Lexware wird nach dem extrahierten Namen durchsucht. Wiederverwendung ist nur bei Vendor-Rolle, exakt normalisiertem Firmennamen und passender Rechnungsadresse erlaubt. Mehrere identische Treffer sind ein Fehler. Fehlt ein Treffer, wird ein Kontakt mit `version: 0`, `roles.vendor`, `company.name` und strukturierter Rechnungsadresse angelegt.
**Symbole:** [ResolveVendorUseCase::execute](/code/113), [LexofficeApiAdapter::findContactByName](/code/136), [LexofficeApiAdapter::createContact](/code/136).
## 7. Eingangsbeleg in Lexware anlegen
Die Integration verwendet ausschliesslich `https://api.lexware.io`. Vor dem Create fragt sie `/v1/voucherlist` mit Typ `purchaseinvoice` und Rechnungsnummer ab. Ein fachlich passender Treffer wird wiederverwendet; ein Treffer mit abweichendem Kontakt oder Betrag stoppt als Konflikt.
Ein neuer Beleg entsteht ueber `/v1/vouchers`; die PDF wird ueber `/v1/vouchers/{id}/files` angehaengt. Lexware dedupliziert diesen Upload per Checksum. Ohne fachlich belegte Buchungskategorie wird der Voucher bewusst als `unchecked` angelegt; mit konfigurierter Kategorie als `open`. Erfolg verlangt Voucher-ID und File-ID.
**Symbole:** [PostInvoiceUseCase::execute](/code/112), [LexofficeApiAdapter::postInvoice](/code/136), [LexwareInvoicePayloadFactory](/code/170), [CurlLexwareTransport](/code/165).
## 8. Status, Fehler und Audit schreiben
Jeder Schritt traegt eine Lauf-ID. Unerwartete Fehler werden nach der aktiven Phase als Download, Extraktion, Kontakt, Posting oder Persistenz klassifiziert. JSONL-Logs redigieren Passwort-, Secret-, Token-, API-Key-, Cookie- und Authorization-Werte rekursiv. Kann das Auditlog nicht geschrieben, geflusht oder synchronisiert werden, scheitert der Lauf sichtbar.
**Symbole:** [ProcessInvoiceUseCase::classify](/code/164), [JsonLineLogger::logStep](/code/135), [InvoiceRecord::withError](/code/120), [InvoiceState::withRecord](/code/121).
## 9. Recovery und Exactly-once
Nach jedem fachlichen Uebergang wird State gespeichert. Scheitert der State direkt nach einem erfolgreichen Remote-Create, findet der naechste Lauf den Lexware-Voucher ueber die echte Rechnungsnummer und laedt die Datei idempotent erneut. Der Fehlerinjektionstest belegt dabei zwei Posting-Aufrufe, aber nur einen Remote-Voucher.
**Symbole:** [ProcessInvoiceUseCase::execute](/code/164), [LexofficeApiAdapter::postInvoice](/code/136), [JsonInvoiceStateAdapter::withExclusiveLock](/code/134).
## 10. Nachweise und Freigabe
Ein fachlich erfolgreicher Real-Lauf muss gemeinsam belegen: Original-PDF samt SHA-256, korrekten lokalen State, wiederverwendeten oder neu angelegten Kontakt, Voucher-ID, File-ID und einen zweiten idempotenten Lauf ohne neue Remote-Objekte.
Aktuelle lokale Gates: PHPUnit 54 Tests/124 Assertions, PHPStan Level 8 ohne Fehler, keine Produktionsdatei ueber 150 nichtkommentierte LOC, Node 9 Tests, Composer/npm-Locks, systemd-Unit sowie frische/no-op Datenbankmigrationen. Die externe Freigabe bleibt offen, bis Wetell-Credentials im bestehenden externen Pfad verfuegbar und Wetell-/Lexware-E2E erfolgreich sind.
**Symbole:** [run-rechnung.php](/code/105), [IngestInvoicesUseCase::execute](/code/109), [ProcessInvoiceUseCase::execute](/code/164), [DateBasedInvoiceStorage](/code/129), [LexofficeApiAdapter](/code/136), [server.js](/code/142).
$rechnung$,
updated_at = clock_timestamp()
WHERE slug = 'rechnung-prozess';
UPDATE dokumentation
SET body = $notes$
# Release Notes: Rechnungsprozess
Zeitlich absteigend; maximal fuenf fachliche Versionsspruenge.
- **2026-07-16, supervisierter Ist-Stand:** Doku auf typisierte Runtime, persistenten Playwright-Browser, Context pro Job, Byte-Download, exakte Geldwerte, strukturierte Kontaktpruefung, `purchaseinvoice`, Remote-Deduplizierung, Prozesslock und Recovery-Nachweise umgestellt.
- **2026-07-16, detaillierte Teilprozesse:** Wetell-Konfiguration, Adapterwahl, Login, Discovery, `SourceInvoice`, Abbruchbedingungen und Fehlerzustaende in Einzelschritte zerlegt.
- **2026-07-16, Code- und Symbolverknuepfung:** Hauptprozesse mit Application-, Domain- und Infrastructure-Symbolen in `/code` verbunden.
- **2026-07-16, didaktische End-to-End-Fassung:** Zehn Hauptprozesse fuer interessierte Laien beschrieben.
- **2026-07-16, Initialfassung:** Kurzer Ueberblick zu Playwright, Download, PDF-Pruefung, Kontakt, Lexware und lokalem State.
$notes$,
updated_at = clock_timestamp()
WHERE slug = 'release-notes';
WITH beschreibung(slug, text) AS (
VALUES
('rechnung-starten', 'Typisierte Konfiguration validieren, Adapter ueber die Factory verdrahten und den exklusiven Ingestionslauf starten.'),
('rechnung-wetell-entdecken', 'Mit genau einem aktiven Adapter und derselben Identitaet anmelden, die Wetell-Uebersicht lesen und erlaubte SourceInvoice-Objekte erzeugen.'),
('rechnung-download', 'PDF-Bytes ueber eine erlaubte HTTPS-URL laden, Magic/EOF/Groesse pruefen, atomar als Original speichern und hashen.'),
('rechnung-pdf-extraktion', 'Textlayer oder gerastertes OCR lesen und Lieferant, strukturierte Adresse, echte Nummer, Datum sowie exakte Betraege validieren.'),
('rechnung-duplikat-pruefen', 'Unter exklusivem Lock Quelle, URL-unabhaengigen Schluessel, PDF-Hash und erfolgreichen lokalen Status pruefen.'),
('rechnung-kontakt-abgleich', 'Lexware-Vendor nur bei passendem Namen und Adresse wiederverwenden, sonst strukturiert und eindeutig anlegen.'),
('rechnung-lexoffice-buchen', 'Purchaseinvoice vor Create remote abgleichen, Voucher anlegen oder wiederverwenden und PDF checksum-idempotent anhaengen.'),
('rechnung-status-protokoll', 'Zustandsuebergaenge atomar speichern und korrelierte, redigierte Auditlogs fail-closed schreiben.'),
('rechnung-fehler-wiederholung', 'Fehler phasengetreu klassifizieren und nach State-/Remote-Ausfall ohne zweiten wirtschaftlichen Beleg wiederaufnehmen.'),
('rechnung-nachweise', 'Original und Hash, lokalen State, Kontakt-, Voucher- und File-ID sowie den idempotenten Zweitlauf gemeinsam belegen.')
)
UPDATE prozesse AS p SET beschreibung = b.text, updated_at = clock_timestamp()
FROM beschreibung AS b WHERE p.slug = b.slug;
WITH schritte(id, name, text) AS (
VALUES
(72, 'Isolierten Playwright-Context herstellen', 'Bestehenden Dienstbrowser verwenden und fuer den Auftrag genau einen isolierten Context oeffnen.'),
(73, 'Headless-Aufruf absichern', 'Loopback, Pflichtsecret, Body-/Kapazitaetslimit, Aktion und redigierte Serviceantwort pruefen.'),
(81, 'PDF-Bytes ueber Wetell anfordern', 'Erlaubte Download-URL ueber den aktiven Wetell-Adapter laden; keinen Host-Dateipfad akzeptieren.'),
(82, 'PDF-Vertrag validieren', 'PDF-Magic, EOF, Maximalgroesse und Nicht-HTML vor der Ablage pruefen.'),
(85, 'Pflichtfelder und exakte Betraege validieren', 'Positive Bruttosumme und Netto-plus-Steuer ohne binaere Float-Arithmetik pruefen.'),
(89, 'Verarbeitungsentscheidung unter Lock treffen', 'Nur erfolgreiche Quelle, kanonischen Schluessel oder Hash ueberspringen; Fehler bleiben wiederholbar.'),
(90, 'Quelle, Schluessel und Hash vergleichen', 'URL-unabhaengige Identitaet und PDF-SHA-256 gegen den vollstaendigen State vergleichen.'),
(92, 'Lexware-Kontakt exakt suchen', 'Vendor-Rolle, normalisierten Firmennamen und Rechnungsadresse gemeinsam abgleichen.'),
(94, 'Original-PDF idempotent anhaengen', 'Voucher-Datei ueber `/v1/vouchers/{id}/files` hochladen und File-ID pruefen.'),
(95, 'Purchaseinvoice abgleichen oder erstellen', 'Voucherlist nach echter Nummer pruefen; Konflikte stoppen; passenden Voucher wiederverwenden.'),
(99, 'Status atomar und exklusiv speichern', 'Korruptes JSON abweisen und State ueber Tempdatei, fsync und Rename unter Prozesslock persistieren.'),
(103, 'Fehler nach aktiver Phase klassifizieren', 'Download, Extraktion, Kontakt, Posting und Persistenz ursachentreu unterscheiden.')
)
UPDATE prozess_schritt AS s SET name = x.name, beschreibung = x.text, updated_at = clock_timestamp()
FROM schritte AS x WHERE s.id = x.id;
UPDATE dokumentation
SET body = replace(body, '**Status:** OFFEN', '**Status:** IN UMSETZUNG')
|| E'\n\n## Supervisionsfortschritt 2026-07-16\n- `/doku/rechnung-prozess`, Release Notes, zehn Hauptprozesse und zentrale Teilschritte wurden auf den belegten Code- und Gateway-Stand aktualisiert. Externe Wetell-/Lexware-Fachabnahme bleibt offen.\n',
tags = array_append(array_remove(tags, 'offen'), 'in-umsetzung')
WHERE slug = 'task-dokumentations-kongruenz-und-fachabnahme';