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:
@@ -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)
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
33
gerbil-manager-web/e2e/vertraege.spec.ts
Normal file
33
gerbil-manager-web/e2e/vertraege.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.',
|
||||
|
||||
Reference in New Issue
Block a user