using GerbilManagerWebAPI.Models; using Microsoft.EntityFrameworkCore; public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet Gerbils => Set(); public DbSet Litters => Set(); public DbSet Contacts => Set(); public DbSet Enclosures => Set(); public DbSet ColorVarieties => Set(); public DbSet GerbilPhotos => Set(); public DbSet EnclosurePhotos => Set(); public DbSet HealthRecords => Set(); public DbSet WeightRecords => Set(); public DbSet SaleContracts => Set(); public DbSet BreederSettings => Set(); public DbSet Sites => Set(); public DbSet Pages => Set(); public DbSet Blocks => Set(); public DbSet Media => Set(); public DbSet Requests => Set(); public DbSet MailSettings => Set(); public DbSet Feedback => Set(); public DbSet FeedbackAttachments => Set(); public DbSet WebPushSubscriptions => Set(); public DbSet AcquisitionRecords => Set(); public DbSet SaleReservations => Set(); public DbSet WaitingListEntries => Set(); public DbSet ReturnRecords => Set(); public DbSet ExhibitionResults => Set(); public DbSet GerbilOverrides => Set(); // Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key), // so it can never drift from Name regardless of which code path mutates the entity. public override int SaveChanges(bool acceptAllChangesOnSuccess) { SyncNameSearch(); return base.SaveChanges(acceptAllChangesOnSuccess); } public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { SyncNameSearch(); return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } private void SyncNameSearch() { foreach (var entry in ChangeTracker.Entries()) { if (entry.State is EntityState.Added or EntityState.Modified) entry.Entity.NameSearch = GerbilSearch.Normalize(entry.Entity.Name); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { // DB-4: ICU German collation on user-visible / searched text columns. // "de-x-icu" gives correct German sort order (ä between a and b) and locale-aware // case-folding (lower('Ä')='ä'). Applied only on Npgsql (Postgres); SQLite does not // support custom collation names and would fail EnsureCreated on the test host. const string DeIcu = "de-x-icu"; bool isNpgsql = Database.ProviderName?.Contains("Npgsql", StringComparison.OrdinalIgnoreCase) ?? false; modelBuilder.Entity(e => { // Enums persisted as their string names (readable, Gridify-friendly). e.Property(g => g.Gender).HasConversion(); e.Property(g => g.Status).HasConversion(); // Residency defaults to true (own stock unless explicitly marked external). e.Property(g => g.IsResident).HasDefaultValue(true); // Manual-flag defaults false: every pre-existing / imported row is import-driven. // Only POST /gerbils sets it true, so the ingest wipe never touches manual animals. e.Property(g => g.IsManual).HasDefaultValue(false); // DB-4: German collation on searched/sorted name fields (Npgsql-only). if (isNpgsql) { e.Property(g => g.Name).UseCollation(DeIcu); e.Property(g => g.NameSearch).UseCollation(DeIcu); e.Property(g => g.OriginBreeder).UseCollation(DeIcu); } // FEAT-14: character traits stored as a JSON text column (works on both // Npgsql and the SQLite test host; opaque labels, no backend vocabulary). var traitsConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter, string>( v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null), v => string.IsNullOrEmpty(v) ? new List() : System.Text.Json.JsonSerializer.Deserialize>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List()); var traitsComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer>( (a, b) => (a ?? new List()).SequenceEqual(b ?? new List()), v => v == null ? 0 : v.Aggregate(0, (h, s) => HashCode.Combine(h, s.GetHashCode())), v => v.ToList()); e.Property(g => g.CharacterTraits).HasConversion(traitsConverter, traitsComparer); e.HasOne(g => g.Litter).WithMany() .HasForeignKey(g => g.LitterId).OnDelete(DeleteBehavior.SetNull); e.HasOne(g => g.OriginContact).WithMany() .HasForeignKey(g => g.OriginContactId).OnDelete(DeleteBehavior.Restrict); e.HasOne(g => g.ReceiverContact).WithMany() .HasForeignKey(g => g.ReceiverContactId).OnDelete(DeleteBehavior.Restrict); e.HasOne(g => g.Enclosure).WithMany(en => en.Gerbils) .HasForeignKey(g => g.EnclosureId).OnDelete(DeleteBehavior.SetNull); e.HasOne(g => g.ColorVariety).WithMany() .HasForeignKey(g => g.ColorVarietyId).OnDelete(DeleteBehavior.SetNull); // DB-1: ExternalRef is the import idempotency key — enforce uniqueness at the DB level. // Filtered (nulls allowed: manually-entered animals have no ExternalRef). e.HasIndex(g => g.ExternalRef) .IsUnique() .HasFilter("\"ExternalRef\" IS NOT NULL"); }); modelBuilder.Entity(e => { e.HasOne(l => l.Father).WithMany() .HasForeignKey(l => l.FatherId).OnDelete(DeleteBehavior.Restrict); e.HasOne(l => l.Mother).WithMany() .HasForeignKey(l => l.MotherId).OnDelete(DeleteBehavior.Restrict); // DB-5: ExternalRef = source litter id from extract.py. // Unique (filtered, nulls allowed for manually-entered litters). e.HasIndex(l => l.ExternalRef) .IsUnique() .HasFilter("\"ExternalRef\" IS NOT NULL"); e.Property(l => l.IsManual).HasDefaultValue(false); }); modelBuilder.Entity(e => { e.Property(h => h.Type).HasConversion(); e.HasOne().WithMany() .HasForeignKey(h => h.GerbilId).OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(e => e.HasOne().WithMany() .HasForeignKey(w => w.GerbilId).OnDelete(DeleteBehavior.Cascade)); modelBuilder.Entity(e => e.HasOne().WithMany() .HasForeignKey(p => p.GerbilId).OnDelete(DeleteBehavior.Cascade)); modelBuilder.Entity(e => e.HasOne().WithMany() .HasForeignKey(p => p.EnclosureId).OnDelete(DeleteBehavior.Cascade)); // FEAT-13: Abgabeverträge + Zuchtprofil. modelBuilder.Entity(e => { e.Property(c => c.Price).HasPrecision(10, 2); // Restrict: ein Kontakt mit Verträgen ist ein Dokumentenbestand, // kein versehentlich löschbarer Datensatz. e.HasOne(c => c.Contact).WithMany() .HasForeignKey(c => c.ContactId).OnDelete(DeleteBehavior.Restrict); }); modelBuilder.Entity(e => { e.HasKey(a => new { a.SaleContractId, a.GerbilId }); e.HasOne().WithMany(c => c.Animals) .HasForeignKey(a => a.SaleContractId).OnDelete(DeleteBehavior.Cascade); // Cascade: wird ein Tier gelöscht, verschwindet nur die Verknüpfung — // der Vertrag (und seine .docx als Beleg) bleibt bestehen. e.HasOne(a => a.Gerbil).WithMany() .HasForeignKey(a => a.GerbilId).OnDelete(DeleteBehavior.Cascade); }); // Zuchtprofil: genau eine (leere) Zeile mit fixer Id. modelBuilder.Entity() .HasData(new BreederSettings { Id = GerbilManagerWebAPI.Models.BreederSettings.SingletonId }); // WEB epic: CMS content model. modelBuilder.Entity(e => { var navConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter, string>( v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null), v => string.IsNullOrEmpty(v) ? new List() : System.Text.Json.JsonSerializer.Deserialize>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List()); var navComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer>( (a, b) => (a ?? new List()).SequenceEqual(b ?? new List()), v => v == null ? 0 : v.Aggregate(0, (h, x) => HashCode.Combine(h, x.GetHashCode())), v => v.ToList()); e.Property(s => s.NavOrder).HasConversion(navConverter, navComparer); }); modelBuilder.Entity(e => { e.HasIndex(p => p.Slug).IsUnique(); e.Property(p => p.Status).HasConversion(); e.HasMany(p => p.Blocks).WithOne() .HasForeignKey(b => b.PageId).OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity(e => e.Property(b => b.Type).HasConversion()); SeedCms(modelBuilder); // INBOX epic: Gmail request inbox. modelBuilder.Entity(e => { e.Property(r => r.Status).HasConversion(); e.HasIndex(r => r.GmailMessageId).IsUnique(); e.HasOne(r => r.AssignedContact).WithMany() .HasForeignKey(r => r.AssignedContactId).OnDelete(DeleteBehavior.Restrict); }); // exactly one MailSettings row, fixed id. modelBuilder.Entity() .HasData(new MailSettings { Id = GerbilManagerWebAPI.Models.MailSettings.SingletonId }); // FEEDBACK: deliberately relationship-free. GerbilId/LitterId are plain nullable // Guid columns (no navigation properties → EF creates NO foreign key), so the // import re-ingest wipe of Gerbils/Litters never cascades into — or breaks — // feedback rows. They survive re-ingest, which is the whole point. modelBuilder.Entity(e => { e.HasIndex(f => f.CreatedAt); }); // ERWERB: like Feedback, deliberately relationship-free. GerbilId/SourceContactId // are plain nullable Guid columns (no navigation properties → EF creates NO foreign // key), so the import re-ingest wipe of Gerbils/Contacts never cascades into — or // breaks — acquisition rows. They survive re-ingest, which is the whole point. modelBuilder.Entity(e => { e.Property(a => a.Price).HasPrecision(10, 2); e.HasIndex(a => a.GerbilId); }); // ABGABE-STATUS: deliberately relationship-free (same rationale as Feedback). // GerbilId/ReservedForContactId are plain Guid columns (no navigation properties → // EF creates NO foreign key), so the import re-ingest wipe of Gerbils/Contacts never // cascades into — or breaks — reservation rows. They survive re-ingest by design. modelBuilder.Entity(e => { e.Property(r => r.Price).HasPrecision(10, 2); e.HasIndex(r => r.GerbilId); }); // WAITLIST: same relationship-free pattern as Feedback. ContactId is a plain // nullable Guid column (no navigation property → EF creates NO foreign key), so // the import re-ingest wipe of Contacts never cascades into — or breaks — // waiting-list rows. They survive re-ingest, which is the whole point. modelBuilder.Entity(e => { e.HasIndex(w => w.CreatedAt); }); // RÜCKNAHMEN (getback_tb): wie Feedback bewusst beziehungsfrei. GerbilId/ // FromContactId sind einfache nullable Guid-Spalten (keine Navigation → EF legt // KEINEN Foreign Key an), damit der Import-Re-Ingest-Wipe von Gerbils/Contacts // diese Zeilen weder löscht noch bricht. Sie überleben den Re-Ingest. modelBuilder.Entity(e => { e.Property(r => r.ReturnPrice).HasPrecision(10, 2); e.Property(r => r.OriginalPrice).HasPrecision(10, 2); e.HasIndex(r => r.GerbilId); e.HasIndex(r => r.CreatedAt); }); // EXHIBITION: deliberately relationship-free (same pattern as Feedback). GerbilId is a // plain nullable Guid column (no navigation property → EF creates NO foreign key), so the // import re-ingest wipe of Gerbils never cascades into or breaks exhibition rows. modelBuilder.Entity(e => { e.HasIndex(x => x.GerbilId); }); // VERIFIED/PROTECTED OVERRIDE: deliberately relationship-free (same pattern as Feedback). // GerbilId is a plain Guid column (no navigation property → EF creates NO foreign key), so // the import re-ingest wipe of Gerbils never cascades into — or breaks — override rows. They // survive re-ingest and are re-applied on top of the fresh import (the freeze), which is the // whole point. modelBuilder.Entity(e => { e.HasIndex(o => o.GerbilId).IsUnique(); }); // Manual-flag default false for Contact (imported/pre-existing rows are import-driven). modelBuilder.Entity().Property(c => c.IsManual).HasDefaultValue(false); // DB-4: German collation on remaining searched/sorted text columns (Npgsql-only). if (isNpgsql) { modelBuilder.Entity() .Property(v => v.Name).UseCollation(DeIcu); modelBuilder.Entity() .Property(c => c.Name).UseCollation(DeIcu); } SeedColorVarieties(modelBuilder); } /// /// Seed the public website's CMS: one Site + the 6 Jimdo-mirror pages, each with a /// placeholder Heading; the abgabetiere page also carries an auto AbgabetiereList block /// (resolved from live ForSale animals at snapshot time). Hand-editable afterwards. /// private static void SeedCms(ModelBuilder modelBuilder) { Guid Pid(int n) => new($"51720001-0000-0000-0000-{n:D12}"); Guid Bid(int n) => new($"51720002-0000-0000-0000-{n:D12}"); (int n, string Slug, string Title)[] pages = { (1, "start", "Startseite"), (2, "ueber-die-zucht", "Über die Zucht"), (3, "abgabetiere", "Abgabetiere"), (4, "abgabebedingungen", "Abgabebedingungen"), (5, "farben-genetik", "Farben & Genetik"), (6, "kontakt", "Kontakt"), }; // WEB-6a: footer-only legal pages (not in main nav) (int n, string Slug, string Title, string BodyMd)[] legalPages = { (7, "impressum", "Impressum", "## Angaben gemäß § 18 Abs. 1 MStV\\n\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\n\\n## Kontakt\\n\\nE-Mail: [E-Mail-Adresse]\\nTelefon: [Telefonnummer]\\n\\n---\\n\\nDiese Seite stellt eine private Hobbyzucht mongolischer Rennmäuse ohne Gewinnerzielungsabsicht vor.\\n\\n*Hinweis für den Seitenbetreiber: Name und ladungsfähige Anschrift (kein Postfach) sind Pflicht (§ 18 Abs. 1 MStV). Sollte die Zucht jemals gewerblich bzw. geschäftsmäßig betrieben werden, gelten zusätzlich § 5 DDG (vollständiges Anbieter-Impressum) und ggf. die Erlaubnispflicht nach § 11 TierSchG — dann zuständige Behörde und Aktenzeichen hier ergänzen. Kein Hinweis auf die EU-Streitbeilegungsplattform: diese wurde zum 20.07.2025 abgeschaltet (ein fortbestehender Link wäre irreführend).*"), (8, "datenschutz", "Datenschutz", "## Datenschutzerklärung\\n\\n## 1. Verantwortlicher\\n\\nVerantwortlich für die Datenverarbeitung auf dieser Webseite ist:\\n[Vor- und Nachname]\\n[Straße und Hausnummer]\\n[PLZ und Ort]\\nE-Mail: [E-Mail-Adresse]\\n\\n## 2. Hosting und Server-Logfiles\\n\\nDiese Webseite wird auf einem eigenen Server betrieben. Beim Aufruf der Seite verarbeitet der Server automatisch Informationen, die dein Browser übermittelt, in sogenannten Server-Logfiles: IP-Adresse, Datum und Uhrzeit des Zugriffs, die aufgerufene Seite bzw. Datei, die übertragene Datenmenge sowie Browser und Betriebssystem.\\nDiese Verarbeitung ist technisch erforderlich, um die Webseite auszuliefern und ihren sicheren und stabilen Betrieb zu gewährleisten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am störungsfreien Betrieb und an der Sicherheit). Die Logfiles werden nach spätestens [z. B. 7] Tagen gelöscht und nicht mit anderen Daten zusammengeführt.\\n\\n## 3. Cookies, Tracking und externe Dienste\\n\\nDiese Webseite verwendet keine Cookies, kein Tracking und keine Analyse- oder Marketing-Dienste. Es werden keine externen Inhalte Dritter (z. B. Schriftarten, Karten, Videos, CDNs) nachgeladen.\\n\\n## 4. Kontaktaufnahme\\n\\nWenn du uns per E-Mail kontaktierst, verarbeiten wir deine Angaben (z. B. Name, E-Mail-Adresse, Inhalt der Nachricht) ausschließlich zur Bearbeitung deiner Anfrage. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO bzw. Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden gelöscht, sobald sie nicht mehr benötigt werden und keine gesetzlichen Aufbewahrungspflichten entgegenstehen.\\n\\n## 5. Empfänger der Daten\\n\\nEine Weitergabe deiner Daten an Dritte findet nicht statt. Eine Datenübermittlung in Länder außerhalb der EU bzw. des EWR erfolgt nicht.\\n\\n## 6. Deine Rechte\\n\\nDu hast jederzeit das Recht auf:\\n- Auskunft über die zu deiner Person gespeicherten Daten (Art. 15 DSGVO)\\n- Berichtigung unrichtiger Daten (Art. 16 DSGVO)\\n- Löschung (Art. 17 DSGVO)\\n- Einschränkung der Verarbeitung (Art. 18 DSGVO)\\n- Datenübertragbarkeit (Art. 20 DSGVO)\\n- Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)\\n\\nZur Ausübung genügt eine formlose Nachricht an die oben genannte E-Mail-Adresse.\\n\\n## 7. Beschwerderecht bei der Aufsichtsbehörde\\n\\nDu hast das Recht, dich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO). Zuständig ist die Aufsichtsbehörde deines Bundeslandes bzw. des Seitenbetreibers: [zuständige Landesdatenschutzbehörde eintragen].\\n\\n## 8. Keine automatisierte Entscheidungsfindung\\n\\nEine automatisierte Entscheidungsfindung einschließlich Profiling nach Art. 22 DSGVO findet nicht statt.\\n\\n---\\n\\n*Hinweis für den Seitenbetreiber: Platzhalter in eckigen Klammern ausfüllen. Aufbewahrungsdauer der Logfiles an deinen Server bzw. Reverse-Proxy anpassen. Sobald externe Dienste oder ein Kontaktformular eingebunden werden, diesen Text ergänzen. Stand: [Datum eintragen].*"), }; var pageRows = new List(); var blockRows = new List(); foreach (var p in pages) { pageRows.Add(new Page { Id = Pid(p.n), Slug = p.Slug, Title = p.Title, Status = PageStatus.Published }); // placeholder Heading per page blockRows.Add(new Block { Id = Bid(p.n), PageId = Pid(p.n), Order = 0, Type = BlockType.Heading, Data = $"{{\"text\":\"{p.Title}\",\"level\":1}}", }); } // abgabetiere (n=3): auto AbgabetiereList as the second block blockRows.Add(new Block { Id = Bid(10), PageId = Pid(3), Order = 1, Type = BlockType.AbgabetiereList, Data = "{\"mode\":\"auto\",\"intro\":\"\"}", }); foreach (var lp in legalPages) { pageRows.Add(new Page { Id = Pid(lp.n), Slug = lp.Slug, Title = lp.Title, Status = PageStatus.Published }); blockRows.Add(new Block { Id = Bid(lp.n), PageId = Pid(lp.n), Order = 0, Type = BlockType.Heading, Data = $"{{\"text\":\"{lp.Title}\",\"level\":1}}" }); blockRows.Add(new Block { Id = Bid(lp.n * 10), PageId = Pid(lp.n), Order = 1, Type = BlockType.RichText, Data = $"{{\"markdown\":\"{lp.BodyMd}\"}}" }); } modelBuilder.Entity().HasData(pageRows); modelBuilder.Entity().HasData(blockRows); modelBuilder.Entity().HasData(new Site { Id = Site.SingletonId, DefaultLocale = "de", NavOrder = pages.Select(p => Pid(p.n)).ToList(), }); } /// /// Seed the colour-variety catalog. Source of truth = Kevin's GEN-2 generated list /// (gerbil-manager-web/src/genetics/colorVarietySeed.generated.json): 73 varieties /// (the 18 frozen-contract names first, then the baseportal.de extension), in stable /// SortOrder. Names are UI filter keys — spelling changes route through god. /// private static void SeedColorVarieties(ModelBuilder modelBuilder) { // CATALOG-RESEED (AR-5 / colorVarietySeed.backend.json): 70 entries, frozen symbols. // ID = array_index + 1 (stable, never changes — live FKs safe). // SortOrder comes from colorVarietySeed.backend.json (decoupled from ID). // New entries appended at end (IDs 67-70: Dilute Algierfuchs/Goldfuchs/Rotfuchs/Polarfuchs). // GUARDRAIL: DO NOT reseed from colorVarietySeed.generated.json — it uses bracket display // notation (e[f]/c[chm]) which breaks CR-11 GenotypePotentiallyMatches() (frozen vs brackets). (string Name, string Genotype, int SortOrder)[] catalog = { // --- C-locus white / partial albino (IDs 1-3) --- ("REW", "AA chch DD EE GG pp spsp rere", 0), ("Hermelin", "aa chch DD EE GG PP spsp rere", 1), ("Himalaya", "AA chch DD EE GG PP spsp rere", 2), // --- Zobel / colourpoint dark (ID 4) --- ("Zobel", "aa cchmcchm DD EE gg PP spsp rere", 3), // --- Schimmel family (IDs 5) --- ("Rotaugenschimmel", "AA CC DD efef GG pp spsp rere", 4), // --- Standard colours (IDs 6-15) --- ("Agouti", "AA CC DD EE GG PP spsp rere", 5), ("Schwarz", "aa CC DD EE GG PP spsp rere", 6), ("Silberagouti","AA CC DD EE gg PP spsp rere", 7), ("Anthrazit", "aa CC DD EE gg PP spsp rere", 8), ("Algierfuchs", "AA CC DD ee GG PP spsp rere", 9), ("Blau", "aa CC dd EE GG PP spsp rere", 10), ("Gold", "AA CC DD EE GG pp spsp rere", 11), ("Platin", "aa CC DD EE GG pp spsp rere", 12), ("Goldfuchs", "AA CC DD ee GG pp spsp rere", 13), ("Rotfuchs", "aa CC DD ee GG pp spsp rere", 14), // --- Dilute (dd) standard (IDs 16-17) --- ("Dilute Gold", "AA CC dd EE GG pp spsp rere", 15), ("Dilute Platin","aa CC dd EE GG pp spsp rere", 16), // --- REW / Apricot / misc C-locus (IDs 18-28) --- ("Altweiss (REW)", "aa CC DD EE gg pp spsp rere", 17), ("Apricot (Blassfuchs)","AA CC DD ee gg pp spsp rere", 18), ("Blaufuchs", "aa CC DD ee gg PP spsp rere", 19), ("C-Separator", "aa CC DD ee gg pp spsp rere", 20), ("Elfenbein", "AA CC DD EE gg pp spsp rere", 21), ("Kohlfuchs", "aa CC DD ee GG PP spsp rere", 22), ("Polarfuchs", "AA CC DD ee gg PP spsp rere", 23), ("Saphir", "aa CC DD EE GG pp spsp rere", 24), ("Orangeschimmel","AA CC DD efef GG PP spsp rere", 25), ("Topas", "AA CC DD EE GG pp spsp rere", 26), ("Platin-Hell", "aa CC DD EE GG pp spsp rere", 27), // --- Dilute (dd) varieties (IDs 29-32) --- ("Dilute Agouti", "AA CC dd EE GG PP spsp rere", 28), ("Dilute Silberagouti","AA CC dd EE gg PP spsp rere", 29), ("Dilute Kohlfuchs", "aa CC dd ee GG PP spsp rere", 30), ("Dilute Anthrazit", "aa CC dd EE gg PP spsp rere", 31), // --- Schimmel / Fuchsschimmel (IDs 33-37) --- ("Silberschimmel", "AA CC DD efef gg PP spsp rere", 36), // GEN-5 (ticket 5826e8e2): *Fuchsschimmel = HET ef/e (internal 'efe'), // not hom ef/ef — a Schimmel-modified Fox. Pure *schimmel stay efef. ("Polarfuchsschimmel", "AA CC DD efe gg PP spsp rere", 37), ("Algierfuchsschimmel", "AA CC DD efe GG PP spsp rere", 38), ("Kohlfuchsschimmel", "aa CC DD efe GG PP spsp rere", 39), ("Blaufuchsschimmel", "aa CC DD efe gg PP spsp rere", 40), // --- Hell variants (IDs 38-48) --- ("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere", 41), ("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere", 42), ("Goldfuchsschimmel", "AA CC DD efe GG pp spsp rere", 43), ("Gold-Hell", "AA CC DD EE GG pp spsp rere", 44), ("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere", 45), ("Rotfuchsschimmel", "aa CC DD efe GG pp spsp rere", 46), ("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere", 47), ("Kohlfuchsschimmel, hell","aa CC DD efe GG PP spsp rere", 48), ("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere", 49), ("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere", 50), ("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere", 51), // --- Dilute (dd) renamed variants (IDs 49-50) --- ("Dilute Topas", "AA CC dd EE GG pp spsp rere", 52), // GEN-5 (ticket 3deab547): Blaufuchs is black-eyed; dilution is P-independent. ("Dilute Blaufuchs","aa CC dd ee gg PP spsp rere", 53), // --- Marder / Siam / CP- series (IDs 51-66) --- ("Marder", "aa cchmcchm DD EE GG PP spsp rere", 54), ("Siam", "aa cchmch DD EE GG PP spsp rere", 55), ("Zobel-Hell","aa cchmch DD EE gg PP spsp rere", 56), ("CP-Agouti", "AA cchmcchm DD EE GG PP spsp rere", 57), ("CP-Silberagouti","AA cchmcchm DD EE gg PP spsp rere", 59), ("CP-Algierfuchs", "AA cchmcchm DD ee GG PP spsp rere", 61), ("CP-Polarfuchs", "AA cchmcchm DD ee gg PP spsp rere", 63), ("CP-Fuchs", "AA cchmcchm dd ee GG PP spsp rere", 65), ("CP-Fuchs-Hell", "AA cchmch dd ee GG PP spsp rere", 66), ("CP-Blaufuchs", "AA cchmcchm dd ee gg PP spsp rere", 67), ("CP-Orangeschimmel","AA cchmcchm DD efef GG PP spsp rere", 68), // GEN-3g / CATALOG-RESEED: CP-*-Hell interleaved (IDs 62-66, SortOrders from backend.json) ("CP-Agouti-Hell", "AA cchmch DD EE GG PP spsp rere", 58), ("CP-Silberagouti-Hell", "AA cchmch DD EE gg PP spsp rere", 60), ("CP-Algierfuchs-Hell", "AA cchmch DD ee GG PP spsp rere", 62), ("CP-Polarfuchs-Hell", "AA cchmch DD ee gg PP spsp rere", 64), ("CP-Orangeschimmel-Hell","AA cchmch DD efef GG PP spsp rere", 69), // CATALOG-RESEED: 4 new Dilute (dd) Fuchs varieties appended (IDs 67-70) ("Dilute Algierfuchs","AA CC dd ee GG PP spsp rere", 32), ("Dilute Goldfuchs", "AA CC dd ee GG pp spsp rere", 33), ("Dilute Rotfuchs", "aa CC dd ee GG pp spsp rere", 34), ("Dilute Polarfuchs", "AA CC dd ee gg PP spsp rere", 35), }; var rows = new ColorVariety[catalog.Length]; for (int i = 0; i < catalog.Length; i++) { rows[i] = new ColorVariety { // Stable, deterministic GUIDs — ID = array_index + 1 (never reorder existing entries). Id = new Guid($"00000000-0000-0000-0000-{(i + 1):D12}"), Name = catalog[i].Name, CanonicalGenotype = catalog[i].Genotype, SortOrder = catalog[i].SortOrder, }; } modelBuilder.Entity().HasData(rows); } }