# Schema-Wünsche aus Etappe 3 „Papier“ Gesammelt beim Bau der Dokumentenakte. `prisma/schema.prisma` wurde absprachegemäß **nicht** angefasst; alles hier ist umgangen worden und funktioniert, wäre aber sauberer, wenn es im Modell stünde. Reihenfolge: was am meisten weh tut, steht oben. --- ## 1. `DokumentVerknuepfung`: genau eine Zielspalte — nur in der Anwendung erzwungen **Stand:** Zwölf nullbare Fremdschlüsselspalten. Der Kommentar im Schema sagt „Die Anwendung stellt sicher, dass genau eine Ziel-Spalte gesetzt ist“ — genau so ist es umgesetzt (`ZIEL_SPALTE` in `src/lib/validierung/dokumente.ts`, Prüfung in `dokumentVerknuepfen`). **Wunsch:** Ein CHECK-Constraint in der Datenbank, damit auch ein Skript, ein Seed oder ein späterer Import das nicht verletzen kann: ```sql ALTER TABLE dokument_verknuepfung ADD CONSTRAINT genau_ein_ziel CHECK ( (("vorgangId" IS NOT NULL)::int + ("fristId" IS NOT NULL)::int + ("buchungId" IS NOT NULL)::int + ("vermoegenspostenId" IS NOT NULL)::int + ("immobilieId" IS NOT NULL)::int + ("vertragId" IS NOT NULL)::int + ("kontaktprotokollId" IS NOT NULL)::int + ("gesundheitsereignisId" IS NOT NULL)::int + ("genehmigungId" IS NOT NULL)::int + ("nachforderungId" IS NOT NULL)::int + ("bescheidId" IS NOT NULL)::int + ("kontaktId" IS NOT NULL)::int) = 1 ); ``` Ließe sich als eigenes SQL-Skript neben `volltextsuche.sql` legen, ohne das Prisma-Schema zu ändern. **Warum wichtig:** Eine Akte, in der eine Verknüpfung auf zwei Dinge gleichzeitig zeigt, ist gegenüber dem Gericht nicht mehr erklärbar. **Zusätzlich:** Es fehlt eine Eindeutigkeit je Paar. Doppelte Verknüpfungen werden derzeit vor dem Anlegen abgefragt (`findFirst`) — ein Race zwischen zwei Klicks kann sie trotzdem erzeugen. Ein partieller Unique-Index je Zielspalte, z. B. `CREATE UNIQUE INDEX ... ON dokument_verknuepfung ("dokumentId", "vorgangId") WHERE "vorgangId" IS NOT NULL;`, würde das schließen. --- ## 2. `Dokument.ocrStatus` ist ein freier String **Stand:** `ocrStatus String?` mit dem Kommentar `"ausstehend" | "laeuft" | "fertig" | "fehlgeschlagen" | "nicht_verfuegbar"`. **Wunsch:** Ein Enum `OcrStatus` mit genau diesen fünf Werten. Der Vertrag in `docs/SCHNITTSTELLEN.md` Abschnitt 3 gibt sie ohnehin vor, und die Oberfläche (`OcrAbzeichen`) schaltet darauf. Ein Tippfehler beim Zurückschreiben fällt heute erst auf, wenn das Abzeichen leer bleibt. --- ## 3. Kein Index auf `Dokument.objektSchluessel` **Stand:** Die Ausliefer-Route `/api/dokumente/datei/[signatur]` sucht das Dokument über `objektSchluessel`, um Anzeigename und Protokolleintrag zu bekommen. Ohne Index ist das ein Seq-Scan bei jedem Dateiabruf. **Wunsch:** `@@unique([objektSchluessel])` (der Schlüssel ist eine UUID, also ohnehin eindeutig) oder mindestens `@@index([objektSchluessel])`. --- ## 4. Keine Dublettenerkennung über die Prüfsumme **Stand:** `pruefsumme String?` (SHA-256, wird beim Ablegen gefüllt). Wer denselben Bescheid zweimal fotografiert, bekommt zwei Datensätze. **Wunsch:** `@@index([betreuteId, pruefsumme])`. Dann ließe sich beim Upload sagen „dieses Blatt liegt schon in der Akte, angelegt am 12.03.2026“ — ohne den zweiten Upload zu verbieten (manchmal ist die zweite Kopie gewollt). --- ## 5. `Nachforderung` und `Bescheid` tragen keine `betreuteId` **Stand:** Beide hängen nur am `SozialhilfeAntrag`. Die Zugriffsprüfung beim Verknüpfen muss deshalb über die Beziehung gehen (`where: { id, antrag: { betreuteId } }`, siehe `zielGehoertZurAkte`). **Wunsch:** Eine denormalisierte `betreuteId` wie bei allen anderen Fachentitäten. Die Konvention „jede Abfrage ist auf `betreuteId` eingegrenzt“ ist sonst an genau diesen zwei Stellen nicht wörtlich einhaltbar — und Ausnahmen von einer Sicherheitsregel sind die Stellen, an denen später Fehler passieren. --- ## 6. Seitenzahl und OCR-Lauf ohne Zeitstempel **Stand:** `seitenzahl Int?` wird von der Texterkennung nachgetragen. Wann ein OCR-Lauf zuletzt versucht wurde, steht nirgends. **Wunsch:** `ocrVersuchtAm DateTime?`. `ocrNachziehen()` in `src/lib/dokumente/ocr-nachlauf.ts` holt heute alles nach, was auf `ausstehend`/`fehlgeschlagen` steht — mit einem Zeitstempel ließe sich ein dauerhaft kaputtes Dokument nach dem dritten Versuch in Ruhe lassen, statt es bei jedem Nachziehen erneut durch Tesseract zu schicken. --- ## 7. Anmerkung zur Verschlüsselung der abgelegten Dateien Kein Schema-Wunsch, aber eine Abweichung vom Kommentar im Schema, die festgehalten gehört. Bei `Dokument.ocrText` steht: > „die Datei selbst liegt verschlüsselt im Objektspeicher“ Umgesetzt ist das **nur beim Dateitreiber**: `src/lib/speicher/datei.ts` verschlüsselt jede Datei anwendungsseitig mit AES-256-GCM (eigener Binär- Umschlag `BCD1 | Schlüsselversion | IV | Tag | Geheimtext`, Schlüssel aus `ENC_KEY_V*`). Beim S3-Treiber geht das nicht: Der Vertrag verlangt für `signierteUrl()` eine presigned URL, die der Objektspeicher selbst ausliefert — ein anwendungsseitig verschlüsselter Blob käme dort als Datenmüll an. Dort läuft der Schutz deshalb über `ServerSideEncryption: AES256` plus den AV-Vertrag nach Art. 28 DSGVO mit dem Anbieter. Die SHA-256-Prüfsumme wird in **beiden** Fällen über den Klartext gebildet — sonst wäre sie als Nachweis gegenüber dem Gericht wertlos. Wer das vereinheitlichen will, müsste den Vertrag ändern: `signierteUrl()` würde auch beim S3-Treiber auf die eigene Route zeigen, die den Blob holt, entschlüsselt und ausliefert. Das kostet Durchsatz bei großen Scans, wäre aber konsequenter. Bewusst offengelassen, weil es Abschnitt 1 der Schnittstellen berührt. --- # Anhang: kein Schema-Wunsch, aber dringend ## `@react-pdf/renderer` kann ohne Krücke keinen Buchstaben setzen Beim Bau der Belegmappe aufgefallen und **betrifft jedes PDF der Anwendung**, also auch die Berichte aus Etappe 5. **Befund:** Jeder Aufruf von `renderToBuffer` scheitert mit ``` TypeError: Cannot read properties of undefined (reading 'unitsPerEm') at @react-pdf/textkit/lib/textkit.js:3090 ``` — auch bei einem Dokument ganz ohne Stilangaben. Nicht die Testumgebung ist schuld: in reinem Node reproduziert es genauso. **Ursache:** In `package.json` ist nur `@react-pdf/renderer` auf `4.1.6` festgelegt. Seine Unterabhängigkeiten stehen als Caret-Bereiche darin und sind inzwischen weitergewandert: | Paket | installiert | von renderer 4.1.6 erwartet | |---|---|---| | `@react-pdf/font` | 3.1.0 | `^3.0.1` | | `@react-pdf/layout` | 4.7.1 | `^4.2.0` | | `@react-pdf/textkit` | 6.4.1 | (kommt über `layout`) | `@react-pdf/font@3.1.0` gibt für die Standardschriften `null` zurück — früher löste die Schicht darunter den bloßen Namen selbst auf. `@react-pdf/textkit@6.4.1` verlangt dagegen ein echtes Schriftobjekt mit `layout()` und wirft bei einem Namen ausdrücklich `Invalid font`. Zwischen beiden klafft eine Lücke. **Was ich getan habe:** `src/lib/pdf/standardschrift.ts` baut das fehlende Schriftobjekt aus den Metriken, die `@react-pdf/pdfkit` ohnehin mitbringt, und hängt sich vor die Schriftauflösung — aber nur, wenn sie kaputt ist. Kein neues Paket, kein Nachladen aus dem Netz. `pdfErzeugen()` ruft das von selbst auf, alle PDF-Wege dieser Anwendung sind damit wieder benutzbar (Test: „Aus einem Bericht und einem Beleg wird eine Mappe" in `tests/dokumente.test.ts`). **Was noch zu tun ist** (außerhalb meiner Pfade, deshalb hier): `package.json` um Festlegungen ergänzen, damit die Pakete zueinander passen — ```jsonc "overrides": { "@react-pdf/layout": "4.2.x", "@react-pdf/textkit": "5.x" } ``` Die genauen Stände bitte gegen `@react-pdf/renderer@4.1.6` prüfen; alternativ `@react-pdf/renderer` auf einen aktuellen Stand heben, dessen Unterabhängigkeiten wieder zusammenpassen. Danach kann `standardschrift.ts` ersatzlos entfallen — `standardschriftenSicherstellen()` merkt von selbst, dass es nicht mehr gebraucht wird, und hält sich heraus.