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:
@@ -238,7 +238,53 @@ namespace GerbilManagerWebAPI.Import
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return $"Ingestion successful! Imported {contactsAdded} contacts ({contactsUpdated} updated), {data.Litters.Count} litters, {data.Gerbils.Count} gerbils, and {data.GerbilPhotos.Count} photos.";
|
||||
// 7. Insert imported Abgabeverträge (SaleContract + join rows).
|
||||
// The table was wiped above, so these are recreated on every ingest
|
||||
// (they survive only by being part of the payload). FK-safe: contacts
|
||||
// and gerbils are already inserted. We skip rows whose buyer contact
|
||||
// is unknown and skip individual animal links to unknown gerbils
|
||||
// (defensive — the resolver should never emit those).
|
||||
// No .docx is copied: the source files live on the network share and
|
||||
// are not staged into Contracts:RootPath, so the Word download 404s
|
||||
// gracefully (the endpoint already returns NotFound) while the PDF is
|
||||
// regenerated from the data. The DTO exposes HasFile=false for these
|
||||
// so the frontend hides the Word button.
|
||||
int contractsAdded = 0;
|
||||
int contractAnimalsSkipped = 0;
|
||||
if (data.SaleContracts.Count > 0)
|
||||
{
|
||||
var contactIds = await _db.Contacts.Select(c => c.Id).ToHashSetAsync();
|
||||
var gerbilIds = await _db.Gerbils.Select(g => g.Id).ToHashSetAsync();
|
||||
var seenContractIds = new HashSet<Guid>();
|
||||
foreach (var sc in data.SaleContracts)
|
||||
{
|
||||
if (!seenContractIds.Add(sc.Id)) continue; // dedupe within payload
|
||||
if (!contactIds.Contains(sc.ContactId)) continue; // unknown buyer
|
||||
|
||||
var animals = new List<SaleContractAnimal>();
|
||||
foreach (var gid in (sc.Animals ?? new List<Guid>()).Distinct())
|
||||
{
|
||||
if (!gerbilIds.Contains(gid)) { contractAnimalsSkipped++; continue; }
|
||||
animals.Add(new SaleContractAnimal { SaleContractId = sc.Id, GerbilId = gid });
|
||||
}
|
||||
|
||||
_db.SaleContracts.Add(new SaleContract
|
||||
{
|
||||
Id = sc.Id,
|
||||
ContactId = sc.ContactId,
|
||||
Price = sc.Price,
|
||||
HandoverDate = sc.HandoverDate,
|
||||
ContractDate = sc.ContractDate,
|
||||
FileName = sc.FileName,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Animals = animals,
|
||||
});
|
||||
contractsAdded++;
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return $"Ingestion successful! Imported {contactsAdded} contacts ({contactsUpdated} updated), {data.Litters.Count} litters, {data.Gerbils.Count} gerbils, {data.GerbilPhotos.Count} photos, and {contractsAdded} sale contracts ({contractAnimalsSkipped} animal links to unknown gerbils skipped).";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,6 +294,22 @@ namespace GerbilManagerWebAPI.Import
|
||||
public List<Litter> Litters { get; set; } = new();
|
||||
public List<Gerbil> Gerbils { get; set; } = new();
|
||||
public List<ResolvedPhoto> GerbilPhotos { get; set; } = new();
|
||||
public List<ResolvedSaleContract> SaleContracts { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>Eine importierte Abgabevertrag-Zeile aus resolved_import.json.
|
||||
/// <c>Animals</c> sind die zugeordneten Gerbil-Ids (Join-Zeilen werden beim
|
||||
/// Ingest gebaut). Die .docx liegt NICHT im Vertrags-Dateiroot — der
|
||||
/// Word-Download liefert 404, das PDF wird aus den Daten neu erzeugt.</summary>
|
||||
public class ResolvedSaleContract
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid ContactId { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
public DateOnly HandoverDate { get; set; }
|
||||
public DateOnly ContractDate { get; set; }
|
||||
public required string FileName { get; set; }
|
||||
public List<Guid> Animals { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ResolvedPhoto
|
||||
|
||||
Reference in New Issue
Block a user