Push plan to Google Calendar (Feature 16) (#127)
CI / ci (push) Successful in 3m56s

Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
This commit was merged in pull request #127.
This commit is contained in:
2026-10-07 23:27:46 -04:00
committed by approver_agent
parent 482bec9727
commit 6262489376
20 changed files with 2227 additions and 90 deletions
+53
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/gcal"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/plan"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/store"
)
@@ -93,6 +94,58 @@ func putPlanDay(logger *slog.Logger, svc *plan.Service) http.HandlerFunc {
}
}
// postPlanPush pushes the week's plan to the chosen Google calendar
// (Push to Calendar) and answers with the plan as synced.
func postPlanPush(logger *slog.Logger, svc *plan.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
week, ok := weekStartOrFail(w, r, logger)
if !ok {
return
}
p, err := svc.Push(r.Context(), week)
if err != nil {
writePlanPushError(w, r, logger, err)
return
}
writeJSON(w, r, logger, http.StatusOK, p)
}
}
// writePlanPushError maps a Push to Calendar error to its API error: 409
// not_connected when Google Calendar is not connected, no_calendar when no
// calendar is chosen, calendar_unavailable when the chosen calendar was
// deleted or is no longer writable, and plan_changed when the plan was
// edited during the push; 502 reconnect_required when Google no longer
// accepts the grant, and google_failed when Google refused or could not be
// reached. A failed push leaves the plan's status as it was. Any other
// error is the plan's, as writePlanError maps it.
func writePlanPushError(w http.ResponseWriter, r *http.Request, logger *slog.Logger, err error) {
var calendar *gcal.CalendarError
switch {
case errors.Is(err, gcal.ErrNotConnected):
writeError(w, r, logger, http.StatusConflict, "not_connected", "Google Calendar is not connected")
case errors.Is(err, gcal.ErrNoCalendar):
writeError(w, r, logger, http.StatusConflict, "no_calendar",
"choose the calendar to push dinners to in Settings, then push again")
case errors.Is(err, gcal.ErrCalendarUnavailable):
logger.WarnContext(r.Context(), "push plan to Calendar", "error", err)
writeError(w, r, logger, http.StatusConflict, "calendar_unavailable",
"the chosen calendar was deleted or MealQuest can no longer add dinners to it; choose another calendar in Settings")
case errors.Is(err, plan.ErrPlanChanged):
writeError(w, r, logger, http.StatusConflict, "plan_changed", err.Error())
case errors.Is(err, gcal.ErrReconnect):
logger.WarnContext(r.Context(), "push plan to Calendar", "error", err)
writeError(w, r, logger, http.StatusBadGateway, "reconnect_required",
"Google no longer accepts MealQuest's access to your calendar; connect Google Calendar again")
case errors.As(err, &calendar):
logger.WarnContext(r.Context(), "push plan to Calendar", "error", err)
writeError(w, r, logger, http.StatusBadGateway, "google_failed",
"Google Calendar could not be reached; push again")
default:
writePlanError(w, r, logger, "push plan to Calendar", err)
}
}
// weekStartOrFail parses the {weekStart} path parameter, or answers 400
// when it is not a Monday date.
func weekStartOrFail(w http.ResponseWriter, r *http.Request, logger *slog.Logger) (store.LocalDate, bool) {