feat(shopping-list) Fill a shopping list from a meal plan (spec 004) #64
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "004-fill-list-from-meal-plan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements spec 004 (fill a shopping list from a meal plan) in recipe-backend.
docs/plans/004-fill-list-from-meal-plan.md../recipe-platform/docs/features/004-fill-list-from-meal-plan.mdcontract.ref=cf101fc4b28dbd3665fd296324b1a8be2316e60a(tip of the unmerged platform branch004-fill-list-from-meal-plan-contract)What was implemented, by plan step
1780012800014-AddMealPlanReasonsAndItemContentEdit: themeal_planenum value; the reason's meal plan columns with FKsON DELETE SET NULL, the check constraint and two indexes;content_edited_at/content_edited_byon items. Entities updated to match.isValidIsoDate,itemUnitsMatch, and the puresrc/utils/shoppingListFillUtils.ts: scaling, normalization, rounding, spelling, grouping into connected components plus split, lines, keys, token (fp1.+ sha256), and the write plan. Unit-tested without mocks, one test per spec rule and acceptance criterion.SOURCE_MEAL_PLAN_*codes added to the 403/404 code unions.MealPlanPermissionHelper(pure, extracted fromMealPlanHandler),MealPlanTransactionHelper, meal plan and recipe repositories inShoppingListTransactionRepositories, and the newMealPlanRepositorylocks and reads.MealPlanHandler.moveDish, which locks plan, then days, then the dish. Day create takes the plan lock first; day saves reject dish ids of other days. NewMealPlanDishRestResource(POST /meal-plan-dish/:dishId/move).ShoppingListItemRepository: an edit marker set by aCASE … IS DISTINCT FROMin the edit statement, a coverage query using the dish identity rule, the fill's conditional writes, reason inserts and deletes, anddeleteMealPlanItemsOnList. PlusRecipeRepository.lockForKeyShare.editItemsets the edit marker;removeTickednow takes the list lock (resolves recipe-backend#27); newremoveMealPlanItems.ShoppingListFillHandler:previewtakes no lock.fillruns in one transaction in the order list row → KEY SHARE on plan, dishes and recipes → items (ascending id) → reasons → change row last.mealPlanreason block inShoppingListItemDtoMapper, and a newShoppingListFillReviewMapper.POST /shopping-list/:id/fill-preview,/filland/remove-meal-plan-items; factories wired.fillPreview,fill,removeMealPlanItems, and the newMealPlanDishRestResource/move. Folder seqs renumbered.ShoppingListFill.integration.test.tsand sharedmealPlanFixtureHelper.ts. The change-time file is extended: remove-ticked moved to the list-locked paths; fill and remove-meal-plan-items were added to change-row-last, recorded/not-recorded and the concurrency matrix.docs/howto-locking.mdanddocs/architecture.mdupdated.Deviations (recorded in the plan's Deviations section)
MealPlanRepository.updateDay(), after the day row is locked and its version checked. The plan had it in the handler beforeupdateDay(), using a newfindDishIdsOfDay, which was not added. In that position, saving the source day with its pre-move version got 400 for the moved dish id instead of the 409 that the contract and the move ADR promise. The plan's own comment there expected 409. Found by integration test 8.Non-obvious judgment calls
tickedCoverage). This follows the plan's rule that such a dish "can never be covered".removeNoLongerPlannedit therefore gives exactly the reviewed result; without it, the gone shares stay included.moveDishToDayandincrementDayVersionare raw SQL. They setupdate_dateexplicitly. The dish's ownversionis not bumped; the ADR allows that either way, since day saves do not check dish versions.meal_plan_day_id,recipe_id,meal_plan_id) appear in raw where clauses. This avoids relying on TypeORM's relation-name resolution in string conditions.remove-ticked, beforereorder, inShoppingListRestResource.docs/architecture.md§5 saidmeal_plan_day_dish → recipedoes not cascade. The migration hasON DELETE CASCADE, which matters for reasons: deleting a recipe deletes its dishes. I corrected it.Review fixes (commit
619e941)src/utils/fillReviewUtils.ts: line checkbox label, "kommt zu „…“ auf der Liste → …", "Du hast … auf … geändert." (fallback "–"), the edited-item radio legend "Bearbeiteten Eintrag „…“", the "Dazu addieren" hint "→ …", the join toggle label and text, skipped dish "… – auf …", "…: keine Zutaten", gone dish "für … (…)", and the no-longer-planned entry details (quantity, "(von dir geändert)", outcome). The create-list dialog's texts inFillReviewInputsbecame module constants.SelectFieldgains an optional nativedisabled, which the list picker now uses (blocked for keyboard too). The date pickers already used nativedisabled.moveDish: comment stating the accepted residual risk (a note save firing during the move hits the 409 dialog; practically unreachable)./verify-uiis arranged by the platform session.Verification
npm run build: passes.npm test: 62 files, 801 tests, all pass.npm run test:integration: 81 tests, all pass in three runs in a row. This includes the deterministic lock tests, change-row-last for fill and remove-meal-plan-items, and 20 rounds of each concurrent pair. CI does not run these tests.npm run migrationon the local dev DB: applied. Schema checked (columns, FKsSET NULL, check constraint, indexes, enum{manual,meal_plan}). Thendown(), thenup()again: both work.A manual API scenario against the running backend passed in full, with two throwaway users: A owns the plan and the list; B has READONLY on the plan and READ_WRITE on the list. It covered:
tickedCoverage;clearTicked;noLongerPlannedand its removal;The test data was removed afterwards. Bruno itself was not used (its environment credentials are secret); the same requests were sent with curl and locally signed tokens.
contract_pinCI check: expected red until the platform contract PR is merged and the check is re-run. Judge this PR by its build and test checks.🤖 Generated with Claude Code
WIP: feat(shopping-list) Fill a shopping list from a meal plan (spec 004)to feat(shopping-list) Fill a shopping list from a meal plan (spec 004)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.