Skip to content

fix: name the trip on a shopping row bought only on a later trip - #74

Merged
guplem merged 10 commits into
mainfrom
73-single-later-trip-label
Oct 2, 2026
Merged

guplem merged 10 commits into
mainfrom
73-single-later-trip-label

Conversation

@guplem

@guplem guplem commented Oct 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #73

A product that only later shopping trips buy now names the trip on the shopping page, for example "Buy 2 pieces Saturday 17 Oct" (the shopping day) instead of a plain "Buy 2 pieces". Before, the trip appeared only in the copied list and the PDF, so the user could buy a perishable product too early.

  • Standalone rows (_tripPurchasesForProduct, shopping_ingredient.dart): when one trip buys the product, the row names the earliest trip that holds the ingredient. When that is the first trip of the plan, the row stays a plain "Buy X". The 2+ trip split is unchanged.
  • Equivalent-product groups (for example two pizza flavors of one size): each member row names the earliest trip that holds the ingredient, when that is not the first trip. It keeps its own one-of-each count.
  • Row (ShoppingProductRow): the single-total line adds the trip label from shoppingTripLabel, the same function that the 2+ lines, the copy and the PDF use. The under-buy reduction still applies.
  • Known difference: when the rounding of the split gives an early trip 0 units, the row names that earlier trip, but the copied list puts the product under the later one. This is on purpose (safer to buy early) and recorded in ADR 0014. The split fix is Rounding of the trip split can put a week-0 need on a later trip #75.
  • Docs: ADR 0014 and the AGENTS.md test count (1066 tests).

Test plan

  • 13 new tests, each proven by a mutation of the production line; 3 weak textContaining checks replaced by exact checks
  • Format check, flutter analyze, full suite (1066 tests) pass locally
  • Manual check: open the shopping page of a multi-week menu and confirm a later-trip-only product names its trip

🤖 Generated with Claude Code

guplem and others added 2 commits October 2, 2026 19:33
A product that only a later shopping trip buys showed a plain
"Buy 2 pieces", which reads as "buy it now". The user then bought a
perishable too early, for example a lettuce that a week 3 meal uses.

The per-trip list now keeps a single purchase when it is not the first
trip, and the row adds that trip's label (the same shoppingTripLabel
that the per-trip lines use). A product that only the first trip buys
still shows the plain total.

The single line keeps the under-buy count: it shows packsToBuy minus
one, not the purchase's full round-up, so the warning chip and the
count agree.

Closes #73

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fix for #73 adds 4 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@guplem guplem added waiting-for-human-check No human has verified this yet -- direct AI output shopping Shopping list feature labels Oct 2, 2026
@guplem guplem self-assigned this Oct 2, 2026

@guplem guplem left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good, focused fix. The single later-trip line reuses shoppingTripLabel through the new _tripWhen helper, so the row, the 2+ trip lines, the copy and the PDF name the trip the same way. The under-buy case is handled, and each new test asserts the full text and was proven by a mutation.

Two gaps keep the fix from being complete:

  • [Required] Equivalent-product groups still show a plain total (menu_management/lib/shopping/shopping_ingredient.dart:487, tripPurchases: isCombinedGroup ? const [] : _tripPurchasesForProduct(product),). Two pizza flavors of one size form such a group. When only a later trip buys the ingredient, each member row still reads "Buy 1 pack", which is the #73 symptom. The old reason for the empty list (a 2+ split would show wrong solo counts) does not apply to a label that only names the trip. Fix: when every purchase of the group falls on one later trip, pass a one-entry list to each visible member, and add a widget test.
  • [Required] Rounding can name a trip that is too late (inline comment on line 292).

Smaller items:

  • [Suggestion] textContaining("week") in test/shopping_ingredient_test.dart:232, :249 and :348 can never fail, because the labels write "Week" with a capital W. These tests now guard the "first trip alone" rule. Replace them with exact find.text(...) checks of the wrong outputs.
  • [Suggestion] Add ingredient-level tests for: a start date (real date label through _tripPurchasesForProduct), owned stock that lowers the single later-trip count, and parity with the copied list's trip section.
  • [Suggestion] adr/0014-multi-trip-shopping-planner.md:77 says the on-screen list shows no trip feedback besides the banner. That is out of date since #25 and more so now.
  • Follow-up candidate: an ingredient with no product shows only "Need: X" and never names a trip.

Comment thread menu_management/lib/shopping/shopping_ingredient.dart Outdated
Comment thread menu_management/lib/shopping/shopping_ingredient.dart Outdated
guplem and others added 4 commits October 2, 2026 19:40
The largest-remainder split can round a small early need down to 0 and
move the whole buy to a later trip. The row then sent the user to that
later trip, although a recipe before it still needs the product.

A row that one trip buys now names the earliest trip that holds a trip
item of the ingredient. When that trip is the first trip of the plan,
the row shows the plain total, the same as a one-trip plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows of an equivalent group (one-of-each cycle) always got an empty
trip list, so they showed a plain total even when only a later trip
buys the ingredient. The user then read "buy it now" for food that the
plan buys on a later trip.

When one later trip buys the whole cover of the group, each visible
member row now gets one trip entry with its own cycled share, so it
prints its count and the trip label. A 2+ trip split still gives the
member rows the plain share, because a per-trip split of the solo
count would contradict the one-of-each share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The checks for a missing trip label searched for "week", but labels
write "Week", so they could never fail. They now name the exact wrong
rows, such as "Buy 6 packs now" and "Buy 6 packs Week 2".

New tests cover the real-date label of a single later trip, a count
that owned stock lowers, and parity with the copied list: the row
names the same trip that the copy puts the product under.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR 0014 still said that the banner and the copy are the only trip
feedback. Product rows now show a per-trip split or the label of a
single later trip, so the ADR says that, and why the label names the
earliest trip that holds the ingredient.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@guplem guplem left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2. Both Required items from round 1 are fixed: equivalent-product rows name a single later trip, and the single-trip row names the earliest trip that holds the ingredient. The weak textContaining("week") checks are now exact, and ADR 0014 records the per-row trip label.

  • [Suggestion] Equivalent group bought only on 2+ later trips still reads "buy now" (shopping_ingredient.dart, groupTripPurchases.length == 1 ? groupTripPurchases.single : null). Trips week 0 (rice only), week 1 and week 2 (pizza): each member row shows a plain "Buy 1 pack". Fix: name the earliest trip that holds the ingredient whenever it is not the first trip.
  • [Suggestion] ADR 0014 says the copy always equals the on-screen list (line 75). In the rounding case the row names an earlier trip than the copy section, on purpose. Say so, so nobody "fixes" one side back. The split itself is tracked in #75.
  • [Nitpick] if (purchases.length != 1) return purchases; // The split weighs ...: move the comment above the firstWhere that it explains.
  • [Nitpick] isFirstTrip: groupSingleTrip.isFirstTrip can only be false; write isFirstTrip: false as the standalone branch does.
  • [Nitpick] groupSingleTrip != null && packsToBuy > 0: the row shows "Covered" for 0 packs before it reads tripPurchases, so the check has no effect.
  • [Nitpick] The owned-stock test uses remainingGrams: 300 and a trip item of 300, so a count from the raw trip amount also passes. Use a different trip amount (for example 900).

guplem and others added 4 commits October 2, 2026 19:54
The single later-trip label needs the earliest trip that holds the
ingredient. Equivalent-group rows need the same lookup next, so it moves
into its own helper instead of a second copy.

The comment that explained the lookup sat at the end of the early-return
line. It now sits on the helper, and the comment above the early return
says what 0 and 2+ purchases give.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An equivalent group got a trip label only when exactly one later trip
bought its cover. When the first trip holds only other ingredients and
2+ later trips buy this one, each member row showed a plain "Buy 1 pack",
which reads as "buy now" although no recipe needs it before the later
trip.

Each member row now names the earliest trip that holds the ingredient
whenever that trip is not the first trip of the plan, with its own cycled
share. The isFirstTrip flag is now a plain false, since a later trip is
never the first one, and the packsToBuy > 0 guard goes away, because the
row shows "Covered" for 0 packs before it reads the trip purchases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trip item held 300 grams, the same as the remaining after owned
stock, so a row that counted packs from the raw trip amount also passed.
The trip item now holds 900 grams, so only a count from the 300 grams
remaining gives "Buy 3 packs Week 2". The comment now names the grams
that the test uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ADR said the copied per-trip amounts always equal the on-screen list.
In the rounding case the row names an earlier trip than the copied
section, on purpose, so the ADR now says so and points to issue #75 for
the split. This keeps a later change from "fixing" one side back to
match the other. It also records that equivalent-group rows use the same
earliest-trip label, also when 2+ later trips buy the ingredient.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@guplem
guplem enabled auto-merge October 2, 2026 17:56
@guplem
guplem merged commit c5b229f into main Oct 2, 2026
1 check passed
@guplem
guplem deleted the 73-single-later-trip-label branch October 2, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

shopping Shopping list feature waiting-for-human-check No human has verified this yet -- direct AI output

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shopping row hides the trip date when a product is bought only on a later trip

1 participant