LITTER-MORTALITY: DeathsWithin8Weeks field (Frühsterblichkeit)

Litter.DeathsWithin8Weeks (int?, nullable). LitterDto + LitterInput extended.
Validation: negative → 400, > TotalBorn → 400 (skipped when TotalBorn null).
Migration AddLitterMortality (additive, nullable). 5 new tests. 194/194 green.
Contract: camelCase field deathsWithin8Weeks in LitterDto/LitterInput.
This commit is contained in:
2026-06-07 03:31:51 +02:00
parent c1b215d06f
commit 78f87f0d44
7 changed files with 1576 additions and 3 deletions

View File

@@ -0,0 +1,88 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
namespace GerbilManager.Tests;
/// <summary>LITTER-MORTALITY: deathsWithin8Weeks field — round-trip, validation, null default.</summary>
public class LitterMortalityTests : IClassFixture<ApiFactory>
{
private readonly HttpClient _client;
public LitterMortalityTests(ApiFactory factory) => _client = factory.CreateClient();
private static int? GetInt(JsonElement el, string prop) =>
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.Number
? v.GetInt32() : null;
private async Task<JsonElement> CreateAndGet(object body)
{
var post = await _client.PostAsJsonAsync("/litters", body);
post.EnsureSuccessStatusCode();
var idStr = JsonDocument.Parse(await post.Content.ReadAsStringAsync()).RootElement
.GetProperty("id").GetString()!;
return JsonDocument.Parse(await _client.GetStringAsync($"/litters/{idStr}")).RootElement;
}
[Fact]
public async Task DeathsWithin8Weeks_round_trips()
{
var el = await CreateAndGet(new
{
name = "Wurf Mortalitätstest",
date = "2026-05-01",
totalBorn = 6,
deathsWithin8Weeks = 2,
});
Assert.Equal(6, GetInt(el, "totalBorn"));
Assert.Equal(2, GetInt(el, "deathsWithin8Weeks"));
}
[Fact]
public async Task DeathsWithin8Weeks_defaults_null()
{
var el = await CreateAndGet(new { name = "Wurf Kein Mort", date = "2026-05-02" });
Assert.True(el.TryGetProperty("deathsWithin8Weeks", out var v));
Assert.Equal(JsonValueKind.Null, v.ValueKind);
}
[Fact]
public async Task DeathsWithin8Weeks_exceeding_TotalBorn_returns_400()
{
var resp = await _client.PostAsJsonAsync("/litters", new
{
name = "Ungültig",
date = "2026-05-03",
totalBorn = 4,
deathsWithin8Weeks = 5,
});
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
var body = await resp.Content.ReadAsStringAsync();
Assert.Contains("TotalBorn", body);
}
[Fact]
public async Task Negative_DeathsWithin8Weeks_returns_400()
{
var resp = await _client.PostAsJsonAsync("/litters", new
{
name = "Negativ",
date = "2026-05-04",
deathsWithin8Weeks = -1,
});
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
var body = await resp.Content.ReadAsStringAsync();
Assert.Contains("negativ", body);
}
[Fact]
public async Task DeathsWithin8Weeks_allowed_when_TotalBorn_null()
{
var el = await CreateAndGet(new
{
name = "Kein TotalBorn",
date = "2026-05-05",
deathsWithin8Weeks = 3,
});
Assert.Equal(3, GetInt(el, "deathsWithin8Weeks"));
}
}

View File

@@ -36,6 +36,7 @@ namespace GerbilManagerWebAPI.Dtos
string Name, string Name,
DateOnly Date, DateOnly Date,
int? TotalBorn, int? TotalBorn,
int? DeathsWithin8Weeks,
Guid? FatherId, Guid? FatherId,
Guid? MotherId, Guid? MotherId,
DateOnly? ExpectedGoHomeDate, DateOnly? ExpectedGoHomeDate,
@@ -85,6 +86,7 @@ namespace GerbilManagerWebAPI.Dtos
string Name, string Name,
DateOnly Date, DateOnly Date,
int? TotalBorn, int? TotalBorn,
int? DeathsWithin8Weeks,
Guid? FatherId, Guid? FatherId,
Guid? MotherId, Guid? MotherId,
DateOnly? ExpectedGoHomeDate, DateOnly? ExpectedGoHomeDate,

View File

@@ -23,10 +23,12 @@ namespace GerbilManagerWebAPI.Endpoints
return l is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(l)); return l is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(l));
}); });
group.MapPost("/", async Task<Results<Created<LitterDto>, BadRequest<ParentGenderError>>> (LitterInput input, ApplicationContext db) => group.MapPost("/", async Task<Results<Created<LitterDto>, BadRequest<ParentGenderError>, BadRequest<string>>> (LitterInput input, ApplicationContext db) =>
{ {
var err = await ValidateParents(db, input.FatherId, input.MotherId); var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err); if (err is not null) return TypedResults.BadRequest(err);
var mortalityErr = ValidateMortality(input.DeathsWithin8Weeks, input.TotalBorn);
if (mortalityErr is not null) return TypedResults.BadRequest(mortalityErr);
var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date }; var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date };
Apply(l, input); Apply(l, input);
@@ -35,12 +37,14 @@ namespace GerbilManagerWebAPI.Endpoints
return TypedResults.Created($"/litters/{l.Id}", ToDto(l)); return TypedResults.Created($"/litters/{l.Id}", ToDto(l));
}); });
group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<ParentGenderError>>> (Guid id, LitterInput input, ApplicationContext db) => group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<ParentGenderError>, BadRequest<string>>> (Guid id, LitterInput input, ApplicationContext db) =>
{ {
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id); var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound(); if (l is null) return TypedResults.NotFound();
var err = await ValidateParents(db, input.FatherId, input.MotherId); var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err); if (err is not null) return TypedResults.BadRequest(err);
var mortalityErr = ValidateMortality(input.DeathsWithin8Weeks, input.TotalBorn);
if (mortalityErr is not null) return TypedResults.BadRequest(mortalityErr);
l.Name = input.Name; l.Name = input.Name;
l.Date = input.Date; l.Date = input.Date;
Apply(l, input); Apply(l, input);
@@ -76,14 +80,25 @@ namespace GerbilManagerWebAPI.Endpoints
private static void Apply(Litter l, LitterInput i) private static void Apply(Litter l, LitterInput i)
{ {
l.TotalBorn = i.TotalBorn; l.TotalBorn = i.TotalBorn;
l.DeathsWithin8Weeks = i.DeathsWithin8Weeks;
l.FatherId = i.FatherId; l.FatherId = i.FatherId;
l.MotherId = i.MotherId; l.MotherId = i.MotherId;
l.ExpectedGoHomeDate = i.ExpectedGoHomeDate; l.ExpectedGoHomeDate = i.ExpectedGoHomeDate;
l.Notes = i.Notes; l.Notes = i.Notes;
} }
private static string? ValidateMortality(int? deaths, int? totalBorn)
{
if (deaths is null) return null;
if (deaths < 0) return "DeathsWithin8Weeks darf nicht negativ sein.";
if (totalBorn is not null && deaths > totalBorn)
return "DeathsWithin8Weeks darf nicht größer als TotalBorn sein.";
return null;
}
private static LitterDto ToDto(Litter l) => new( private static LitterDto ToDto(Litter l) => new(
l.Id, l.Name, l.Date, l.TotalBorn, l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes); l.Id, l.Name, l.Date, l.TotalBorn, l.DeathsWithin8Weeks,
l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes);
} }
/// <summary>400 body for a father×mother gender mismatch; frontend localises by Code.</summary> /// <summary>400 body for a father×mother gender mismatch; frontend localises by Code.</summary>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace GerbilManagerWebAPI.Migrations
{
/// <inheritdoc />
public partial class AddLitterMortality : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DeathsWithin8Weeks",
table: "Litters",
type: "integer",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DeathsWithin8Weeks",
table: "Litters");
}
}
}

View File

@@ -924,6 +924,9 @@ namespace GerbilManagerWebAPI.Migrations
b.Property<DateOnly>("Date") b.Property<DateOnly>("Date")
.HasColumnType("date"); .HasColumnType("date");
b.Property<int?>("DeathsWithin8Weeks")
.HasColumnType("integer");
b.Property<DateOnly?>("ExpectedGoHomeDate") b.Property<DateOnly?>("ExpectedGoHomeDate")
.HasColumnType("date"); .HasColumnType("date");

View File

@@ -13,6 +13,10 @@ namespace GerbilManagerWebAPI.Models
/// <summary>Total born count (was "Strength").</summary> /// <summary>Total born count (was "Strength").</summary>
public int? TotalBorn { get; set; } public int? TotalBorn { get; set; }
/// <summary>Frühsterblichkeit: Anzahl der in den ersten 8 Wochen verstorbenen Tiere.
/// Null = unbekannt (Altdaten). Must be ≤ TotalBorn if both are set.</summary>
public int? DeathsWithin8Weeks { get; set; }
public Guid? FatherId { get; set; } public Guid? FatherId { get; set; }
public Gerbil? Father { get; set; } public Gerbil? Father { get; set; }
public Guid? MotherId { get; set; } public Guid? MotherId { get; set; }