IMPORT-COUNTER-BUG: undatierte Wuerfe als WithoutDate zaehlen, nicht als Created
31 undatierte Wurfchronik-Eintraege (leeres Datumsfeld) wurden bei jedem Lauf als littersCreated++ gezaehlt, weil ihr Idempotenz-Key (kein Datum) nie in existingLitterKeySet stand -- aber kein DB-Insert folgte, da date is DateOnly d false war. Das verfaelschte Arithmetik und Julians Bericht (752 vs 723). Fix: undatierte Eintraege werden am Schleifenanfang uebersprungen (littersWithoutDate++, continue) bevor sie in litterIdMap oder den Created-Zaehler einfliessen. Execute-Block ohne redundante date-Pruefung. Neues Feld LitterSummary.WithoutDate; Report-Notiz wenn WithoutDate > 0. Gate: 126/126 C#-Tests, has-pending-model-changes = No. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, Guid>(); // 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),
|
||||
|
||||
Reference in New Issue
Block a user