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

@@ -363,6 +363,7 @@ export async function installMockApi(page: Page): Promise<MockDb> {
fileName: `vertrag-${id}.docx`,
createdAt: new Date().toISOString(),
url: `/contracts/${id}/file`,
hasFile: true,
}
;(db.contracts as unknown as Row[]).push(contract as unknown as Row)
// Tiere auf GivenAway setzen (wie das echte Backend)

View File

@@ -55,6 +55,8 @@ export interface MockContract {
createdAt: string
gerbilIds: string[]
url: string
/** Ob die Word-Datei vorliegt. Importierte Verträge: false (kein Word-Button). */
hasFile: boolean
}
export interface MockDb {
@@ -372,7 +374,21 @@ export function seedDb(): MockDb {
requests,
aiConfigured: true,
mailConfigured: true,
contracts: [],
// Ein importierter Vertrag (hasFile:false) prüft die Word-Button-Ausblendung.
contracts: [
{
id: 'contract-import-1',
contactId: 'con-meier',
price: 30,
handoverDate: '2023-09-29',
contractDate: '2023-09-29',
fileName: 'Zucht der kleinen Chaoten _ (Krümel) - Zoohandlung Meier_.docx',
createdAt: '2023-09-29T00:00:00Z',
gerbilIds: ['kruemel'],
url: '/contracts/contract-import-1/file',
hasFile: false,
},
],
saleAdConfigured: true,
namesConfigured: true,
feedback: [],

View File

@@ -0,0 +1,33 @@
/**
* FEAT-Contracts: Verträge-Liste (/vertraege) zeigt importierte Abgabeverträge.
*
* Der Mock-Seed enthält EINEN importierten Vertrag (hasFile:false). Wir prüfen,
* dass er gerendert wird (Abnehmer, Preis, Datum, Tieranzahl) und dass der
* Word-Download-Button für ihn ausgeblendet ist (kein .docx), während der
* PDF-Button (aus den Daten neu erzeugt) sichtbar bleibt.
*/
import { de, expect, gotoSection, skipUnlessMock, test } from './fixtures'
const t = de.pages.vertraege
test.describe('Verträge-Liste', () => {
test('importierter Vertrag wird gelistet; Word-Button ausgeblendet, PDF bleibt', async ({
page,
}) => {
skipUnlessMock()
await gotoSection(page, de.nav.contracts)
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
const card = page.locator('.vertrag-card').filter({ hasText: 'Zoohandlung Meier' })
await expect(card).toBeVisible()
// Tieranzahl + Preis + Übergabedatum erscheinen in der Meta-Zeile.
await expect(card).toContainText(t.animalsCount(1))
await expect(card).toContainText('30,00 €')
await expect(card).toContainText('29.09.2023')
// PDF-Button vorhanden, Word-Button NICHT (importiert, keine Datei).
await expect(card.getByRole('link', { name: t.downloadPdf })).toBeVisible()
await expect(card.getByRole('link', { name: t.download, exact: true })).toHaveCount(0)
await expect(card.getByText(t.noWordFileShort)).toBeVisible()
})
})