Freeze ended weeks into the Supply archive and serve the archive API (Feature 17)
CI / ci (push) Successful in 5m4s
CI / ci (pull_request) Successful in 4m55s

shopping.Freeze(weekStart) archives a week's Supply run once its week has
ended in the household's time zone, setting status archived and frozen_at;
it is idempotent, and a week not yet ended (ErrWeekNotEnded) or with no
list (ErrListNotFound) is an explicit error. A list still untitled is
titled first with archive_title and the same conditional write the
title_supply_run job uses; a failed title is logged and the list is
frozen untitled, keeping its week label.

GET /api/v1/shopping/archive?by=week|month|year returns the Supply runs
up to the household's current week, newest first, grouped by the
Monday's ISO week, month or year, each post with its week label, title or
null, dinner, item and on-hand counts, dinner names and current flag. Any
other by is 400 invalid_request. Frozen lists stay readable through
GET /shopping/{weekStart} and reject item PATCHes with 409 archived.

Maestro task 7ac36526-cb51-4dc7-bbad-6ebbee481942.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 07:10:06 -04:00
co-authored by Claude Opus 5.5
parent 1cbd215286
commit 6da5643f49
12 changed files with 954 additions and 5 deletions
+23 -3
View File
@@ -8,6 +8,10 @@
// (see Generate). The one model call is the Supply run's title, written by
// archive_title in a KindTitleSupplyRun job when the list is generated
// untitled (see Generate).
//
// Freeze moves a week's Supply run into the Supply archive once its week
// has ended, and Archive reads the archive back as posts grouped by week,
// month or year.
package shopping
import (
@@ -19,6 +23,7 @@ import (
"fmt"
"log/slog"
"strings"
"time"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/ai/skills"
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/db"
@@ -70,6 +75,14 @@ var ErrListNotFound = errors.New("the week has no Supply run")
// ErrItemNotFound is a change to an item on no Supply run.
var ErrItemNotFound = errors.New("no Supply run has that item")
// ErrWeekNotEnded is a freeze of a Supply run whose week has not ended in
// the household's time zone.
var ErrWeekNotEnded = errors.New("the week has not ended")
// ErrInvalidPeriod is an Archive grouping other than ByWeek, ByMonth and
// ByYear.
var ErrInvalidPeriod = errors.New("by must be week, month or year")
// Titler runs archive_title on in; scope names the week, its plan revision
// and its dinners.
// In production it is skills.TitleArchive with the household's
@@ -82,6 +95,8 @@ type Service struct {
title Titler
titleJob *jobs.Kind[TitleJob]
logger *slog.Logger
// now is the clock weeks are ended and current on; tests replace it.
now func() time.Time
}
// NewService returns a service over st that titles Supply runs with title
@@ -98,7 +113,7 @@ func NewService(st *store.Store, title Titler, runner *jobs.Runner, logger *slog
case logger == nil:
return nil, errors.New("shopping: nil logger")
}
s := &Service{st: st, title: title, logger: logger}
s := &Service{st: st, title: title, logger: logger, now: time.Now}
kind, err := jobs.Register(runner, KindTitleSupplyRun, s.runTitleJob, ReasonTitleFailed)
if err != nil {
return nil, err
@@ -423,13 +438,18 @@ func (s *Service) Get(ctx context.Context, weekStart store.LocalDate) (l List, e
if err != nil {
return List{}, fmt.Errorf("shopping: read list: %w", err)
}
return readList(ctx, q, list)
}
// readList reads the groups and items of list through q.
func readList(ctx context.Context, q *db.Queries, list db.ShoppingList) (List, error) {
rows, err := q.ListShoppingGroupDinners(ctx, list.ID)
if err != nil {
return List{}, fmt.Errorf("shopping: read groups: %w", err)
return List{}, fmt.Errorf("shopping: read groups of %s: %w", list.WeekStart, err)
}
items, err := q.ListShoppingItems(ctx, list.ID)
if err != nil {
return List{}, fmt.Errorf("shopping: read items: %w", err)
return List{}, fmt.Errorf("shopping: read items of %s: %w", list.WeekStart, err)
}
byGroup := make(map[string][]Item, len(rows))
for _, r := range items {