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 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(); // 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) { modelBuilder.Entity(e => { // Enums persisted as their string names (readable, Gridify-friendly). e.Property(g => g.Gender).HasConversion(); e.Property(g => g.Status).HasConversion(); // 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); }); 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); }); 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)); // 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 }); 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"), }; 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\":\"\"}", }); 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) { (string Name, string Genotype)[] catalog = { ("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere"), ("Hermelin", "aa chch DD EE GG PP spsp rere"), ("Himalaya", "AA chch DD EE GG PP spsp rere"), ("Zobel", "aa cchmcchm DD EE gg PP spsp rere"), ("Schwarzschimmel", "AA CC DD efef GG PP spsp rere"), ("Rotaugenschimmel", "AA CC DD efef GG pp spsp rere"), ("Agouti", "AA CC DD EE GG PP spsp rere"), ("Schwarz", "aa CC DD EE GG PP spsp rere"), ("Silberagouti", "AA CC DD EE gg PP spsp rere"), ("Anthrazit", "aa CC DD EE gg PP spsp rere"), ("Algierfuchs", "AA CC DD ee GG PP spsp rere"), ("Blau", "aa CC dd EE GG PP spsp rere"), ("Gold", "AA CC DD EE GG pp spsp rere"), ("Platin", "aa CC DD EE GG pp spsp rere"), ("Goldfuchs", "AA CC DD ee GG pp spsp rere"), ("Rotfuchs", "aa CC DD ee GG pp spsp rere"), ("dd Gold", "AA CC dd EE GG pp spsp rere"), ("dd Platin", "aa CC dd EE GG pp spsp rere"), ("Altweiss (REW)", "aa CC DD EE gg pp spsp rere"), ("Apricot (Blassfuchs)", "AA CC DD ee gg pp spsp rere"), ("Blaufuchs", "aa CC DD ee gg PP spsp rere"), ("C-Separator", "aa CC DD ee gg pp spsp rere"), ("Elfenbein", "AA CC DD EE gg pp spsp rere"), ("Kohlfuchs", "aa CC DD ee GG PP spsp rere"), ("Marder", "aa cchmcchm DD EE GG PP spsp rere"), ("Siam (Marder-Hell)", "aa cchmcchm DD EE GG PP spsp rere"), ("Polarfuchs", "AA CC DD ee gg PP spsp rere"), ("Saphir", "aa CC DD EE GG pp spsp rere"), ("Schimmel (Orangeschimmel)", "AA CC DD efef GG PP spsp rere"), ("Topas", "AA CC DD EE GG pp spsp rere"), ("Platin-Hell", "aa CC DD EE GG pp spsp rere"), ("Agouti dd", "AA CC dd EE GG PP spsp rere"), ("Silberagouti dd", "AA CC dd EE gg PP spsp rere"), ("Kohlfuchs dd", "aa CC dd ee GG PP spsp rere"), ("Anthrazit dd", "aa CC dd EE gg PP spsp rere"), ("Agouti CP-Hell", "AA cchmcchm DD EE GG PP spsp rere"), ("Blaufuchs CP", "aa cchmcchm DD ee gg PP spsp rere"), ("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere"), ("Silberschimmel", "AA CC DD efef gg PP spsp rere"), ("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere"), ("Polarfuchs-Hell CP", "AA cchmcchm DD ee gg PP spsp rere"), ("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere"), ("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere"), ("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere"), ("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere"), ("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere"), ("Gold-Hell", "AA CC DD EE GG pp spsp rere"), ("Siam (Marder-Hell) dd", "aa cchmcchm dd EE GG PP spsp rere"), ("Marder dd", "aa cchmcchm dd EE GG PP spsp rere"), ("Zobel-Hell", "aa cchmcchm DD EE gg PP spsp rere"), ("Silberagouti dd CP", "AA cchmcchm dd EE gg PP spsp rere"), ("Silberagouti-Hell dd CP", "AA cchmcchm dd EE gg PP spsp rere"), ("Agouti dd CP", "AA cchmcchm dd EE GG PP spsp rere"), ("Agouti-Hell dd CP", "AA cchmcchm dd EE GG PP spsp rere"), ("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere"), ("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere"), ("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere"), ("Kohlfuchsschimmel, hell", "aa CC DD efef GG PP spsp rere"), ("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere"), ("Zobel dd", "aa cchmcchm dd EE gg PP spsp rere"), ("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere"), ("Kohlfuchs CP", "aa cchmcchm DD ee GG PP spsp rere"), ("Algierfuchs CP", "AA cchmcchm DD ee GG PP spsp rere"), ("Silberagouti CP", "AA cchmcchm DD EE gg PP spsp rere"), ("Agouti CP", "AA cchmcchm DD EE GG PP spsp rere"), ("Algierfuchs-Hell CP", "AA cchmcchm DD ee GG PP spsp rere"), ("Kohlfuchs,hell CP", "aa cchmcchm DD ee GG PP spsp rere"), ("Polarfuchs CP", "AA cchmcchm DD ee gg PP spsp rere"), ("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere"), ("Topas dd", "AA CC dd EE GG pp spsp rere"), ("Zobel-Hell dd", "aa cchmcchm dd EE gg PP spsp rere"), ("Kohlfuchsschimmel CP", "aa cchmcchm DD efef GG PP spsp rere"), ("Blaufuchs dd", "aa CC dd ee gg PP spsp rere"), }; var rows = new ColorVariety[catalog.Length]; for (int i = 0; i < catalog.Length; i++) { rows[i] = new ColorVariety { // Stable, deterministic GUIDs so the HasData seed is migration-stable. Id = new Guid($"00000000-0000-0000-0000-{(i + 1):D12}"), Name = catalog[i].Name, CanonicalGenotype = catalog[i].Genotype, SortOrder = i, }; } modelBuilder.Entity().HasData(rows); } }