Merge feature/litter-mortality (LITTER-MORTALITY-BE): Litter.DeathsWithin8Weeks int? + additive Migration + DTO + Validierung <=TotalBorn
This commit is contained in:
88
GerbilManager.Tests/LitterMortalityTests.cs
Normal file
88
GerbilManager.Tests/LitterMortalityTests.cs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,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,
|
||||||
@@ -86,6 +87,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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
1433
GerbilManagerWebAPI/Migrations/20260607013052_AddLitterMortality.Designer.cs
generated
Normal file
1433
GerbilManagerWebAPI/Migrations/20260607013052_AddLitterMortality.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user