feat(tier): Erwerb/Kauf je Tier (Datum, Preis, Notiz)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:43:18 +02:00
parent 5e14124322
commit 55e79cda51
16 changed files with 2360 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Endpoints
{
/// <summary>
/// ERWERB: acquisition data per animal (purchase date/price/note).
/// GET /acquisitions?gerbilId=… -> records for one animal (or all), newest date first.
/// GET /acquisitions/{id} -> a single record.
/// POST /acquisitions -> create, returns 201.
/// PUT /acquisitions/{id} -> update, returns 204.
/// DELETE /acquisitions/{id} -> delete, returns 204.
/// Decoupled from gerbils/contacts (loose nullable Guid columns, no FK), so rows survive
/// the import re-ingest wipe (mirrors Feedback).
/// </summary>
public static class AcquisitionEndpoints
{
public static IEndpointRouteBuilder MapAcquisitionEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/acquisitions").WithTags("Acquisitions");
group.MapGet("/", async (ApplicationContext db, Guid? gerbilId) =>
{
var rows = await db.AcquisitionRecords.AsNoTracking()
.Where(a => gerbilId == null || a.GerbilId == gerbilId)
.ToListAsync();
// Order in memory: SQLite (test host) cannot ORDER BY a DateTimeOffset column,
// and DateOnly ordering stays consistent across providers this way.
return TypedResults.Ok(rows
.OrderByDescending(a => a.Date ?? DateOnly.MinValue)
.ThenByDescending(a => a.CreatedAt)
.Select(ToDto)
.ToList());
});
group.MapGet("/{id:guid}", async Task<Results<Ok<AcquisitionDto>, NotFound>> (Guid id, ApplicationContext db) =>
{
var a = await db.AcquisitionRecords.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
return a is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(a));
});
group.MapPost("/", async (AcquisitionInput input, ApplicationContext db) =>
{
var a = new AcquisitionRecord
{
Id = Guid.NewGuid(),
GerbilId = input.GerbilId,
SourceContactId = input.SourceContactId,
Date = input.Date,
Price = input.Price,
Note = string.IsNullOrWhiteSpace(input.Note) ? null : input.Note.Trim(),
CreatedAt = DateTimeOffset.UtcNow,
};
db.AcquisitionRecords.Add(a);
await db.SaveChangesAsync();
return TypedResults.Created($"/acquisitions/{a.Id}", ToDto(a));
});
group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, AcquisitionInput input, ApplicationContext db) =>
{
var a = await db.AcquisitionRecords.FirstOrDefaultAsync(x => x.Id == id);
if (a is null) return TypedResults.NotFound();
a.GerbilId = input.GerbilId;
a.SourceContactId = input.SourceContactId;
a.Date = input.Date;
a.Price = input.Price;
a.Note = string.IsNullOrWhiteSpace(input.Note) ? null : input.Note.Trim();
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
{
var a = await db.AcquisitionRecords.FirstOrDefaultAsync(x => x.Id == id);
if (a is null) return TypedResults.NotFound();
db.AcquisitionRecords.Remove(a);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
return app;
}
private static AcquisitionDto ToDto(AcquisitionRecord a) =>
new(a.Id, a.GerbilId, a.SourceContactId, a.Date, a.Price, a.Note, a.CreatedAt);
}
}