Merge feature/inbox-2: POST /api/requests/{id}/draft (AI German reply draft, never auto-send), extract OpenAiChatClient to Ai/, privacy stripping, 8 tests [god-QA pending]

This commit is contained in:
2026-06-06 10:37:56 +02:00
7 changed files with 481 additions and 69 deletions

View File

@@ -51,6 +51,34 @@ namespace GerbilManagerWebAPI.Endpoints
return TypedResults.NoContent();
});
// POST /api/requests/{id}/draft — INBOX-2: KI-Antwortentwurf. NUR Entwurf
// (human-in-the-loop, Versand ist /send). 503 AiKeyMissing solange die
// AI-Sektion unkonfiguriert ist (gleiches UI-Muster wie sale-ad).
api.MapPost("/requests/{id:guid}/draft", async Task<IResult> (
Guid id, DraftReplyService drafter, ApplicationContext db, CancellationToken ct) =>
{
var r = await db.Requests.FirstOrDefaultAsync(x => x.Id == id, ct);
if (r is null) return TypedResults.NotFound();
// Datenminimierung: NUR die ForSale-Liste (Name + Farbschlag) geht
// zusätzlich zum Anfragetext an den Anbieter (arch §privacy).
var forSale = await db.Gerbils.AsNoTracking()
.Where(g => g.Status == GerbilStatus.ForSale)
.OrderBy(g => g.Name)
.Select(g => new DraftReplyService.ForSaleAnimal(g.Name, g.ColorVariety!.Name))
.ToListAsync(ct);
var result = await drafter.DraftAsync(r, forSale, ct);
if (result.Status == Ai.AiCallStatus.NotConfigured)
return Results.Json(new { code = "AiKeyMissing", message = result.Error }, statusCode: 503);
if (result.Status != Ai.AiCallStatus.Ok)
return Results.Json(new { code = "AiUpstreamError", message = result.Error }, statusCode: 502);
r.DraftReply = result.Text;
await db.SaveChangesAsync(ct);
return TypedResults.Ok(ToDto(r));
});
// POST /api/requests/{id}/send — send the (edited) reply, threaded, mark Answered
api.MapPost("/requests/{id:guid}/send", async Task<Results<Ok<RequestDto>, NotFound, ProblemHttpResult>> (
Guid id, SendReplyInput input, SendReplyService sender, ApplicationContext db) =>