Source: gerbil-manager-web/src/genetics/colorVarietySeed.backend.json (70 entries, frozen symbols). - 8 names updated to "Dilute" prefix (dd Gold→Dilute Gold, Agouti dd→Dilute Agouti, etc.) - SortOrder decoupled from ID (CP-*-Hell interleaved per backend.json sortOrder values) - 4 new entries appended at end (IDs 67-70): Dilute Algierfuchs/Goldfuchs/Rotfuchs/Polarfuchs - All 66 existing ID→Name bindings preserved (append-only, no FK drift) - Migration ReseedColorVarietiesAR5: UpdateData (renames+SortOrders) + InsertData (4 new) - Tests updated: SeedGen3g_* asserts 70 entries + IDs 67-70 verified - GATE: 157/157 C# tests, has-pending-model-changes=No Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
393 lines
21 KiB
C#
393 lines
21 KiB
C#
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
public class ApplicationContext : DbContext
|
|
{
|
|
public ApplicationContext(DbContextOptions options) : base(options)
|
|
{
|
|
}
|
|
|
|
public DbSet<Gerbil> Gerbils => Set<Gerbil>();
|
|
public DbSet<Litter> Litters => Set<Litter>();
|
|
public DbSet<Contact> Contacts => Set<Contact>();
|
|
public DbSet<Enclosure> Enclosures => Set<Enclosure>();
|
|
public DbSet<ColorVariety> ColorVarieties => Set<ColorVariety>();
|
|
public DbSet<GerbilPhoto> GerbilPhotos => Set<GerbilPhoto>();
|
|
public DbSet<HealthRecord> HealthRecords => Set<HealthRecord>();
|
|
public DbSet<WeightRecord> WeightRecords => Set<WeightRecord>();
|
|
public DbSet<SaleContract> SaleContracts => Set<SaleContract>();
|
|
public DbSet<BreederSettings> BreederSettings => Set<BreederSettings>();
|
|
public DbSet<Site> Sites => Set<Site>();
|
|
public DbSet<Page> Pages => Set<Page>();
|
|
public DbSet<Block> Blocks => Set<Block>();
|
|
public DbSet<Media> Media => Set<Media>();
|
|
public DbSet<Request> Requests => Set<Request>();
|
|
public DbSet<MailSettings> MailSettings => Set<MailSettings>();
|
|
|
|
// 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<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
|
{
|
|
SyncNameSearch();
|
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
}
|
|
|
|
private void SyncNameSearch()
|
|
{
|
|
foreach (var entry in ChangeTracker.Entries<Gerbil>())
|
|
{
|
|
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<Gerbil>(e =>
|
|
{
|
|
// Enums persisted as their string names (readable, Gridify-friendly).
|
|
e.Property(g => g.Gender).HasConversion<string>();
|
|
e.Property(g => g.Status).HasConversion<string>();
|
|
|
|
// Residency defaults to true (own stock unless explicitly marked external).
|
|
e.Property(g => g.IsResident).HasDefaultValue(true);
|
|
|
|
// 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<List<string>, string>(
|
|
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
|
v => string.IsNullOrEmpty(v)
|
|
? new List<string>()
|
|
: System.Text.Json.JsonSerializer.Deserialize<List<string>>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List<string>());
|
|
var traitsComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer<List<string>>(
|
|
(a, b) => (a ?? new List<string>()).SequenceEqual(b ?? new List<string>()),
|
|
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<Litter>(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");
|
|
});
|
|
|
|
modelBuilder.Entity<HealthRecord>(e =>
|
|
{
|
|
e.Property(h => h.Type).HasConversion<string>();
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(h => h.GerbilId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
|
|
modelBuilder.Entity<WeightRecord>(e =>
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(w => w.GerbilId).OnDelete(DeleteBehavior.Cascade));
|
|
|
|
modelBuilder.Entity<GerbilPhoto>(e =>
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(p => p.GerbilId).OnDelete(DeleteBehavior.Cascade));
|
|
|
|
// FEAT-13: Abgabeverträge + Zuchtprofil.
|
|
modelBuilder.Entity<SaleContract>(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<SaleContractAnimal>(e =>
|
|
{
|
|
e.HasKey(a => new { a.SaleContractId, a.GerbilId });
|
|
e.HasOne<SaleContract>().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<BreederSettings>()
|
|
.HasData(new BreederSettings { Id = GerbilManagerWebAPI.Models.BreederSettings.SingletonId });
|
|
|
|
// WEB epic: CMS content model.
|
|
modelBuilder.Entity<Site>(e =>
|
|
{
|
|
var navConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<Guid>, string>(
|
|
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
|
v => string.IsNullOrEmpty(v)
|
|
? new List<Guid>()
|
|
: System.Text.Json.JsonSerializer.Deserialize<List<Guid>>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List<Guid>());
|
|
var navComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer<List<Guid>>(
|
|
(a, b) => (a ?? new List<Guid>()).SequenceEqual(b ?? new List<Guid>()),
|
|
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<Page>(e =>
|
|
{
|
|
e.HasIndex(p => p.Slug).IsUnique();
|
|
e.Property(p => p.Status).HasConversion<string>();
|
|
e.HasMany(p => p.Blocks).WithOne()
|
|
.HasForeignKey(b => b.PageId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
|
|
modelBuilder.Entity<Block>(e => e.Property(b => b.Type).HasConversion<string>());
|
|
|
|
SeedCms(modelBuilder);
|
|
|
|
// INBOX epic: Gmail request inbox.
|
|
modelBuilder.Entity<Request>(e =>
|
|
{
|
|
e.Property(r => r.Status).HasConversion<string>();
|
|
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<MailSettings>()
|
|
.HasData(new MailSettings { Id = GerbilManagerWebAPI.Models.MailSettings.SingletonId });
|
|
|
|
// DB-4: German collation on remaining searched/sorted text columns (Npgsql-only).
|
|
if (isNpgsql)
|
|
{
|
|
modelBuilder.Entity<ColorVariety>()
|
|
.Property(v => v.Name).UseCollation(DeIcu);
|
|
modelBuilder.Entity<Contact>()
|
|
.Property(c => c.Name).UseCollation(DeIcu);
|
|
}
|
|
|
|
SeedColorVarieties(modelBuilder);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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äß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"),
|
|
(8, "datenschutz", "Datenschutz",
|
|
"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*"),
|
|
};
|
|
|
|
var pageRows = new List<Page>();
|
|
var blockRows = new List<Block>();
|
|
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<Page>().HasData(pageRows);
|
|
modelBuilder.Entity<Block>().HasData(blockRows);
|
|
|
|
modelBuilder.Entity<Site>().HasData(new Site
|
|
{
|
|
Id = Site.SingletonId,
|
|
DefaultLocale = "de",
|
|
NavOrder = pages.Select(p => Pid(p.n)).ToList(),
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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) ---
|
|
("Pink Eyed White (PEW)", "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),
|
|
("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere", 37),
|
|
("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere", 38),
|
|
("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere", 39),
|
|
("Blaufuchsschimmel", "aa CC DD efef 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 efef 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 efef GG pp spsp rere", 46),
|
|
("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere", 47),
|
|
("Kohlfuchsschimmel, hell","aa CC DD efef 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),
|
|
("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<ColorVariety>().HasData(rows);
|
|
}
|
|
}
|