Merge branch 'worktree-agent-a36d202be311b9289'
# Conflicts: # GerbilManagerWebAPI/ApplicationContext.cs # GerbilManagerWebAPI/Program.cs # gerbil-manager-web/e2e/mock-data.ts
This commit is contained in:
124
GerbilManagerWebAPI/Endpoints/SaleReservationEndpoints.cs
Normal file
124
GerbilManagerWebAPI/Endpoints/SaleReservationEndpoints.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using GerbilManagerWebAPI.Dtos;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GerbilManagerWebAPI.Endpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// ABGABE-STATUS (RPRO3 abgeben_tb / abstat_tb): the pre-handover reservation/sale
|
||||
/// pipeline — verfügbar → reserviert → abgegeben.
|
||||
/// GET /reservations -> list all, newest-updated first.
|
||||
/// GET /reservations?gerbilId= -> the (single) reservation status for one animal, if any.
|
||||
/// POST /reservations -> create/establish a status, returns 201.
|
||||
/// PUT /reservations/{id} -> change status / reservation details. 404 on missing id.
|
||||
/// DELETE /reservations/{id} -> remove. 404 on missing id.
|
||||
/// Decoupled from gerbils/contacts (loose nullable Guid columns, no FK), so rows survive
|
||||
/// the import re-ingest wipe — like Feedback.
|
||||
/// </summary>
|
||||
public static class SaleReservationEndpoints
|
||||
{
|
||||
private static readonly string[] AllowedStatuses = { "verfuegbar", "reserviert", "abgegeben" };
|
||||
|
||||
/// <summary>Normalize a status string to one of the three canonical values; default "verfuegbar".</summary>
|
||||
private static string NormalizeStatus(string? raw)
|
||||
{
|
||||
var s = raw?.Trim().ToLowerInvariant();
|
||||
return AllowedStatuses.Contains(s) ? s! : "verfuegbar";
|
||||
}
|
||||
|
||||
public static IEndpointRouteBuilder MapSaleReservationEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/reservations").WithTags("Reservations");
|
||||
|
||||
group.MapGet("/", async (ApplicationContext db, Guid? gerbilId) =>
|
||||
{
|
||||
// Order in memory: SQLite (test host) cannot ORDER BY a DateTimeOffset column.
|
||||
var rows = await db.SaleReservations.AsNoTracking().ToListAsync();
|
||||
var filtered = gerbilId is { } gid
|
||||
? rows.Where(r => r.GerbilId == gid)
|
||||
: rows;
|
||||
return TypedResults.Ok(filtered
|
||||
.OrderByDescending(r => r.UpdatedAt)
|
||||
.Select(ToDto)
|
||||
.ToList());
|
||||
});
|
||||
|
||||
group.MapPost("/", async Task<Results<Created<SaleReservationDto>, BadRequest<string>>> (
|
||||
SaleReservationInput input, ApplicationContext db) =>
|
||||
{
|
||||
if (input.GerbilId == Guid.Empty)
|
||||
return TypedResults.BadRequest("GerbilId ist erforderlich.");
|
||||
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var entity = new SaleReservation
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
GerbilId = input.GerbilId,
|
||||
GerbilName = Trim(input.GerbilName),
|
||||
Status = NormalizeStatus(input.Status),
|
||||
ReservedForContactId = input.ReservedForContactId,
|
||||
ContactName = Trim(input.ContactName),
|
||||
AppointmentDate = input.AppointmentDate,
|
||||
Price = input.Price,
|
||||
Note = Trim(input.Note),
|
||||
HandedOverDate = input.HandedOverDate,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
db.SaleReservations.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.Created($"/reservations/{entity.Id}", ToDto(entity));
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async Task<Results<Ok<SaleReservationDto>, NotFound>> (
|
||||
Guid id, SaleReservationUpdate input, ApplicationContext db) =>
|
||||
{
|
||||
var entity = await db.SaleReservations.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (entity is null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
if (input.Status is not null)
|
||||
entity.Status = NormalizeStatus(input.Status);
|
||||
if (input.GerbilName is not null)
|
||||
entity.GerbilName = Trim(input.GerbilName);
|
||||
if (input.ContactName is not null)
|
||||
entity.ContactName = Trim(input.ContactName);
|
||||
if (input.Note is not null)
|
||||
entity.Note = Trim(input.Note);
|
||||
|
||||
// Value-type/nullable fields are always applied from the payload (a null clears them).
|
||||
entity.ReservedForContactId = input.ReservedForContactId;
|
||||
entity.AppointmentDate = input.AppointmentDate;
|
||||
entity.Price = input.Price;
|
||||
entity.HandedOverDate = input.HandedOverDate;
|
||||
|
||||
entity.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.Ok(ToDto(entity));
|
||||
});
|
||||
|
||||
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (
|
||||
Guid id, ApplicationContext db) =>
|
||||
{
|
||||
var entity = await db.SaleReservations.FirstOrDefaultAsync(r => r.Id == id);
|
||||
if (entity is null)
|
||||
return TypedResults.NotFound();
|
||||
|
||||
db.SaleReservations.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.NoContent();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static string? Trim(string? s) =>
|
||||
string.IsNullOrWhiteSpace(s) ? null : s.Trim();
|
||||
|
||||
private static SaleReservationDto ToDto(SaleReservation r) =>
|
||||
new(r.Id, r.GerbilId, r.GerbilName, r.Status, r.ReservedForContactId, r.ContactName,
|
||||
r.AppointmentDate, r.Price, r.Note, r.HandedOverDate, r.CreatedAt, r.UpdatedAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user