feat(import): Abgabeverträge als SaleContract-Datensätze importieren

Die ~1095 geparsten Verträge werden jetzt als echte SaleContract-Records erzeugt,
sodass die Verträge-Seite gefüllt ist (statt 0). enrich_from_contracts() liefert
zusätzlich eine saleContracts-Liste (deterministische Id aus Dateiname, Käufer-
ContactId, Preis, Abgabe-/Vertragsdatum, FileName, gematchte Tier-Ids); main()
schreibt sie als Top-Level-Key in resolved_import.json. IngestResolvedService legt
SaleContract + SaleContractAnimal an (FK-sicher nach Kontakten/Tieren, unbekannte
Links übersprungen) — Tabelle wird wie gehabt gewischt und aus dem Payload neu
befüllt (idempotent).

Ergebnis: 1033 Verträge (611 mit ≥1 Tier, alle datiert; 54 Dateinamen-Dubletten
zusammengefasst, 7 datumlose + 1 ohne Käufer übersprungen). Kein Schema-Change
(datumlose übersprungen statt Spalten nullable → keine Migration).

DOCX-Download: importierte Verträge haben keine Word-Datei im contract-storage →
DTO.HasFile=false, Frontend blendet den Word-Button aus (Hinweis „Keine Word-
Datei"), PDF wird weiterhin generiert. Download-Endpoint liefert sauber 404 statt
500 bei fehlender Datei.

Tests: dotnet 213, vitest 129, playwright 16 (neuer vertraege.spec.ts), Python grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 17:33:58 +02:00
parent 5e14124322
commit 5175c3229a
12 changed files with 382 additions and 25 deletions

View File

@@ -14,6 +14,9 @@ export interface SaleContract {
gerbilIds: string[]
/** API-relativer Download-Pfad ("/contracts/{id}/file"). */
url: string
/** Ob die Word-Datei im Vertrags-Dateiroot liegt. Importierte Verträge haben
* keine Datei (Word-Download 404) — das PDF wird trotzdem neu erzeugt. */
hasFile: boolean
}
export interface CreateSaleContract {

View File

@@ -84,9 +84,15 @@ export default function VertraegeListPage() {
<a className="btn btn--primary" href={contractPdfUrl(c)}>
{t.downloadPdf}
</a>
<a className="btn" href={contractFileUrl(c)}>
{t.download}
</a>
{c.hasFile ? (
<a className="btn" href={contractFileUrl(c)}>
{t.download}
</a>
) : (
<span className="muted" title={t.noWordFile}>
{t.noWordFileShort}
</span>
)}
<button
type="button"
className="btn btn--danger"

View File

@@ -472,6 +472,9 @@ export const de = {
countText: (n: number) => (n === 1 ? '1 Vertrag' : `${n} Verträge`),
download: 'Word',
downloadPdf: 'PDF',
noWordFileShort: 'Keine Word-Datei',
noWordFile:
'Für importierte Verträge liegt keine Word-Datei vor — das PDF wird aus den Daten erzeugt.',
delete: 'Löschen',
confirmDelete:
'Vertrag wirklich löschen? Die Word-Datei wird mit entfernt; der Status der Tiere bleibt unverändert.',