Implements the architecture 4.8 resolution ladder: local SQLCipher cache ->
Open Food Facts (4s timeout) -> USDA FoodData Central fallback, with every
remote hit written through to the new FoodProductsTable (schema v5).
- domain: FoodProduct entity + IFoodSearchRepository contract (pure Dart)
- data: UsdaFdcClient (FDC nutrient-ID mapper 1008/1003/1004/1005/1079/1093,
api_key sent as query parameter - FDC ignores body keys and 403s otherwise),
OpenFoodFactsClient (userAgent with app identity, SearchTerms search,
getProductV3 barcode lookup), shared RemoteFoodDataSource interface
- presentation: FoodSearchController (400ms debounce, request-token
supersession), FoodSearchScreen (MobileScanner sheet on mobile, manual UPC
dialog + notification on desktop/web, macro action sheet with serving
multipliers, Log to Diary / Add to Pantry actions)
- integration: /food-search route with ?preset=pantry, Nutrition screen entry
button, Pantry screen Scan Item action
- hardening: FoodSearchUnavailableException distinguishes total source outage
from a healthy empty result; GoogleHealthConnector dotenv reads now guarded
with isInitialized (fixes NotInitializedError crashing app_navigation_test)
- tests: +16 (fallback sequence cache->OFF->USDA for text and barcode,
USDA request shape via MockClient); full suite 170/170, analyze clean
Domain (pure Dart):
- Add PantryItem and ShoppingListItem entities plus the PantryCategory aisle
enum (produce/meat_seafood/dairy/dry_goods/spices/other) with a keyword
infer() heuristic and ExpirationStatus warning buckets (expired /
expiringSoon within 3 days)
- Add measurement_units value object (mass/volume/count dimensions with
lb/oz/g/kg and ml/l/tsp/tbsp/cup conversion onto a shared base scale) and
ingredient_name canonicalizer (lowercase, parenthetical stripping,
plural folding: eggs -> egg, lbs -> lb)
- Define IPantryRepository contract covering inventory and checklist CRUD
- Add CalculateShoppingDelta use case: aggregates the active 7-day meal-plan
slot window's recipe ingredient manifests by canonical name, computes
deficit = totalRequired - currentPantryQuantity in base units (dominant
recipe unit wins display, 2-decimal rounding), upserts isAutoGenerated
rows with stable ids and source recipe titles, clears stale auto rows once
stock covers the plan, and never modifies or removes manual rows
Data:
- Bump AppDatabase to schema v4 (migration for v1-v3 installs) with
PantryItemsTable (UUID PK, canonical name, quantity/unit, aisle key,
nullable expirationDate, lastUpdated) and ShoppingListItemsTable (UUID PK,
required quantity/unit, purchased/auto flags, JSON sourceNotes for recipe
titles) plus watch/get/upsert/delete helpers
- Implement DriftPantryRepository (record <-> entity mapping, enum keys,
JSON recipe-title round-trip)
Presentation:
- Add PantryInventoryController (live watch stream, quick +/- stock steps
clamped at zero, aisle-grouping vs expiration-sort modes) and
ShoppingListController (manual add, All/To Buy/Checked filter, check-off
with "Add to Pantry?" unit-converting stock merge or aisle-inferred row
creation, syncWithMealPlan reading the planner's Monday-Sunday window and
recipe catalog)
- Refactor PantryScreen: Inventory/Shopping tab header with remaining-count
badge, Aisles/Expiration segmented control, categorized tiles with +/-
counters and expired/expiring badges, swipe-to-delete on both lists,
prominent "Sync with Meal Plan" banner with run summary + snackbar,
"From: <recipe>" origin chips on auto rows, checklist filters, check-off
-> pantry prompt, and Add Pantry Item / Add Shopping Item dialogs
Testing:
- 10 new tests in delta_calculator_test.dart: 300 g on hand vs 800 g
requirement -> +500 g entry, sufficient stock -> no entry, manual rows
preserved across repeated syncs, stale auto cleanup, cross-recipe
plural/unit normalization (4 eggs + 2 egg - 2 -> +4 egg), lb/oz scale
unification (0.75 lb), check-off -> pantry transfer (merge +
aisle-inferred create), end-to-end syncWithMealPlan, and inventory
canonicalization / quick-adjust clamp; full suite green (99/99)
- flutter analyze clean (No issues found!) after codegen; BUILD_STATE.md:
mark Phase 5 complete and record all verification gates