diff --git a/GerbilManagerWebAPI/Endpoints/ImportEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ImportEndpoints.cs
new file mode 100644
index 0000000..cfc5b28
--- /dev/null
+++ b/GerbilManagerWebAPI/Endpoints/ImportEndpoints.cs
@@ -0,0 +1,35 @@
+using GerbilManagerWebAPI.Import;
+using Microsoft.AspNetCore.Http.HttpResults;
+
+namespace GerbilManagerWebAPI.Endpoints
+{
+ ///
+ /// FEAT-8 stage 3 — spreadsheet import loader. Consumes tools/import/output.
+ /// POST /import/dry-run -> report what WOULD be created/linked/quarantined (no writes)
+ /// POST /import/execute -> load the conflict-free subset (idempotent). GATED: only run
+ /// against Julian's DB under god-supervision after he approves the dry-run.
+ ///
+ public static class ImportEndpoints
+ {
+ public static IEndpointRouteBuilder MapImportEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/import").WithTags("Import");
+
+ group.MapPost("/dry-run", async Task> (
+ ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
+ {
+ var report = await new ImportService(db, config, env).RunAsync(execute: false);
+ return TypedResults.Ok(report);
+ });
+
+ group.MapPost("/execute", async Task> (
+ ApplicationContext db, IConfiguration config, IWebHostEnvironment env) =>
+ {
+ var report = await new ImportService(db, config, env).RunAsync(execute: true);
+ return TypedResults.Ok(report);
+ });
+
+ return app;
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Import/ImportModels.cs b/GerbilManagerWebAPI/Import/ImportModels.cs
new file mode 100644
index 0000000..adecdb4
--- /dev/null
+++ b/GerbilManagerWebAPI/Import/ImportModels.cs
@@ -0,0 +1,90 @@
+using System.Text.Json.Serialization;
+
+namespace GerbilManagerWebAPI.Import
+{
+ // ---- Source shapes (tools/import/output JSON, produced by extract.py) ----
+
+ public sealed class SourceAnimal
+ {
+ public string Id { get; set; } = "";
+ public string Name { get; set; } = "";
+ public List NameVariants { get; set; } = new();
+ public string Dob { get; set; } = "";
+ public string Death { get; set; } = "";
+ public string? Gender { get; set; }
+ public string Farbschlag { get; set; } = "";
+ public List FarbschlagVariants { get; set; } = new();
+ public SourceGenotype Genotype { get; set; } = new();
+ public string Zucht { get; set; } = "";
+ public List ParentRefs { get; set; } = new();
+ public List Photos { get; set; } = new();
+ public List SourceFiles { get; set; } = new();
+ public bool Conflict { get; set; }
+ public SourceLitterRef? LitterRef { get; set; }
+ }
+
+ public sealed class SourceGenotype
+ {
+ public Dictionary> Mapped8locus { get; set; } = new();
+ public string RawGenotype { get; set; } = "";
+ public List UnmappedTokens { get; set; } = new();
+ }
+
+ public sealed class SourceParentRef
+ {
+ public string Name { get; set; } = "";
+ public string Dob { get; set; } = "";
+ public string RoleGuess { get; set; } = "";
+ }
+
+ public sealed class SourceLitterRef
+ {
+ public string LitterId { get; set; } = "";
+ public string Method { get; set; } = "";
+ public string Confidence { get; set; } = ""; // "hoch" | "niedrig" | (ambiguous => candidates)
+ public List? Candidates { get; set; }
+ }
+
+ public sealed class SourceLitter
+ {
+ public string Id { get; set; } = "";
+ public string LitterId { get; set; } = "";
+ public string Date { get; set; } = "";
+ public string DamName { get; set; } = "";
+ public string SireName { get; set; } = "";
+ public int? TotalBorn { get; set; }
+ public string Zuchtnummer { get; set; } = "";
+ public string Note { get; set; } = "";
+ public List Warnings { get; set; } = new();
+ }
+
+ // ---- Report shapes (Julian-readable: counts per category + samples) ----
+
+ public sealed record ImportReport(
+ bool Executed,
+ LitterSummary Litters,
+ AnimalSummary Animals,
+ PhotoSummary Photos,
+ IReadOnlyList Samples,
+ IReadOnlyList Notes);
+
+ public sealed record LitterSummary(int InSource, int Created, int AlreadyImported);
+
+ public sealed record AnimalSummary(
+ int InSource,
+ int Created,
+ int LinkedToLitter,
+ int FarbschlagMatched,
+ int FarbschlagUnmatched,
+ int AlreadyImported,
+ QuarantineSummary Quarantined);
+
+ public sealed record QuarantineSummary(
+ int Conflicts,
+ int Stubs,
+ int DateOnlyLinks,
+ int AmbiguousLinks,
+ int Total);
+
+ public sealed record PhotoSummary(int Attached, int SourceFilesMissing);
+}
diff --git a/GerbilManagerWebAPI/Import/ImportService.cs b/GerbilManagerWebAPI/Import/ImportService.cs
new file mode 100644
index 0000000..a254009
--- /dev/null
+++ b/GerbilManagerWebAPI/Import/ImportService.cs
@@ -0,0 +1,293 @@
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using GerbilManagerWebAPI.Models;
+using Microsoft.EntityFrameworkCore;
+
+namespace GerbilManagerWebAPI.Import
+{
+ ///
+ /// FEAT-8c import loader. Consumes tools/import/output (animals.json + litters.json
+ /// produced by extract.py) and loads conflict-free data into the database:
+ /// litters first (Wurfchronik = authoritative), then animals matched onto them.
+ ///
+ /// Load policy:
+ /// - Litters: all created (idempotent by ExternalRef = source litter id).
+ /// - Animals: created when they have a DOB and are NOT in conflict. The birth-litter
+ /// link is set only for HIGH-confidence matches (litterRef.confidence == "hoch");
+ /// date-only/ambiguous links are quarantined (animal loads with LitterId = null).
+ /// - QUARANTINED (never loaded): conflicts + stubs (no DOB) — await the wife's review.
+ /// Idempotent: re-running matches on ExternalRef and skips existing rows.
+ /// Execute is gated by the endpoint; this service only acts when asked.
+ ///
+ public sealed class ImportService
+ {
+ private static readonly string[] LocusOrder = { "A", "C", "D", "E", "G", "P", "Sp", "Re" };
+ private static readonly JsonSerializerOptions Json = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ };
+ private const string ImportSourceTag = "FEAT-8 Stammbaum/Wurfchronik";
+
+ private readonly ApplicationContext _db;
+ private readonly string _sourceDir;
+ private readonly string _photoRoot;
+
+ public ImportService(ApplicationContext db, IConfiguration config, IWebHostEnvironment env)
+ {
+ _db = db;
+ _sourceDir = config["Import:SourcePath"]
+ ?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "tools", "import", "output"));
+ _photoRoot = config["Photos:RootPath"] ?? Path.Combine(env.ContentRootPath, "photo-storage");
+ }
+
+ public async Task RunAsync(bool execute)
+ {
+ var notes = new List();
+ var samples = new List();
+
+ var animals = Load>("animals.json") ?? new();
+ var litters = Load>("litters.json") ?? new();
+ if (animals.Count == 0 && litters.Count == 0)
+ notes.Add($"Keine Quelldaten gefunden in {_sourceDir} (animals.json/litters.json). extract.py zuerst ausführen.");
+
+ // ---- categorise animals ----
+ var loadable = new List();
+ int conflicts = 0, stubs = 0, dateOnly = 0, ambiguous = 0;
+ foreach (var a in animals)
+ {
+ if (a.Conflict) { conflicts++; continue; }
+ if (string.IsNullOrEmpty(a.Dob)) { stubs++; continue; }
+ loadable.Add(a);
+ var conf = a.LitterRef?.Confidence;
+ if (a.LitterRef?.Candidates is { Count: > 0 }) ambiguous++;
+ else if (conf == "niedrig") dateOnly++;
+ }
+
+ // existing rows (idempotency). Gerbils carry ExternalRef; Litters have no such
+ // column, so we key litter idempotency on the stable (Name + Date) pair instead.
+ var existingGerbilExtRefs = await _db.Gerbils
+ .Where(g => g.ExternalRef != null)
+ .Select(g => g.ExternalRef!).ToListAsync();
+ var existingGerbilSet = existingGerbilExtRefs.ToHashSet();
+
+ var existingLitterKeys = await _db.Litters
+ .Select(l => new { l.Name, l.Date }).ToListAsync();
+ var existingLitterKeySet = existingLitterKeys
+ .Select(x => $"{x.Name}|{x.Date:yyyy-MM-dd}").ToHashSet();
+
+ // colour-variety name -> id (case-insensitive)
+ var varieties = await _db.ColorVarieties.Select(v => new { v.Id, v.Name }).ToListAsync();
+ var varietyByName = varieties
+ .GroupBy(v => v.Name.Trim().ToLowerInvariant())
+ .ToDictionary(g => g.Key, g => g.First().Id);
+
+ // gender inference from litter roles (sire -> male, dam -> female; both -> unknown)
+ var sireNames = litters.Select(l => Normalize(StripZucht(l.SireName))).Where(s => s.Length > 0).ToHashSet();
+ 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;
+ var litterIdMap = new Dictionary(); // source litter id -> Litter.Id
+ foreach (var sl in litters)
+ {
+ var date = ParseDate(sl.Date);
+ var name = $"Wurf {sl.LitterId}".Trim();
+ var key = $"{name}|{date:yyyy-MM-dd}";
+ if (existingLitterKeySet.Contains(key)) { littersExisting++; continue; }
+
+ var id = Guid.NewGuid();
+ litterIdMap[sl.Id] = id;
+ littersCreated++;
+ if (execute && date is DateOnly d)
+ {
+ _db.Litters.Add(new Litter
+ {
+ Id = id,
+ Name = name,
+ Date = d,
+ 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)
+ samples.Add($"Wurf: {name} ({sl.Date}) — {sl.DamName} × {sl.SireName}");
+ }
+ if (execute) await _db.SaveChangesAsync();
+
+ // ---- animals ----
+ int animalsCreated = 0, linked = 0, fbMatched = 0, fbUnmatched = 0, animalsExisting = 0;
+ int photosAttached = 0, photosMissing = 0;
+ var createdAnimalByName = new Dictionary(); // normalized name -> gerbil id (for litter back-link)
+
+ foreach (var a in loadable)
+ {
+ if (existingGerbilSet.Contains(a.Id)) { animalsExisting++; continue; }
+ animalsCreated++;
+
+ Guid? litterId = null;
+ if (a.LitterRef?.Confidence == "hoch" && a.LitterRef.Candidates is not { Count: > 0 }
+ && litterIdMap.TryGetValue(a.LitterRef.LitterId, out var lid))
+ {
+ litterId = lid;
+ linked++;
+ }
+
+ Guid? colorVarietyId = null;
+ var fbCandidates = new[] { a.Farbschlag }.Concat(a.FarbschlagVariants)
+ .Where(s => !string.IsNullOrWhiteSpace(s));
+ foreach (var fb in fbCandidates)
+ {
+ if (varietyByName.TryGetValue(fb.Trim().ToLowerInvariant(), out var vid))
+ { colorVarietyId = vid; break; }
+ }
+ if (colorVarietyId is null) fbUnmatched++; else fbMatched++;
+
+ var gender = InferGender(a, sireNames, damNames);
+ var gid = Guid.NewGuid();
+ var norm = Normalize(StripZucht(a.Name));
+ if (norm.Length > 0) createdAnimalByName.TryAdd(norm, gid);
+
+ if (samples.Count < 16)
+ samples.Add($"Tier: {a.Name} (*{a.Dob}), Genotyp {ComposeGenotype(a.Genotype)}"
+ + (litterId is not null ? ", Wurf-verknüpft" : "")
+ + (colorVarietyId is not null ? $", Farbschlag „{a.Farbschlag}\"" : ""));
+
+ if (execute)
+ {
+ _db.Gerbils.Add(new Gerbil
+ {
+ Id = gid,
+ Name = a.Name,
+ Gender = gender,
+ Status = GerbilStatus.Active,
+ DateOfBirth = ParseDate(a.Dob),
+ DateOfDeath = ParseDate(a.Death),
+ LitterId = litterId,
+ ColorVarietyId = colorVarietyId,
+ Genotype = ComposeGenotype(a.Genotype),
+ ImportSource = ImportSourceTag,
+ ExternalRef = a.Id,
+ RawImportData = JsonSerializer.Serialize(new
+ {
+ a.Genotype.RawGenotype,
+ a.Genotype.UnmappedTokens,
+ a.Zucht,
+ a.SourceFiles,
+ FarbschlagRaw = a.Farbschlag,
+ }),
+ });
+ }
+
+ // photos
+ foreach (var rel in a.Photos)
+ {
+ var src = Path.Combine(_sourceDir, rel.Replace('/', Path.DirectorySeparatorChar));
+ if (!File.Exists(src)) { photosMissing++; continue; }
+ photosAttached++;
+ if (execute)
+ {
+ Directory.CreateDirectory(_photoRoot);
+ var fileName = $"{Guid.NewGuid():N}{Path.GetExtension(src)}";
+ File.Copy(src, Path.Combine(_photoRoot, fileName), overwrite: true);
+ _db.GerbilPhotos.Add(new GerbilPhoto
+ {
+ Id = Guid.NewGuid(),
+ GerbilId = gid,
+ FileName = fileName,
+ SortOrder = 0,
+ CreatedAt = DateTimeOffset.UtcNow,
+ });
+ }
+ }
+ }
+ if (execute) await _db.SaveChangesAsync();
+
+ // ---- back-link litter parents by name (best effort) ----
+ if (execute)
+ {
+ foreach (var sl in litters)
+ {
+ if (!litterIdMap.TryGetValue(sl.Id, out var lid)) continue;
+ var litter = await _db.Litters.FirstOrDefaultAsync(l => l.Id == lid);
+ if (litter is null) continue;
+ if (createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.SireName)), out var fId))
+ litter.FatherId = fId;
+ if (createdAnimalByName.TryGetValue(Normalize(StripZucht(sl.DamName)), out var mId))
+ litter.MotherId = mId;
+ }
+ await _db.SaveChangesAsync();
+ }
+
+ notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
+ if (!execute) notes.Add("DRY-RUN: nichts gespeichert. /import/execute lädt die konfliktfreien Daten.");
+
+ return new ImportReport(
+ Executed: execute,
+ Litters: new LitterSummary(litters.Count, littersCreated, littersExisting),
+ Animals: new AnimalSummary(
+ animals.Count, animalsCreated, linked, fbMatched, fbUnmatched, animalsExisting,
+ new QuarantineSummary(conflicts, stubs, dateOnly, ambiguous, conflicts + stubs)),
+ Photos: new PhotoSummary(photosAttached, photosMissing),
+ Samples: samples,
+ Notes: notes);
+ }
+
+ private T? Load(string file)
+ {
+ var path = Path.Combine(_sourceDir, file);
+ if (!File.Exists(path)) return default;
+ using var fs = File.OpenRead(path);
+ return JsonSerializer.Deserialize(fs, Json);
+ }
+
+ internal static string ComposeGenotype(SourceGenotype g)
+ {
+ var tokens = LocusOrder.Select(locus =>
+ {
+ if (g.Mapped8locus.TryGetValue(locus, out var pair) && pair.Count == 2)
+ return StripCaret(pair[0]) + StripCaret(pair[1]);
+ return "??";
+ });
+ return string.Join(' ', tokens);
+ }
+
+ private static string StripCaret(string allele) => allele.Replace("^", "");
+
+ private static Gender InferGender(SourceAnimal a, HashSet sires, HashSet dams)
+ {
+ var n = Normalize(StripZucht(a.Name));
+ bool isSire = sires.Contains(n), isDam = dams.Contains(n);
+ if (isSire && !isDam) return Gender.male;
+ if (isDam && !isSire) return Gender.female;
+ return Gender.unknown;
+ }
+
+ internal static DateOnly? ParseDate(string s)
+ {
+ if (string.IsNullOrWhiteSpace(s)) return null;
+ var m = Regex.Match(s, @"(\d{1,2})\.(\d{1,2})\.(\d{2,4})");
+ if (!m.Success) return null;
+ int d = int.Parse(m.Groups[1].Value), mo = int.Parse(m.Groups[2].Value);
+ int y = int.Parse(m.Groups[3].Value);
+ if (y < 100) y += 2000;
+ try { return new DateOnly(y, mo, d); }
+ catch { return null; }
+ }
+
+ private static string StripZucht(string name)
+ {
+ var n = Regex.Replace(name ?? "", @"\[.*?\]", " ");
+ n = Regex.Replace(n, @"\s+(?:of|von\s+den|von\s+der|v\.\s?d\.|von)\s+.*$", "", RegexOptions.IgnoreCase);
+ return n.Trim();
+ }
+
+ private static string Normalize(string name)
+ {
+ var n = (name ?? "").ToLowerInvariant();
+ n = Regex.Replace(n, @"\bgen\.\b", " ");
+ n = Regex.Replace(n, @"[^a-z0-9äöüß]", "");
+ return n;
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.Designer.cs b/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.Designer.cs
new file mode 100644
index 0000000..bf9b777
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.Designer.cs
@@ -0,0 +1,897 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ [DbContext(typeof(ApplicationContext))]
+ [Migration("20260606052952_AddImportProvenanceFields")]
+ partial class AddImportProvenanceFields
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanonicalGenotype")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("ColorVarieties");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
+ Name = "Pink Eyed White (PEW)",
+ SortOrder = 0
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000002"),
+ CanonicalGenotype = "aa chch DD EE GG PP spsp rere",
+ Name = "Hermelin",
+ SortOrder = 1
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000003"),
+ CanonicalGenotype = "AA chch DD EE GG PP spsp rere",
+ Name = "Himalaya",
+ SortOrder = 2
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000004"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel",
+ SortOrder = 3
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000005"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Schwarzschimmel",
+ SortOrder = 4
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000006"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Rotaugenschimmel",
+ SortOrder = 5
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000007"),
+ CanonicalGenotype = "AA CC DD EE GG PP spsp rere",
+ Name = "Agouti",
+ SortOrder = 6
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000008"),
+ CanonicalGenotype = "aa CC DD EE GG PP spsp rere",
+ Name = "Schwarz",
+ SortOrder = 7
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000009"),
+ CanonicalGenotype = "AA CC DD EE gg PP spsp rere",
+ Name = "Silberagouti",
+ SortOrder = 8
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000010"),
+ CanonicalGenotype = "aa CC DD EE gg PP spsp rere",
+ Name = "Anthrazit",
+ SortOrder = 9
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000011"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs",
+ SortOrder = 10
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000012"),
+ CanonicalGenotype = "aa CC dd EE GG PP spsp rere",
+ Name = "Blau",
+ SortOrder = 11
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000013"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold",
+ SortOrder = 12
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000014"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin",
+ SortOrder = 13
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000015"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs",
+ SortOrder = 14
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000016"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs",
+ SortOrder = 15
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000017"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "dd Gold",
+ SortOrder = 16
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000018"),
+ CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
+ Name = "dd Platin",
+ SortOrder = 17
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000019"),
+ CanonicalGenotype = "aa CC DD EE gg pp spsp rere",
+ Name = "Altweiss (REW)",
+ SortOrder = 18
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000020"),
+ CanonicalGenotype = "AA CC DD ee gg pp spsp rere",
+ Name = "Apricot (Blassfuchs)",
+ SortOrder = 19
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000021"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs",
+ SortOrder = 20
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000022"),
+ CanonicalGenotype = "aa CC DD ee gg pp spsp rere",
+ Name = "C-Separator",
+ SortOrder = 21
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000023"),
+ CanonicalGenotype = "AA CC DD EE gg pp spsp rere",
+ Name = "Elfenbein",
+ SortOrder = 22
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000024"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs",
+ SortOrder = 23
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000025"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Marder",
+ SortOrder = 24
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000026"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Siam (Marder-Hell)",
+ SortOrder = 25
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000027"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs",
+ SortOrder = 26
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000028"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Saphir",
+ SortOrder = 27
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000029"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Schimmel (Orangeschimmel)",
+ SortOrder = 28
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000030"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Topas",
+ SortOrder = 29
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000031"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin-Hell",
+ SortOrder = 30
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000032"),
+ CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
+ Name = "Agouti dd",
+ SortOrder = 31
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000033"),
+ CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
+ Name = "Silberagouti dd",
+ SortOrder = 32
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000034"),
+ CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
+ Name = "Kohlfuchs dd",
+ SortOrder = 33
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000035"),
+ CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
+ Name = "Anthrazit dd",
+ SortOrder = 34
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000036"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "Agouti CP-Hell",
+ SortOrder = 35
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000037"),
+ CanonicalGenotype = "aa cchmcchm DD ee gg PP spsp rere",
+ Name = "Blaufuchs CP",
+ SortOrder = 36
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000038"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Polarfuchsschimmel",
+ SortOrder = 37
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000039"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Silberschimmel",
+ SortOrder = 38
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000040"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Algierfuchsschimmel",
+ SortOrder = 39
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000041"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "Polarfuchs-Hell CP",
+ SortOrder = 40
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000042"),
+ CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel",
+ SortOrder = 41
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000043"),
+ CanonicalGenotype = "aa CC DD efef gg PP spsp rere",
+ Name = "Blaufuchsschimmel",
+ SortOrder = 42
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000044"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs, hell",
+ SortOrder = 43
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000045"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs, hell",
+ SortOrder = 44
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000046"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Goldfuchsschimmel",
+ SortOrder = 45
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000047"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold-Hell",
+ SortOrder = 46
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000048"),
+ CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
+ Name = "Siam (Marder-Hell) dd",
+ SortOrder = 47
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000049"),
+ CanonicalGenotype = "aa cchmcchm dd EE GG PP spsp rere",
+ Name = "Marder dd",
+ SortOrder = 48
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000050"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel-Hell",
+ SortOrder = 49
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000051"),
+ CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
+ Name = "Silberagouti dd CP",
+ SortOrder = 50
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000052"),
+ CanonicalGenotype = "AA cchmcchm dd EE gg PP spsp rere",
+ Name = "Silberagouti-Hell dd CP",
+ SortOrder = 51
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000053"),
+ CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
+ Name = "Agouti dd CP",
+ SortOrder = 52
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000054"),
+ CanonicalGenotype = "AA cchmcchm dd EE GG PP spsp rere",
+ Name = "Agouti-Hell dd CP",
+ SortOrder = 53
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000055"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs, hell",
+ SortOrder = 54
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000056"),
+ CanonicalGenotype = "aa CC DD efef GG pp spsp rere",
+ Name = "Rotfuchsschimmel",
+ SortOrder = 55
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000057"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs, hell",
+ SortOrder = 56
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000058"),
+ CanonicalGenotype = "aa CC DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel, hell",
+ SortOrder = 57
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000059"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs, hell",
+ SortOrder = 58
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000060"),
+ CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
+ Name = "Zobel dd",
+ SortOrder = 59
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000061"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs-Hell",
+ SortOrder = 60
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000062"),
+ CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
+ Name = "Kohlfuchs CP",
+ SortOrder = 61
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000063"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "Algierfuchs CP",
+ SortOrder = 62
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000064"),
+ CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
+ Name = "Silberagouti CP",
+ SortOrder = 63
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000065"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "Agouti CP",
+ SortOrder = 64
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000066"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "Algierfuchs-Hell CP",
+ SortOrder = 65
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000067"),
+ CanonicalGenotype = "aa cchmcchm DD ee GG PP spsp rere",
+ Name = "Kohlfuchs,hell CP",
+ SortOrder = 66
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000068"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "Polarfuchs CP",
+ SortOrder = 67
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000069"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs, hell",
+ SortOrder = 68
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000070"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Topas dd",
+ SortOrder = 69
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000071"),
+ CanonicalGenotype = "aa cchmcchm dd EE gg PP spsp rere",
+ Name = "Zobel-Hell dd",
+ SortOrder = 70
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000072"),
+ CanonicalGenotype = "aa cchmcchm DD efef GG PP spsp rere",
+ Name = "Kohlfuchsschimmel CP",
+ SortOrder = 71
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000073"),
+ CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
+ Name = "Blaufuchs dd",
+ SortOrder = 72
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contacts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Enclosures");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CauseOfDeath")
+ .HasColumnType("text");
+
+ b.Property("ColorVarietyId")
+ .HasColumnType("uuid");
+
+ b.Property("DateOfBirth")
+ .HasColumnType("date");
+
+ b.Property("DateOfDeath")
+ .HasColumnType("date");
+
+ b.Property("EnclosureId")
+ .HasColumnType("uuid");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Genotype")
+ .HasColumnType("text");
+
+ b.Property("GoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ImportSource")
+ .HasColumnType("text");
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("OriginContactId")
+ .HasColumnType("uuid");
+
+ b.Property("RawImportData")
+ .HasColumnType("text");
+
+ b.Property("ReceiverContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ColorVarietyId");
+
+ b.HasIndex("EnclosureId");
+
+ b.HasIndex("LitterId");
+
+ b.HasIndex("OriginContactId");
+
+ b.HasIndex("ReceiverContactId");
+
+ b.ToTable("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Caption")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("GerbilPhotos");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Veterinarian")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("HealthRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("ExpectedGoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("FatherId")
+ .HasColumnType("uuid");
+
+ b.Property("MotherId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("PairingCode")
+ .HasColumnType("text");
+
+ b.Property("TotalBorn")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("FatherId");
+
+ b.HasIndex("MotherId");
+
+ b.ToTable("Litters");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("WeightGrams")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("WeightRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety")
+ .WithMany()
+ .HasForeignKey("ColorVarietyId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure")
+ .WithMany("Gerbils")
+ .HasForeignKey("EnclosureId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter")
+ .WithMany()
+ .HasForeignKey("LitterId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact")
+ .WithMany()
+ .HasForeignKey("OriginContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact")
+ .WithMany()
+ .HasForeignKey("ReceiverContactId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("ColorVariety");
+
+ b.Navigation("Enclosure");
+
+ b.Navigation("Litter");
+
+ b.Navigation("OriginContact");
+
+ b.Navigation("ReceiverContact");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father")
+ .WithMany()
+ .HasForeignKey("FatherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother")
+ .WithMany()
+ .HasForeignKey("MotherId")
+ .OnDelete(DeleteBehavior.Restrict);
+
+ b.Navigation("Father");
+
+ b.Navigation("Mother");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b =>
+ {
+ b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null)
+ .WithMany()
+ .HasForeignKey("GerbilId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Navigation("Gerbils");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.cs b/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.cs
new file mode 100644
index 0000000..c7c5f08
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260606052952_AddImportProvenanceFields.cs
@@ -0,0 +1,38 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ ///
+ public partial class AddImportProvenanceFields : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "PairingCode",
+ table: "Litters",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "RawImportData",
+ table: "Gerbils",
+ type: "text",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "PairingCode",
+ table: "Litters");
+
+ migrationBuilder.DropColumn(
+ name: "RawImportData",
+ table: "Gerbils");
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
index 2116917..b27de05 100644
--- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
+++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
@@ -650,6 +650,9 @@ namespace GerbilManagerWebAPI.Migrations
b.Property("OriginContactId")
.HasColumnType("uuid");
+ b.Property("RawImportData")
+ .HasColumnType("text");
+
b.Property("ReceiverContactId")
.HasColumnType("uuid");
@@ -759,6 +762,9 @@ namespace GerbilManagerWebAPI.Migrations
b.Property("Notes")
.HasColumnType("text");
+ b.Property("PairingCode")
+ .HasColumnType("text");
+
b.Property("TotalBorn")
.HasColumnType("integer");
diff --git a/GerbilManagerWebAPI/Models/Gerbil.cs b/GerbilManagerWebAPI/Models/Gerbil.cs
index 0839591..5d96bce 100644
--- a/GerbilManagerWebAPI/Models/Gerbil.cs
+++ b/GerbilManagerWebAPI/Models/Gerbil.cs
@@ -46,5 +46,9 @@ namespace GerbilManagerWebAPI.Models
// Provenance (for the FEAT-8 spreadsheet import).
public string? ImportSource { get; set; }
public string? ExternalRef { get; set; }
+
+ /// Raw import payload preserved verbatim (rawGenotype + unmappedTokens like
+ /// the Uw locus / WFNZ markers) so nothing from the spreadsheets is lost. JSON text.
+ public string? RawImportData { get; set; }
}
}
diff --git a/GerbilManagerWebAPI/Models/Litter.cs b/GerbilManagerWebAPI/Models/Litter.cs
index 89b68af..954c4c4 100644
--- a/GerbilManagerWebAPI/Models/Litter.cs
+++ b/GerbilManagerWebAPI/Models/Litter.cs
@@ -21,5 +21,9 @@ namespace GerbilManagerWebAPI.Models
/// Computed ~35 days after Date by default; editable.
public DateOnly? ExpectedGoHomeDate { get; set; }
public string? Notes { get; set; }
+
+ /// Zuchtnummer der Verpaarung (Wurfchronik col H) — pairing-level code;
+ /// litters sharing it are the same Zuchtpaar. Set by the FEAT-8 import.
+ public string? PairingCode { get; set; }
}
}
diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs
index 6c28261..335275e 100644
--- a/GerbilManagerWebAPI/Program.cs
+++ b/GerbilManagerWebAPI/Program.cs
@@ -67,5 +67,6 @@ app.MapWeightRecordEndpoints();
app.MapInbreedingEndpoints();
app.MapPhotoEndpoints();
app.MapSaleAdEndpoints();
+app.MapImportEndpoints();
app.Run();