POST /api/v1/suggestions/batches enqueues a suggestion_batch job that runs
suggest_side_quests through the harness, grounds the survivors with
recipes.Grounder.GroundMany (dropping those no recipe grounds) and stores
each as a dinner (status suggested, origin side_quest) with a pending
suggestion holding its reason and cited_regular_ids. Only one batch is
pending or running at a time; a POST while one is active returns it.
GET /api/v1/suggestions/deck lists pending suggestions oldest first with
the dinner's name, photo, difficulty and the reason. POST
/api/v1/suggestions/{id}/accept moves the dinner to the Quest log; /decline
records the Pass and archives the dinner, whose name every later batch
reads as declined. Both are idempotent and 409 on the opposite decision.
A decision leaving fewer than 5 in the deck starts a replenish batch
unless one is already pending or running. Batches left unfinished by a
restart are failed at boot so they never block the next.
Task: b4f2558c-227f-4627-852b-be5aa190242a
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
2.9 KiB
Go
83 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/suggest"
|
|
)
|
|
|
|
type deck struct {
|
|
Suggestions []suggest.Suggestion `json:"suggestions"`
|
|
}
|
|
|
|
// postSuggestionBatch starts a manual suggestion batch and answers 202 with
|
|
// its batch_id and the job_id that GET /jobs/{id} reports on, without
|
|
// waiting for the job. When a batch is already pending or running it
|
|
// answers 202 with that batch instead of starting another.
|
|
func postSuggestionBatch(logger *slog.Logger, svc *suggest.Service) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
b, err := svc.StartBatch(r.Context(), suggest.TriggerManual)
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, "start suggestion batch", err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusAccepted, b)
|
|
}
|
|
}
|
|
|
|
// getSuggestionDeck answers GET /suggestions/deck with the pending
|
|
// suggestions, oldest first.
|
|
func getSuggestionDeck(logger *slog.Logger, svc *suggest.Service) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
list, err := svc.Deck(r.Context())
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, "list suggestion deck", err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, deck{Suggestions: list})
|
|
}
|
|
}
|
|
|
|
// postSuggestionDecision records Try it (accept) or Pass (decline) on a
|
|
// suggestion, recording the session's actor as its dinner's updated_by.
|
|
func postSuggestionDecision(logger *slog.Logger, svc *suggest.Service, accept bool) http.HandlerFunc {
|
|
decide, action := svc.Decline, "decline suggestion"
|
|
if accept {
|
|
decide, action = svc.Accept, "accept suggestion"
|
|
}
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
actor, ok := actorOrFail(w, r, logger)
|
|
if !ok {
|
|
return
|
|
}
|
|
d, err := decide(r.Context(), chi.URLParam(r, "id"), actor.Subject)
|
|
if err != nil {
|
|
writeSuggestionError(w, r, logger, action, err)
|
|
return
|
|
}
|
|
writeJSON(w, r, logger, http.StatusOK, d)
|
|
}
|
|
}
|
|
|
|
// writeSuggestionError maps a suggest.Service error to its API error: an
|
|
// unknown suggestion is 404, and a decision on one already decided the
|
|
// other way, or whose dinner left the deck, is 409.
|
|
func writeSuggestionError(w http.ResponseWriter, r *http.Request, logger *slog.Logger, action string, err error) {
|
|
var decided *suggest.DecidedError
|
|
switch {
|
|
case errors.As(err, &decided):
|
|
writeError(w, r, logger, http.StatusConflict, "suggestion_decided", decided.Error())
|
|
case errors.Is(err, suggest.ErrDinnerNotSuggested):
|
|
writeError(w, r, logger, http.StatusConflict, "dinner_not_suggested", err.Error())
|
|
case errors.Is(err, suggest.ErrNotFound):
|
|
writeError(w, r, logger, http.StatusNotFound, "not_found", "suggestion not found")
|
|
default:
|
|
logger.ErrorContext(r.Context(), action, "error", err)
|
|
writeError(w, r, logger, http.StatusInternalServerError, "internal", "internal error")
|
|
}
|
|
}
|