diff --git a/GerbilManager.Tests/ImportServiceTests.cs b/GerbilManager.Tests/ImportServiceTests.cs index f40fd14..ffdb400 100644 --- a/GerbilManager.Tests/ImportServiceTests.cs +++ b/GerbilManager.Tests/ImportServiceTests.cs @@ -507,6 +507,44 @@ namespace GerbilManager.Tests } } + [Fact] + public async Task UndatedLitters_counted_as_WithoutDate_not_Created() + { + // COUNTER-BUG regression: litters with no parseable date must go into WithoutDate, + // NOT Created. On re-import, Created must be 0 (not 31-phantom-phantom-phantom...). + var dir = Path.Combine(Path.GetTempPath(), "undated-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + // One dated litter, one undated litter (blank date field) + File.WriteAllText(Path.Combine(dir, "litters.json"), """ + [ + {"id":"L-dated","litterId":"A","date":"01.02.2020","damName":"Mutter","sireName":"Vater","totalBorn":3,"zuchtnummer":"","note":""}, + {"id":"L-undated","litterId":"B","date":"","damName":"Mutter","sireName":"Vater","totalBorn":0,"zuchtnummer":"","note":""} + ] + """); + File.WriteAllText(Path.Combine(dir, "animals.json"), "[]"); + + using var db = NewDb(); + + // First run + var r1 = await new ImportService(db, dir, dir).RunAsync(execute: true); + Assert.Equal(2, r1.Litters.InSource); + Assert.Equal(1, r1.Litters.Created); // only the dated one + Assert.Equal(0, r1.Litters.AlreadyImported); + Assert.Equal(1, r1.Litters.WithoutDate); // the undated one + Assert.Equal(1, await db.Litters.CountAsync()); // only 1 persisted + + // Second run (re-import): dated litter is now existing, undated still WithoutDate + var r2 = await new ImportService(db, dir, dir).RunAsync(execute: true); + Assert.Equal(0, r2.Litters.Created); // no phantom "created" + Assert.Equal(1, r2.Litters.AlreadyImported); + Assert.Equal(1, r2.Litters.WithoutDate); + Assert.Equal(1, await db.Litters.CountAsync()); // still only 1 row + } + finally { try { Directory.Delete(dir, recursive: true); } catch { } } + } + [Theory] [InlineData("01.02.2020", 2020, 2, 1)] [InlineData("5.3.21", 2021, 3, 5)] diff --git a/GerbilManagerWebAPI/Import/ImportModels.cs b/GerbilManagerWebAPI/Import/ImportModels.cs index 449c875..fd1c8f2 100644 --- a/GerbilManagerWebAPI/Import/ImportModels.cs +++ b/GerbilManagerWebAPI/Import/ImportModels.cs @@ -87,7 +87,7 @@ namespace GerbilManagerWebAPI.Import public sealed record LitterSummary(int InSource, int Created, int AlreadyImported, int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0, - int ParentFksBackfilled = 0); + int ParentFksBackfilled = 0, int WithoutDate = 0); public sealed record AnimalSummary( int InSource, diff --git a/GerbilManagerWebAPI/Import/ImportService.cs b/GerbilManagerWebAPI/Import/ImportService.cs index da85725..fc6c9ae 100644 --- a/GerbilManagerWebAPI/Import/ImportService.cs +++ b/GerbilManagerWebAPI/Import/ImportService.cs @@ -94,11 +94,17 @@ namespace GerbilManagerWebAPI.Import var damNames = litters.Select(l => Normalize(StripZucht(l.DamName))).Where(s => s.Length > 0).ToHashSet(); // ---- litters: create map source.id -> Litter (for high-confidence animal links) ---- - int littersCreated = 0, littersExisting = 0; + // COUNTER-BUG FIX: undated litters (31 in the Wurfchronik) have no parseable date, + // so their existingLitterKeySet key was always "" → they were always counted as + // "created" even though the execute block skipped them (date is DateOnly d = false). + // Fix: skip undated litters early — they can never be created or linked to animals. + int littersCreated = 0, littersExisting = 0, littersWithoutDate = 0; var litterIdMap = new Dictionary(); // source litter id -> Litter.Id foreach (var sl in litters) { var date = ParseDate(sl.Date); + if (date is null) { littersWithoutDate++; continue; } // undated: skip entirely + var name = $"Wurf {sl.LitterId}".Trim(); var key = $"{name}|{date:yyyy-MM-dd}"; if (existingLitterKeySet.Contains(key)) { littersExisting++; continue; } @@ -106,19 +112,19 @@ namespace GerbilManagerWebAPI.Import var id = Guid.NewGuid(); litterIdMap[sl.Id] = id; littersCreated++; - if (execute && date is DateOnly d) + if (execute) { _db.Litters.Add(new Litter { Id = id, Name = name, - Date = d, + Date = date.Value, TotalBorn = sl.TotalBorn, Notes = string.IsNullOrWhiteSpace(sl.Note) ? null : sl.Note, PairingCode = string.IsNullOrWhiteSpace(sl.Zuchtnummer) ? null : sl.Zuchtnummer, }); } - if (samples.Count < 8 && date is not null) + if (samples.Count < 8) samples.Add($"Wurf: {name} ({sl.Date}) — {sl.DamName} × {sl.SireName}"); } if (execute) await _db.SaveChangesAsync(); @@ -454,6 +460,8 @@ namespace GerbilManagerWebAPI.Import if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync(); } + if (littersWithoutDate > 0) + notes.Add($"Würfe ohne Datum: {littersWithoutDate} Wurfchronik-Einträge ohne parsbares Geburtsdatum übersprungen (weder erstellt noch verknüpft)."); notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin."); if (parentLinksAdded > 0) notes.Add($"Stammbaum-Diagramm: {parentLinksAdded} Tiere über Eltern-Verknüpfung einem (abgeleiteten) Wurf zugeordnet ({derivedLitters} abgeleitete Würfe)."); @@ -468,7 +476,7 @@ namespace GerbilManagerWebAPI.Import return new ImportReport( Executed: execute, - Litters: new LitterSummary(litters.Count, littersCreated, littersExisting, derivedLitters, derivedLittersSkipped, litterParentFksDropped, parentFksBackfilled), + Litters: new LitterSummary(litters.Count, littersCreated, littersExisting, derivedLitters, derivedLittersSkipped, litterParentFksDropped, parentFksBackfilled, littersWithoutDate), Animals: new AnimalSummary( animals.Count, animalsCreated, linked, fbMatched, fbUnmatched, animalsExisting, new QuarantineSummary(conflicts, stubs, dateOnly, ambiguous, conflicts + stubs),