Files
GerbilManager/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs
Gulum 78f87f0d44 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.
2026-06-07 03:31:51 +02:00

107 lines
5.1 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GerbilManagerWebAPI.Common;
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Gridify;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Endpoints
{
public static class LitterEndpoints
{
public static IEndpointRouteBuilder MapLitterEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/litters").WithTags("Litters");
// GET /litters (Gridify: date range + orderBy=date supported on the DateOnly column)
group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) =>
TypedResults.Ok(await db.Litters.AsNoTracking().ToPagedResultAsync(query, ToDto)));
group.MapGet("/{id:guid}", async Task<Results<Ok<LitterDto>, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
return l is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(l));
});
group.MapPost("/", async Task<Results<Created<LitterDto>, BadRequest<ParentGenderError>, BadRequest<string>>> (LitterInput input, ApplicationContext db) =>
{
var err = await ValidateParents(db, input.FatherId, input.MotherId);
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 };
Apply(l, input);
db.Litters.Add(l);
await db.SaveChangesAsync();
return TypedResults.Created($"/litters/{l.Id}", ToDto(l));
});
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);
if (l is null) return TypedResults.NotFound();
var err = await ValidateParents(db, input.FatherId, input.MotherId);
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.Date = input.Date;
Apply(l, input);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound();
db.Litters.Remove(l);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
return app;
}
// father must not be female; mother must not be male (unknown is allowed).
private static async Task<ParentGenderError?> ValidateParents(ApplicationContext db, Guid? fatherId, Guid? motherId)
{
var father = fatherId is Guid f ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == f) : null;
var mother = motherId is Guid m ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == m) : null;
if (father?.Gender == Gender.female || mother?.Gender == Gender.male)
{
return new ParentGenderError("InvalidParentGender",
father?.Gender ?? Gender.unknown, mother?.Gender ?? Gender.unknown);
}
return null;
}
private static void Apply(Litter l, LitterInput i)
{
l.TotalBorn = i.TotalBorn;
l.DeathsWithin8Weeks = i.DeathsWithin8Weeks;
l.FatherId = i.FatherId;
l.MotherId = i.MotherId;
l.ExpectedGoHomeDate = i.ExpectedGoHomeDate;
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(
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>
public record ParentGenderError(string Code, Gender FatherGender, Gender MotherGender);
}