Mark unfrozen Supply archive posts and open them in the Supply run
The Supply archive presented every post as a frozen, read-only list, even a past week's Supply run that week_rollover had not frozen and that the Supply run route could still change. Each archive post now carries frozen; a post not yet frozen is marked (Current, or Still open for a past week) and links to the Supply run, where it can still change. Only frozen posts open read-only in the archive. Maestro task 29b629aa-1ad9-48a3-be75-0380b645d4fe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -222,8 +222,9 @@ type archivePeriod struct {
|
||||
}
|
||||
|
||||
// archivePost is one week's Supply run in the Supply archive. label is
|
||||
// the week label, which stands in for title while it is null, and
|
||||
// onHandCount counts the items marked "have it".
|
||||
// the week label, which stands in for title while it is null,
|
||||
// onHandCount counts the items marked "have it", and frozen is false
|
||||
// while the list can still be changed from the Supply run.
|
||||
type archivePost struct {
|
||||
WeekStart string `json:"weekStart"`
|
||||
Label string `json:"label"`
|
||||
@@ -233,6 +234,7 @@ type archivePost struct {
|
||||
OnHandCount int `json:"onHandCount"`
|
||||
Dinners []string `json:"dinners"`
|
||||
Current bool `json:"current"`
|
||||
Frozen bool `json:"frozen"`
|
||||
}
|
||||
|
||||
// getSupplyArchive answers GET /shopping/archive?by=week|month|year with
|
||||
@@ -262,6 +264,7 @@ func getSupplyArchive(logger *slog.Logger, svc *shopping.Service) http.HandlerFu
|
||||
OnHandCount: post.OnHand,
|
||||
Dinners: post.Dinners,
|
||||
Current: post.Current,
|
||||
Frozen: post.Frozen,
|
||||
}
|
||||
}
|
||||
out.Periods[i] = archivePeriod{Key: p.Key, Heading: p.Heading, Posts: posts}
|
||||
|
||||
@@ -720,6 +720,7 @@ type archiveJSON struct {
|
||||
OnHandCount int `json:"onHandCount"`
|
||||
Dinners []string `json:"dinners"`
|
||||
Current bool `json:"current"`
|
||||
Frozen bool `json:"frozen"`
|
||||
} `json:"posts"`
|
||||
} `json:"periods"`
|
||||
}
|
||||
@@ -798,7 +799,7 @@ func TestSupplyArchive(t *testing.T) {
|
||||
if k := keys(period); !reflect.DeepEqual(k, []string{"heading", "key", "posts"}) {
|
||||
t.Errorf("period fields = %v", k)
|
||||
}
|
||||
wantPost := []string{"current", "dinnerCount", "dinners", "itemCount", "label", "onHandCount", "title", "weekStart"}
|
||||
wantPost := []string{"current", "dinnerCount", "dinners", "frozen", "itemCount", "label", "onHandCount", "title", "weekStart"}
|
||||
if k := keys(period["posts"].([]any)[0].(map[string]any)); !reflect.DeepEqual(k, wantPost) {
|
||||
t.Errorf("post fields = %v, want %v", k, wantPost)
|
||||
}
|
||||
@@ -815,13 +816,13 @@ func TestSupplyArchive(t *testing.T) {
|
||||
if post.Label != label || post.Title != nil {
|
||||
t.Errorf("post %s: label %q title %v, want %q and null", post.WeekStart, post.Label, post.Title, label)
|
||||
}
|
||||
posts = append(posts, fmt.Sprintf("%s %d dinners %d items %d on hand %v current=%t",
|
||||
post.WeekStart, post.DinnerCount, post.ItemCount, post.OnHandCount, post.Dinners, post.Current))
|
||||
posts = append(posts, fmt.Sprintf("%s %d dinners %d items %d on hand %v current=%t frozen=%t",
|
||||
post.WeekStart, post.DinnerCount, post.ItemCount, post.OnHandCount, post.Dinners, post.Current, post.Frozen))
|
||||
}
|
||||
}
|
||||
want := []string{
|
||||
thisWeek.String() + " 1 dinners 2 items 0 on hand [Lemon Chicken] current=true",
|
||||
lastWeek.String() + " 1 dinners 3 items 1 on hand [Beef Stew] current=false",
|
||||
thisWeek.String() + " 1 dinners 2 items 0 on hand [Lemon Chicken] current=true frozen=false",
|
||||
lastWeek.String() + " 1 dinners 3 items 1 on hand [Beef Stew] current=false frozen=true",
|
||||
}
|
||||
if got.By != "week" || !reflect.DeepEqual(posts, want) {
|
||||
t.Errorf("archive by %s =\n %s\nwant\n %s", got.By, strings.Join(posts, "\n "), strings.Join(want, "\n "))
|
||||
@@ -852,6 +853,70 @@ func TestSupplyArchive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupplyArchiveUnfrozenPastWeek: a past week's Supply run that
|
||||
// week_rollover has not frozen is in the Supply archive as not frozen for
|
||||
// as long as the Supply run route can still change it, and as frozen once
|
||||
// it cannot.
|
||||
func TestSupplyArchiveUnfrozenPastWeek(t *testing.T) {
|
||||
t.Parallel()
|
||||
e := newPlansRouter(t)
|
||||
if _, err := e.st.Writer.ExecContext(t.Context(), `DELETE FROM plan_days; DELETE FROM week_plans`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
zone, err := household.LoadZone("America/New_York") // the env's household zone
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
thisWeek := household.WeekStart(household.Today(time.Now(), zone))
|
||||
y, m, d := thisWeek.Date()
|
||||
lastWeek := store.LocalDateOf(time.Date(y, m, d-7, 0, 0, 0, 0, time.UTC))
|
||||
last := e.weekSupplyRun(t, lastWeek, "Beef Stew", shopIng{"stewing beef", "2", "lb"}, shopIng{"carrots", "4", ""})
|
||||
|
||||
// post is last week's post in the Supply archive.
|
||||
post := func() (frozen bool) {
|
||||
t.Helper()
|
||||
var got archiveJSON
|
||||
if status, raw := callShopping(t, e, http.MethodGet, shoppingPath+"archive?by=week", "", &got); status != http.StatusOK {
|
||||
t.Fatalf("GET archive = %d %v", status, raw)
|
||||
}
|
||||
for _, p := range got.Periods {
|
||||
for _, post := range p.Posts {
|
||||
if post.WeekStart == lastWeek.String() {
|
||||
if post.Current {
|
||||
t.Errorf("last week's post is current")
|
||||
}
|
||||
return post.Frozen
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("archive %+v has no post for %s", got, lastWeek)
|
||||
return false
|
||||
}
|
||||
// change marks carrots and reports whether the Supply run route took it.
|
||||
change := func(needed bool) bool {
|
||||
t.Helper()
|
||||
status, body := callShopping(t, e, http.MethodPatch, shoppingPath+"items/"+last.itemID(t, "carrots"), fmt.Sprintf(`{"needed":%t}`, needed), nil)
|
||||
switch {
|
||||
case status == http.StatusOK:
|
||||
return true
|
||||
case status == http.StatusConflict && errorCode(body) == "archived":
|
||||
return false
|
||||
}
|
||||
t.Fatalf("PATCH = %d %v, want 200 or 409 archived", status, body)
|
||||
return false
|
||||
}
|
||||
|
||||
if frozen, changed := post(), change(false); frozen || !changed {
|
||||
t.Errorf("unfrozen past week: post frozen=%t, PATCH changed=%t; want a post not frozen for a list that changes", frozen, changed)
|
||||
}
|
||||
if err := e.shop.Freeze(t.Context(), lastWeek); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if frozen, changed := post(), change(true); !frozen || changed {
|
||||
t.Errorf("frozen past week: post frozen=%t, PATCH changed=%t; want a frozen post for a list that cannot change", frozen, changed)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSupplyRunSendChat posts the week's Supply run to Google Chat once,
|
||||
// records sent_to_chat_at, answers a second send with the status as it
|
||||
// is, and posts again only with resend.
|
||||
|
||||
@@ -152,6 +152,11 @@ type Post struct {
|
||||
Items, OnHand int
|
||||
// Current is true for the household's current week.
|
||||
Current bool
|
||||
// Frozen is true once the list is frozen into the Supply archive and
|
||||
// can no longer change. A list not yet frozen — the current week's, or
|
||||
// a past week's that week_rollover has not frozen — can still be
|
||||
// changed from the Supply run.
|
||||
Frozen bool
|
||||
}
|
||||
|
||||
// Archive returns the Supply archive: the Supply run of every week up to
|
||||
@@ -208,7 +213,7 @@ func (s *Service) Archive(ctx context.Context, by string) (out []Period, err err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := postOf(monday, l, monday == thisWeek)
|
||||
p, err := postOf(monday, l, monday == thisWeek, frozen(row))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -223,7 +228,7 @@ func (s *Service) Archive(ctx context.Context, by string) (out []Period, err err
|
||||
|
||||
// postOf is l, the Supply run of the week starting monday, as a post in
|
||||
// the Supply archive.
|
||||
func postOf(monday store.LocalDate, l List, current bool) (Post, error) {
|
||||
func postOf(monday store.LocalDate, l List, current, isFrozen bool) (Post, error) {
|
||||
label, err := household.WeekLabel(monday)
|
||||
if err != nil {
|
||||
return Post{}, fmt.Errorf("shopping: Supply run %s: %w", l.ID, err)
|
||||
@@ -241,5 +246,6 @@ func postOf(monday store.LocalDate, l List, current bool) (Post, error) {
|
||||
Items: c.Total,
|
||||
OnHand: c.Total - c.Needed,
|
||||
Current: current,
|
||||
Frozen: isFrozen,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -381,6 +381,9 @@ func postLine(p shopping.Post) string {
|
||||
if p.Current {
|
||||
cur = " | current"
|
||||
}
|
||||
if !p.Frozen {
|
||||
cur += " | open"
|
||||
}
|
||||
return p.WeekStart + " " + p.Label + " | " + title + " | " + strings.Join(p.Dinners, ", ") + " | " +
|
||||
strconv.Itoa(p.Items) + " items, " + strconv.Itoa(p.OnHand) + " on hand" + cur
|
||||
}
|
||||
@@ -427,7 +430,7 @@ func TestArchiveGroups(t *testing.T) {
|
||||
}
|
||||
|
||||
posts := map[string]string{
|
||||
"jan": "2027-01-04 Week 1 · Jan 4 – 10 | - | Pad Thai, Beef Chili | 5 items, 1 on hand | current",
|
||||
"jan": "2027-01-04 Week 1 · Jan 4 – 10 | - | Pad Thai, Beef Chili | 5 items, 1 on hand | current | open",
|
||||
"dec": "2026-12-28 Week 53 · Dec 28 – Jan 3 | Chili, Noodles & Co | Beef Chili, Pad Thai | 5 items, 2 on hand",
|
||||
"d07": "2026-12-07 Week 50 · Dec 7 – 13 | - | Pad Thai | 2 items, 0 on hand",
|
||||
"nov": "2026-11-30 Week 49 · Nov 30 – Dec 6 | - | Pot Roast | 1 items, 1 on hand",
|
||||
|
||||
Reference in New Issue
Block a user