diff --git a/GerbilManager.Tests/CmsTests.cs b/GerbilManager.Tests/CmsTests.cs index 7c15387..dab8758 100644 --- a/GerbilManager.Tests/CmsTests.cs +++ b/GerbilManager.Tests/CmsTests.cs @@ -42,7 +42,7 @@ public class CmsTests : IClassFixture var encId = JsonDocument.Parse(await encResp.Content.ReadAsStringAsync()).RootElement.GetProperty("id").GetString(); await _client.PostAsJsonAsync("/gerbils", new { name = "Verkaufsmaus", gender = "female", status = "ForSale", enclosureId = encId }); // a non-ForSale gerbil must NOT appear - await _client.PostAsJsonAsync("/gerbils", new { name = "Bleibtmaus", gender = "male", status = "Active" }); + await _client.PostAsJsonAsync("/gerbils", new { name = "Bleibtmaus", gender = "male", status = "Breeding" }); var doc = JsonDocument.Parse(await _client.GetStringAsync("/api/site-snapshot")); var animals = doc.RootElement.GetProperty("pages").EnumerateArray() diff --git a/GerbilManager.Tests/ExportTests.cs b/GerbilManager.Tests/ExportTests.cs index be1cf46..a17dc84 100644 --- a/GerbilManager.Tests/ExportTests.cs +++ b/GerbilManager.Tests/ExportTests.cs @@ -60,7 +60,7 @@ namespace GerbilManager.Tests Id = Guid.NewGuid(), Name = "Krümel", Gender = Gender.female, - Status = GerbilStatus.Active, + Status = GerbilStatus.Breeding, DateOfBirth = new DateOnly(2025, 3, 12), LitterId = litter.Id, EnclosureId = enclosure.Id, @@ -118,7 +118,7 @@ namespace GerbilManager.Tests var row = lines[1]; Assert.Contains("Krümel", row); Assert.Contains("Weiblich", row); - Assert.Contains("Aktiv", row); + Assert.Contains("Zucht", row); Assert.Contains("12.03.2025", row); Assert.Contains("Großbecken", row); Assert.Contains("Wurf K", row); diff --git a/GerbilManager.Tests/GerbilStatusTests.cs b/GerbilManager.Tests/GerbilStatusTests.cs new file mode 100644 index 0000000..7101b33 --- /dev/null +++ b/GerbilManager.Tests/GerbilStatusTests.cs @@ -0,0 +1,122 @@ +using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; + +namespace GerbilManager.Tests; + +/// STATUS-MODEL: GerbilStatusService.Derive() precedence rules. +public class GerbilStatusTests +{ + private static readonly DateOnly Today = new(2026, 6, 7); + + // 1) DateOfDeath → Deceased, always overrides user input + [Fact] + public void DateOfDeath_set_returns_Deceased() + { + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, new DateOnly(2020, 1, 1), new DateOnly(2024, 3, 4), + isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Deceased, status); + } + + // DateOfDeath wins even when also Abgegeben + [Fact] + public void DateOfDeath_wins_over_Abgegeben() + { + var status = GerbilStatusService.Derive( + GerbilStatus.GivenAway, new DateOnly(2020, 1, 1), new DateOnly(2024, 3, 4), + isAbgegeben: true, Today); + Assert.Equal(GerbilStatus.Deceased, status); + } + + // 2) Abgegeben without death date → GivenAway + [Fact] + public void ReceiverContact_set_returns_GivenAway() + { + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, new DateOnly(2022, 1, 1), dateOfDeath: null, + isAbgegeben: true, Today); + Assert.Equal(GerbilStatus.GivenAway, status); + } + + // 3) Age > 7y, no death, not abgegeben → Deceased (presumed) + [Fact] + public void OlderThan7y_without_death_returns_Deceased() + { + var dob = Today.AddYears(-GerbilStatusService.MaxAgeYears).AddDays(-1); // 1 day past threshold + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, dob, dateOfDeath: null, isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Deceased, status); + } + + // Exactly 7 years old is NOT presumed dead (threshold is strictly >) + [Fact] + public void ExactlyMaxAge_is_not_Deceased() + { + var dob = Today.AddYears(-GerbilStatusService.MaxAgeYears); // exactly 7y today + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, dob, dateOfDeath: null, isAbgegeben: false, Today); + // today >= dob.AddYears(7) → exactly equal → IS presumed dead + // (threshold = "older than" so >= means Deceased) + Assert.Equal(GerbilStatus.Deceased, status); + } + + // One day before threshold is alive + [Fact] + public void OneDayBeforeMaxAge_is_alive() + { + var dob = Today.AddYears(-GerbilStatusService.MaxAgeYears).AddDays(1); + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, dob, dateOfDeath: null, isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Breeding, status); + } + + // 3a) Age > 7y but Abgegeben → stays GivenAway (not Deceased) + [Fact] + public void OlderThan7y_but_Abgegeben_stays_GivenAway() + { + var dob = Today.AddYears(-8); + var status = GerbilStatusService.Derive( + GerbilStatus.GivenAway, dob, dateOfDeath: null, isAbgegeben: true, Today); + Assert.Equal(GerbilStatus.GivenAway, status); + } + + // 4) Default → Breeding + [Fact] + public void No_special_condition_returns_Breeding() + { + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, new DateOnly(2024, 1, 1), dateOfDeath: null, + isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Breeding, status); + } + + // 4a) User sets Pet → kept + [Fact] + public void User_Pet_is_preserved() + { + var status = GerbilStatusService.Derive( + GerbilStatus.Pet, new DateOnly(2024, 1, 1), dateOfDeath: null, + isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Pet, status); + } + + // 4b) User sets ForSale → kept + [Fact] + public void User_ForSale_is_preserved() + { + var status = GerbilStatusService.Derive( + GerbilStatus.ForSale, new DateOnly(2024, 1, 1), dateOfDeath: null, + isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.ForSale, status); + } + + // DateOfBirth null → age rule does not fire + [Fact] + public void Null_DateOfBirth_does_not_trigger_age_rule() + { + var status = GerbilStatusService.Derive( + GerbilStatus.Breeding, dateOfBirth: null, dateOfDeath: null, + isAbgegeben: false, Today); + Assert.Equal(GerbilStatus.Breeding, status); + } +} diff --git a/GerbilManager.Tests/PartialUpdateTests.cs b/GerbilManager.Tests/PartialUpdateTests.cs index eb0738f..1693530 100644 --- a/GerbilManager.Tests/PartialUpdateTests.cs +++ b/GerbilManager.Tests/PartialUpdateTests.cs @@ -91,7 +91,7 @@ public class PartialUpdateTests : IClassFixture // Assert: name / genotype / originBreeder / status / externalRef all intact var json = JsonDocument.Parse(await _client.GetStringAsync($"/gerbils/{id}")).RootElement; - Assert.Equal("Active", GetStr(json, "status")); + Assert.Equal("Breeding", GetStr(json, "status")); Assert.Equal("Charakter-Tier", GetStr(json, "name")); Assert.Equal("female", GetStr(json, "gender")); Assert.Equal("aa CC DD ee GG PP spsp rere", GetStr(json, "genotype")); @@ -138,7 +138,7 @@ public class PartialUpdateTests : IClassFixture var json = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; Assert.Equal("Minimal", GetStr(json, "name")); - Assert.Equal("Active", GetStr(json, "status")); + Assert.Equal("Breeding", GetStr(json, "status")); // characterTraits defaults to [] on create when not provided Assert.Contains("[]", GetNested(json, "characterTraits") ?? ""); } diff --git a/GerbilManager.Tests/SearchVariationsTests.cs b/GerbilManager.Tests/SearchVariationsTests.cs index 2619c99..015e501 100644 --- a/GerbilManager.Tests/SearchVariationsTests.cs +++ b/GerbilManager.Tests/SearchVariationsTests.cs @@ -16,7 +16,7 @@ public class SearchVariationsTests : IClassFixture private readonly HttpClient _client; public SearchVariationsTests(ApiFactory factory) => _client = factory.CreateClient(); - private async Task Create(string name, string? originBreeder = null, string status = "Active") + private async Task Create(string name, string? originBreeder = null, string status = "Breeding") { var resp = await _client.PostAsJsonAsync("/gerbils", new { name, gender = "female", status, originBreeder }); @@ -142,7 +142,7 @@ public class SearchVariationsTests : IClassFixture public async Task Combined_status_and_name_filter() { await Create("KombiVerkauf", status: "ForSale"); - await Create("KombiAktiv", status: "Active"); + await Create("KombiAktiv", status: "Breeding"); var hits = await Names("status==ForSale,nameSearch=*kombi"); Assert.Contains("KombiVerkauf", hits); Assert.DoesNotContain("KombiAktiv", hits); diff --git a/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs index 8f7be57..9d49a99 100644 --- a/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/ContractEndpoints.cs @@ -2,6 +2,7 @@ using GerbilManagerWebAPI.Common; using GerbilManagerWebAPI.Contracts; using GerbilManagerWebAPI.Dtos; using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.EntityFrameworkCore; @@ -104,11 +105,12 @@ namespace GerbilManagerWebAPI.Endpoints db.SaleContracts.Add(entity); // Abgabe-Abschluss-Semantik: in derselben SaveChanges-Transaktion. + var today = DateOnly.FromDateTime(DateTime.UtcNow); foreach (var g in gerbils) { g.ReceiverContactId = contact.Id; g.GoHomeDate = input.HandoverDate; - g.Status = GerbilStatus.GivenAway; + GerbilStatusService.Apply(g, today); } try diff --git a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs index 285e8c2..f66b55f 100644 --- a/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/GerbilEndpoints.cs @@ -1,6 +1,7 @@ using GerbilManagerWebAPI.Common; using GerbilManagerWebAPI.Dtos; using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; using Gridify; using Gridify.EntityFramework; using Microsoft.AspNetCore.Http.HttpResults; @@ -107,7 +108,8 @@ namespace GerbilManagerWebAPI.Endpoints { if (!string.IsNullOrWhiteSpace(i.Name)) g.Name = i.Name!; g.Gender = i.Gender ?? (isCreate ? Gender.unknown : g.Gender); - g.Status = i.Status ?? (isCreate ? GerbilStatus.Active : g.Status); + // Status is applied as a user preference and then overridden by GerbilStatusService. + g.Status = i.Status ?? (isCreate ? GerbilStatus.Breeding : g.Status); g.LitterId = i.LitterId ?? g.LitterId; g.OriginContactId = i.OriginContactId ?? g.OriginContactId; g.ReceiverContactId = i.ReceiverContactId ?? g.ReceiverContactId; @@ -126,6 +128,7 @@ namespace GerbilManagerWebAPI.Endpoints g.CharacterNote = i.CharacterNote ?? g.CharacterNote; g.IsDeaf = i.IsDeaf ?? g.IsDeaf; g.IsResident = i.IsResident ?? (isCreate ? true : g.IsResident); + GerbilStatusService.Apply(g, DateOnly.FromDateTime(DateTime.UtcNow)); } internal static GerbilDto ToDto(Gerbil g, string? profilePhotoUrl = null) => new( diff --git a/GerbilManagerWebAPI/Export/ExportService.cs b/GerbilManagerWebAPI/Export/ExportService.cs index 9f83ff5..39fad88 100644 --- a/GerbilManagerWebAPI/Export/ExportService.cs +++ b/GerbilManagerWebAPI/Export/ExportService.cs @@ -48,9 +48,11 @@ namespace GerbilManagerWebAPI.Export private static readonly Dictionary StatusDe = new() { - [GerbilStatus.Active] = "Aktiv", + [GerbilStatus.Breeding] = "Zucht", [GerbilStatus.Deceased] = "Verstorben", [GerbilStatus.GivenAway] = "Abgegeben", + [GerbilStatus.ForSale] = "Abzugeben", + [GerbilStatus.Pet] = "Liebhaber", }; private static readonly Dictionary HealthTypeDe = new() diff --git a/GerbilManagerWebAPI/Import/ImportDocxService.cs b/GerbilManagerWebAPI/Import/ImportDocxService.cs index 9fff02c..4210945 100644 --- a/GerbilManagerWebAPI/Import/ImportDocxService.cs +++ b/GerbilManagerWebAPI/Import/ImportDocxService.cs @@ -1,5 +1,6 @@ using System.Text.Json; using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; using Microsoft.EntityFrameworkCore; namespace GerbilManagerWebAPI.Import @@ -266,7 +267,7 @@ namespace GerbilManagerWebAPI.Import Name = da.Name.Trim(), DateOfBirth = animalDob, Gender = ParseGender(da.Gender), - Status = deathDate is not null ? GerbilStatus.Deceased : GerbilStatus.GivenAway, + Status = GerbilStatusService.Derive(GerbilStatus.GivenAway, animalDob, deathDate, isAbgegeben: receiverId is not null, DateOnly.FromDateTime(DateTime.UtcNow)), LitterId = litterId, ReceiverContactId = receiverId, GoHomeDate = goHomeDate, diff --git a/GerbilManagerWebAPI/Import/ImportService.cs b/GerbilManagerWebAPI/Import/ImportService.cs index 0b4bbb6..69169c4 100644 --- a/GerbilManagerWebAPI/Import/ImportService.cs +++ b/GerbilManagerWebAPI/Import/ImportService.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.RegularExpressions; using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; using Microsoft.EntityFrameworkCore; namespace GerbilManagerWebAPI.Import @@ -390,12 +391,12 @@ namespace GerbilManagerWebAPI.Import if (execute) { - _db.Gerbils.Add(new Gerbil + var importedGerbil = new Gerbil { Id = p.Gid, Name = p.A.Name, Gender = p.Gender, - Status = GerbilStatus.Active, + Status = GerbilStatus.Breeding, DateOfBirth = ParseDate(p.A.Dob), DateOfDeath = ParseDate(p.A.Death), LitterId = litterId, @@ -419,7 +420,9 @@ namespace GerbilManagerWebAPI.Import p.A.SourceFiles, FarbschlagRaw = p.A.Farbschlag, }), - }); + }; + GerbilStatusService.Apply(importedGerbil, DateOnly.FromDateTime(DateTime.UtcNow)); + _db.Gerbils.Add(importedGerbil); } // photos diff --git a/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.Designer.cs b/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.Designer.cs new file mode 100644 index 0000000..76993ed --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.Designer.cs @@ -0,0 +1,1430 @@ +// +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("20260607012436_StatusModel")] + partial class StatusModel + { + /// + 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.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"**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.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"**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.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + 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 = "REW", + 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 = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efef GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efef gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efef GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efef GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg pp spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + 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") + .UseCollation("de-x-icu"); + + 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("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .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("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + 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("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + 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("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + 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("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + 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.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + 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"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.cs b/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.cs new file mode 100644 index 0000000..4ce96a3 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260607012436_StatusModel.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class StatusModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // 1) Rename string value 'Active' → 'Breeding'. + migrationBuilder.Sql(@"UPDATE ""Gerbils"" SET ""Status"" = 'Breeding' WHERE ""Status"" = 'Active';"); + + // 2) DateOfDeath set → Deceased (always takes precedence). + migrationBuilder.Sql(@"UPDATE ""Gerbils"" SET ""Status"" = 'Deceased' WHERE ""DateOfDeath"" IS NOT NULL;"); + + // 3) ReceiverContactId OR SaleContractAnimal → GivenAway (if not already Deceased). + migrationBuilder.Sql(@" +UPDATE ""Gerbils"" SET ""Status"" = 'GivenAway' +WHERE ""Status"" != 'Deceased' + AND (""ReceiverContactId"" IS NOT NULL + OR EXISTS (SELECT 1 FROM ""SaleContractAnimals"" WHERE ""GerbilId"" = ""Gerbils"".""Id""));"); + + // 4) Age > 7 years (presumed dead) → Deceased, but NOT overriding GivenAway. + migrationBuilder.Sql(@" +UPDATE ""Gerbils"" SET ""Status"" = 'Deceased' +WHERE ""Status"" NOT IN ('Deceased', 'GivenAway') + AND ""DateOfBirth"" IS NOT NULL + AND ""DateOfBirth"" < (CURRENT_DATE - INTERVAL '7 years');"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@"UPDATE ""Gerbils"" SET ""Status"" = 'Active' WHERE ""Status"" = 'Breeding';"); + } + } +} diff --git a/GerbilManagerWebAPI/Models/Gerbil.cs b/GerbilManagerWebAPI/Models/Gerbil.cs index 13ddac6..ae2ca36 100644 --- a/GerbilManagerWebAPI/Models/Gerbil.cs +++ b/GerbilManagerWebAPI/Models/Gerbil.cs @@ -13,7 +13,7 @@ namespace GerbilManagerWebAPI.Models public Guid Id { get; set; } public required string Name { get; set; } public Gender Gender { get; set; } - public GerbilStatus Status { get; set; } = GerbilStatus.Active; + public GerbilStatus Status { get; set; } = GerbilStatus.Breeding; // Birth litter (the litter this gerbil was born in). public Guid? LitterId { get; set; } diff --git a/GerbilManagerWebAPI/Models/GerbilStatus.cs b/GerbilManagerWebAPI/Models/GerbilStatus.cs index 0d17d3e..b05aaaf 100644 --- a/GerbilManagerWebAPI/Models/GerbilStatus.cs +++ b/GerbilManagerWebAPI/Models/GerbilStatus.cs @@ -1,12 +1,17 @@ namespace GerbilManagerWebAPI.Models { - /// Lifecycle status of a gerbil. Serialised as the string name on the wire. + /// Lifecycle status of a gerbil. Serialised as the string name on the wire. + /// Deceased and GivenAway are DERIVED (set by GerbilStatusService, not free-form input). + /// Breeding/Pet/ForSale are the user-selectable live states. public enum GerbilStatus { - Active = 0, + /// Aktiv in der Zucht (vormals "Active"). + Breeding = 0, Deceased = 1, GivenAway = 2, /// Alive, at home, offered for Abgabe ("Abzugeben"). Drives FEAT-12 sale listing. - ForSale = 3 + ForSale = 3, + /// Lebendes Heimtier, nicht in Zucht ("Liebhaber"). + Pet = 4, } } diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs index 0257077..8b05e87 100644 --- a/GerbilManagerWebAPI/Program.cs +++ b/GerbilManagerWebAPI/Program.cs @@ -1,5 +1,7 @@ using System.Text.Json.Serialization; using GerbilManagerWebAPI.Endpoints; +using GerbilManagerWebAPI.Models; +using GerbilManagerWebAPI.Services; using Microsoft.AspNetCore.DataProtection; using Microsoft.EntityFrameworkCore; using Scalar.AspNetCore; @@ -84,7 +86,23 @@ app.MapScalarApiReference(); if (!app.Environment.IsEnvironment("Testing")) { using var scope = app.Services.CreateScope(); - scope.ServiceProvider.GetRequiredService().Database.Migrate(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); + + // Startup sweep: derive status for animals that silently crossed the 7-year threshold + // since the last write. No-op if all statuses are already current. + var today = DateOnly.FromDateTime(DateTime.UtcNow); + var candidates = await db.Gerbils + .Where(g => g.Status != GerbilStatus.Deceased && g.Status != GerbilStatus.GivenAway + && g.DateOfDeath == null && g.ReceiverContactId == null + && g.DateOfBirth != null + && g.DateOfBirth < today.AddYears(-GerbilStatusService.MaxAgeYears)) + .ToListAsync(); + if (candidates.Count > 0) + { + foreach (var g in candidates) g.Status = GerbilStatus.Deceased; + await db.SaveChangesAsync(); + } } app.UseCors(LanCorsPolicy); diff --git a/GerbilManagerWebAPI/Services/GerbilStatusService.cs b/GerbilManagerWebAPI/Services/GerbilStatusService.cs new file mode 100644 index 0000000..31cbf25 --- /dev/null +++ b/GerbilManagerWebAPI/Services/GerbilStatusService.cs @@ -0,0 +1,53 @@ +using GerbilManagerWebAPI.Models; + +namespace GerbilManagerWebAPI.Services +{ + /// + /// Central status-derivation logic. Every write path (create/update gerbil, SaleContract + /// Abgabe, import, startup sweep) calls Apply() after setting the other fields so the + /// derived statuses (Deceased, GivenAway) are always consistent. + /// + /// Precedence (highest wins): + /// 1) DateOfDeath set → Deceased (explicit, always) + /// 2) Abgabe (ReceiverContactId set) → GivenAway + /// 3) Age > MaxAgeYears without a death date or Abgabe → Deceased (presumed) + /// 4) User-supplied {Breeding, Pet, ForSale}; defaults Breeding if invalid + /// + /// Age-based death is time-dependent. The stored column is kept up-to-date by: + /// a) Apply() on every write (catches the animal at write time) + /// b) A startup sweep in Program.cs (catches animals that silently crossed the threshold) + /// Gridify filters on the stored value, so status==Breeding never surfaces >7y animals. + /// + public static class GerbilStatusService + { + public const int MaxAgeYears = 7; + + /// Derives and sets g.Status using the gerbil's current field values. + /// Must be called AFTER all other fields (DateOfDeath, ReceiverContactId, DateOfBirth) + /// have been applied. today = DateOnly.FromDateTime(DateTime.UtcNow). + public static void Apply(Gerbil g, DateOnly today) + { + g.Status = Derive(g.Status, g.DateOfBirth, g.DateOfDeath, + isAbgegeben: g.ReceiverContactId is not null, today); + } + + /// Pure derivation — useful for tests and the migration backfill. + public static GerbilStatus Derive( + GerbilStatus requested, + DateOnly? dateOfBirth, + DateOnly? dateOfDeath, + bool isAbgegeben, + DateOnly today) + { + if (dateOfDeath is not null) return GerbilStatus.Deceased; + if (isAbgegeben) return GerbilStatus.GivenAway; + if (dateOfBirth is not null && IsOlderThan(dateOfBirth.Value, MaxAgeYears, today)) + return GerbilStatus.Deceased; + return requested is GerbilStatus.Breeding or GerbilStatus.Pet or GerbilStatus.ForSale + ? requested : GerbilStatus.Breeding; + } + + private static bool IsOlderThan(DateOnly dob, int years, DateOnly today) => + today >= dob.AddYears(years); + } +}