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:
@@ -345,6 +345,75 @@ check("history: discard lines carry the warning marker",
|
||||
all(s.startswith(m.DISCARD_MARK) for s in h_disc if "verworfen" in s))
|
||||
|
||||
|
||||
# ── enrich_from_contracts: SaleContract record emission ───────────────────────
|
||||
# A contract whose buyer resolves to a contact and whose animal call-name matches
|
||||
# a breeder-owned gerbil must yield a SaleContract record carrying the buyer
|
||||
# ContactId, a deterministic Id, the parsed dates and the matched gerbil id.
|
||||
def _balu():
|
||||
# A breeder-owned ("Chaoten") gerbil whose call-name is "Balu".
|
||||
return {
|
||||
"Id": "11111111-1111-1111-1111-111111111111",
|
||||
"Name": "Balu von den kleinen Chaoten",
|
||||
"Gender": "male", "DateOfBirth": "2022-05-01",
|
||||
"OriginBreeder": "Zucht der kleinen Chaoten",
|
||||
"Status": "Active", "ColorVarietyId": None,
|
||||
"Provenance": None,
|
||||
}
|
||||
|
||||
_g = _balu()
|
||||
_resolved = [_g]
|
||||
_contacts_by_norm = {}
|
||||
_contracts = [{
|
||||
"sourceFile": "Zucht der kleinen Chaoten _ Schwarz (Balu) - Max Muster_.docx",
|
||||
"buyer": "Max Muster", "animals": ["Balu"], "color": "schwarz",
|
||||
"gender": "Male", "dob": "2022-05-01",
|
||||
"handoverDate": "2022-07-01", "contractDate": "2022-07-01", "price": "30,00",
|
||||
}]
|
||||
_stats, _sale = m.enrich_from_contracts(_contracts, _resolved, _contacts_by_norm, {})
|
||||
check("contracts: exactly one SaleContract record emitted", len(_sale) == 1)
|
||||
_rec = _sale[0] if _sale else {}
|
||||
check("contracts: record Id is deterministic from filename",
|
||||
_rec.get("Id") == m.generate_guid(
|
||||
"contract-Zucht der kleinen Chaoten _ Schwarz (Balu) - Max Muster_.docx"))
|
||||
check("contracts: record ContactId is the resolved buyer contact",
|
||||
_rec.get("ContactId") and
|
||||
_rec["ContactId"] == _contacts_by_norm.get(m.normalize_name("Max Muster"), {}).get("Id"))
|
||||
check("contracts: record lists the matched gerbil",
|
||||
_rec.get("Animals") == [_g["Id"]])
|
||||
check("contracts: price parsed as float", _rec.get("Price") == 30.0)
|
||||
check("contracts: dates carried through",
|
||||
_rec.get("HandoverDate") == "2022-07-01" and _rec.get("ContractDate") == "2022-07-01")
|
||||
check("contracts: stats count the created record", _stats.get("records_created") == 1)
|
||||
|
||||
# A dateless contract is skipped from record creation (non-nullable DateOnly) but
|
||||
# still counted, and the buyer contact is still created.
|
||||
_c2 = [{
|
||||
"sourceFile": "Zucht der kleinen Chaoten _ (Nala) - Erika Muster_.docx",
|
||||
"buyer": "Erika Muster", "animals": ["Nala"], "color": "",
|
||||
"gender": "", "dob": "", "handoverDate": "", "contractDate": "", "price": "",
|
||||
}]
|
||||
_stats2, _sale2 = m.enrich_from_contracts(_c2, [], {}, {})
|
||||
check("contracts: dateless contract skipped from records", len(_sale2) == 0)
|
||||
check("contracts: dateless contract counted", _stats2.get("dateless_skipped") == 1)
|
||||
|
||||
# Price-only / no-date fallback: contract with only a contractDate gets it copied
|
||||
# into HandoverDate too (and vice versa), and an animal-less contract still
|
||||
# becomes a record (better to show it than drop it).
|
||||
_c3 = [{
|
||||
"sourceFile": "Zucht der kleinen Chaoten _ (Unbekannt) - Tom Muster_.docx",
|
||||
"buyer": "Tom Muster", "animals": ["Unbekannt"], "color": "",
|
||||
"gender": "", "dob": "", "handoverDate": "", "contractDate": "2023-01-15",
|
||||
"price": "",
|
||||
}]
|
||||
_stats3, _sale3 = m.enrich_from_contracts(_c3, [], {}, {})
|
||||
check("contracts: animal-less contract still becomes a record", len(_sale3) == 1)
|
||||
check("contracts: missing handover falls back to contract date",
|
||||
_sale3 and _sale3[0]["HandoverDate"] == "2023-01-15"
|
||||
and _sale3[0]["ContractDate"] == "2023-01-15")
|
||||
check("contracts: animal-less record has empty Animals list",
|
||||
_sale3 and _sale3[0]["Animals"] == [])
|
||||
|
||||
|
||||
if check.failed:
|
||||
print(f"\n{check.failed} test(s) FAILED")
|
||||
sys.exit(1)
|
||||
|
||||
Reference in New Issue
Block a user