Wire plan_ready, list_ready, new_suggestions and Send to Google Chat
Connect the features to the notification outbox so real events notify,
each stored by the new notify.EmitTx in the transaction of the change it
is about (Wake after commit):
- plan_ready:{weekStart} (/plan) when a draft job stores the AI's draft;
a redraft shares the key and a manual swap never emits. The title reads
"Next week's plan is ready" for the coming week.
- list_ready:{weekStart} (/plan/shopping) when a week's Supply run is
first created, by generate or after Push to Calendar.
- new_suggestions:{batchId} (/discover) when a scheduled or manual batch
ends done with cards; replenish batches never notify.
- plan.Service.EmitDayOfReminder(date) emits day_of_reminder:{date} for
the Feature 20 scheduler.
Add POST /api/v1/shopping/{weekStart}/send-chat: shopping.SendToChat posts
the dinners with needed items and a link through chat.PostSupplyRun,
records sent_to_chat_at, and is idempotent per week unless
{"resend": true}. The Supply run's Send to Google Chat button calls it
("Send again" once sent) and shows failures in an ErrorBanner; the unused
Unavailable placeholder is removed.
Task: f0cd9d50-9c11-41be-a945-7bae9f5f28a4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/chat"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/household"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/shopping"
|
||||
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
// maxShoppingItemBody bounds a PATCH /shopping/items/{id} body.
|
||||
const maxShoppingItemBody = 1 << 10
|
||||
|
||||
// maxSendChatBody bounds a POST /shopping/{weekStart}/send-chat body.
|
||||
const maxSendChatBody = 1 << 10
|
||||
|
||||
// supplyRun is a Supply run as the API shows it. label is the week label,
|
||||
// which stands in for title until the AI writes one.
|
||||
type supplyRun struct {
|
||||
@@ -59,6 +63,12 @@ type generatedSupplyRun struct {
|
||||
Skipped []shopping.SkippedDay `json:"skipped"`
|
||||
}
|
||||
|
||||
// sendChatRequest is the POST /shopping/{weekStart}/send-chat body:
|
||||
// resend posts a week already sent again.
|
||||
type sendChatRequest struct {
|
||||
Resend bool `json:"resend"`
|
||||
}
|
||||
|
||||
// shoppingItemPatch is the PATCH /shopping/items/{id} body.
|
||||
type shoppingItemPatch struct {
|
||||
Needed *bool `json:"needed"`
|
||||
@@ -120,6 +130,51 @@ func postSupplyRunGenerate(logger *slog.Logger, svc *shopping.Service) http.Hand
|
||||
}
|
||||
}
|
||||
|
||||
// postSupplyRunSendChat posts the week's Supply run to Google Chat and
|
||||
// answers with it, sent_to_chat_at recorded. Its body is a JSON object,
|
||||
// {} or {"resend": true}: a week already sent is answered as it is,
|
||||
// without a second post, unless resend is true. No webhook stored is 409
|
||||
// not_configured; Google Chat not answering is 502 chat_unreachable,
|
||||
// retryable, and refusing the message 502 chat_refused, retryable only for
|
||||
// 429 or 5xx.
|
||||
func postSupplyRunSendChat(logger *slog.Logger, svc *shopping.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
week, ok := weekStartOrFail(w, r, logger)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
body, err := decodePatch[sendChatRequest](http.MaxBytesReader(w, r.Body, maxSendChatBody))
|
||||
if err != nil {
|
||||
writeError(w, r, logger, http.StatusBadRequest, "invalid_request", err.Error())
|
||||
return
|
||||
}
|
||||
l, err := svc.SendToChat(r.Context(), week, body.Resend)
|
||||
var refused *chat.RefusedError
|
||||
switch {
|
||||
case errors.Is(err, chat.ErrNotConfigured):
|
||||
writeError(w, r, logger, http.StatusConflict, "not_configured", "no Google Chat webhook is configured; add one in Settings")
|
||||
return
|
||||
case errors.Is(err, chat.ErrUnreachable):
|
||||
logger.WarnContext(r.Context(), "send Supply run to Google Chat", "error", err)
|
||||
writeAPIError(w, r, logger, http.StatusBadGateway, apiError{Code: "chat_unreachable", Message: "Google Chat did not answer", Retryable: true})
|
||||
return
|
||||
case errors.As(err, &refused):
|
||||
logger.WarnContext(r.Context(), "send Supply run to Google Chat", "error", err)
|
||||
writeAPIError(w, r, logger, http.StatusBadGateway, apiError{Code: "chat_refused", Message: err.Error(), Retryable: refused.Retryable()})
|
||||
return
|
||||
case err != nil:
|
||||
writeShoppingError(w, r, logger, "send Supply run to Google Chat", err)
|
||||
return
|
||||
}
|
||||
out, err := supplyRunOf(week, l)
|
||||
if err != nil {
|
||||
writeShoppingError(w, r, logger, "send Supply run to Google Chat", err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, r, logger, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// patchShoppingItem marks a Supply run item needed or "have it" and
|
||||
// answers with its Supply run's new counts.
|
||||
func patchShoppingItem(logger *slog.Logger, svc *shopping.Service) http.HandlerFunc {
|
||||
|
||||
Reference in New Issue
Block a user