# Phase 9 — Grocery & Price Tracking **Goal**: Track food shopping across stores, compare prices over time, optimize where to buy. Auto-generate shopping lists from meal plans (Phase 8) or manually. Close the loop: when items are purchased, add them to the pantry (Phase 7). Reuses Store infrastructure from Phase 4. **Depends on**: Phase 0, Phase 4 (stores), Phase 5 (products), Phase 7 (pantry), Phase 8 (meal planning) --- ## Deliverables 1. `Store`, `PriceRecord`, `ShoppingList` MongoDB schemas 2. Shopping list CRUD with real-time sync (WebSocket) 3. Auto-generate shopping lists from meal plan gaps 4. Price entry and history tracking 5. Price analytics: cheapest store per product, per shopping list, trends 6. Shopping → Pantry flow (checked items → add to pantry) 7. Web UI: shopping lists, price history charts, store comparison --- ## Data Model ### Store Schema ```typescript // packages/shared/src/types/store.ts export interface Store { id: string; householdId: string; name: string; address?: string; location?: { lat: number; lng: number; }; url?: string; notes?: string; tags: string[]; // e.g., 'organic', 'bulk', 'discount' isActive: boolean; createdBy: string; createdAt: Date; updatedAt: Date; } ``` ### PriceRecord Schema ```typescript // packages/shared/src/types/price.ts export interface PriceRecord { id: string; householdId: string; productId: string; productName: string; // Denormalized storeId: string; storeName: string; // Denormalized price: number; currency: string; // Default from household settings quantity: number; // How much for this price unit: ServingUnit; pricePerUnit: number; // Computed: price / quantity (normalized) date: Date; receiptImageUrl?: string; notes?: string; createdBy: string; createdAt: Date; } ``` ### ShoppingList Schema ```typescript // packages/shared/src/types/shopping-list.ts export interface ShoppingList { id: string; householdId: string; name: string; items: ShoppingItem[]; status: ShoppingListStatus; createdFrom?: ShoppingListSource; mealPlanId?: string; totalEstimatedCost?: number; // Sum of estimated prices preferredStoreId?: string; completedAt?: Date; createdBy: string; createdAt: Date; updatedAt: Date; } export interface ShoppingItem { id: string; // UUID for real-time sync reference productId?: string; // Linked product (optional for custom items) customName?: string; // For items not in product library quantity: number; unit: ServingUnit; checked: boolean; checkedAt?: Date; checkedBy?: string; // userId who checked it off estimatedPrice?: number; // From price history actualPrice?: number; // Entered when checked off storeId?: string; // Preferred store for this item notes?: string; category?: ProductCategory; // For grouping in shopping aisle order addedToPantry: boolean; // Tracks if item was added to pantry after purchase } export enum ShoppingListStatus { ACTIVE = 'active', SHOPPING = 'shopping', // Currently at the store COMPLETED = 'completed', ARCHIVED = 'archived', } export interface ShoppingListSource { type: 'meal_plan' | 'manual' | 'pantry_restock'; referenceId?: string; // MealPlan ID, etc. } ``` ### MongoDB Indexes ```javascript // PriceRecord { householdId: 1, productId: 1, storeId: 1, date: -1 } // Price history per product per store { householdId: 1, productId: 1, date: -1 } // Price history per product (all stores) { householdId: 1, storeId: 1, date: -1 } // All purchases at a store { date: 1, expireAfterSeconds: 63072000 } // Optional TTL: 2 years // ShoppingList { householdId: 1, status: 1 } { householdId: 1, createdAt: -1 } // Store { householdId: 1, name: 1 } ``` --- ## API Endpoints ### StoresModule | Method | Path | Description | Auth | | ------ | ------------- | ------------------------- | ------ | | GET | `/stores` | List stores for household | member | | POST | `/stores` | Add a store | member | | PATCH | `/stores/:id` | Update store | member | | DELETE | `/stores/:id` | Deactivate store | admin | ### PriceRecordsModule | Method | Path | Description | Auth | | ------ | ---------------------------- | ------------------------------------- | ------ | | POST | `/prices` | Record a price | member | | POST | `/prices/bulk` | Record multiple prices (from receipt) | member | | GET | `/prices/history/:productId` | Price history for a product | member | | GET | `/prices/compare/:productId` | Compare stores for a product | member | | GET | `/prices/analytics` | Aggregated price analytics | member | | POST | `/prices/parse-receipt` | LLM receipt parsing (placeholder) | member | ### ShoppingListsModule | Method | Path | Description | Auth | | ------ | ---------------------------------------- | ---------------------------------------------- | ------ | | GET | `/shopping-lists` | List shopping lists | member | | GET | `/shopping-lists/:id` | Get shopping list | member | | POST | `/shopping-lists` | Create shopping list | member | | PATCH | `/shopping-lists/:id` | Update list metadata | member | | DELETE | `/shopping-lists/:id` | Delete list | admin | | POST | `/shopping-lists/:id/items` | Add item to list | member | | PATCH | `/shopping-lists/:id/items/:itemId` | Update item (check off, change qty, set price) | member | | DELETE | `/shopping-lists/:id/items/:itemId` | Remove item from list | member | | POST | `/shopping-lists/from-meal-plan/:planId` | Auto-generate from meal plan gap analysis | member | | POST | `/shopping-lists/:id/add-to-pantry` | Move checked items to pantry | member | | GET | `/shopping-lists/:id/store-comparison` | Best store(s) for this list | member | --- ## Tasks ### 9.1 — Shared Types & Validation - Add all types above to `packages/shared` - Zod schemas for all create/update operations ### 9.2 — Stores CRUD - Standard CRUD, straightforward ### 9.3 — Price Record Service ```typescript class PriceService { /** Record a price, computing pricePerUnit */ recordPrice(data: CreatePriceRecord): Promise; /** Get price history for a product, optionally filtered by store */ getPriceHistory( productId: string, householdId: string, options?: { storeId?: string; startDate?: Date; endDate?: Date; limit?: number; }, ): Promise; /** Compare current prices across stores for a product */ compareStores(productId: string, householdId: string): Promise; /** Estimate price for a product based on recent history */ estimatePrice(productId: string, householdId: string, storeId?: string): Promise; /** Detect significant price changes */ detectPriceChanges(householdId: string): Promise; } ``` ### 9.4 — Shopping List CRUD & Real-Time Sync - Standard CRUD - **WebSocket integration**: shopping list room per list ID - Events: `shopping:item-checked`, `shopping:item-added`, `shopping:item-removed`, `shopping:item-updated` - Enables multiple household members to shop simultaneously with real-time checkoff sync - Optimistic updates on frontend with server reconciliation ### 9.5 — Auto-Generate from Meal Plan - `POST /shopping-lists/from-meal-plan/:planId`: 1. Call Phase 8's shopping gap analysis for the meal plan 2. For each item in `needToBuy`: - Create a `ShoppingItem` linked to the product - Call `PriceService.estimatePrice()` to pre-fill estimated price - Set `category` for store aisle grouping 3. Optionally group by cheapest store per item 4. Return the created shopping list ### 9.6 — Shopping → Pantry Flow - `POST /shopping-lists/:id/add-to-pantry`: - For each checked (purchased) item with `addedToPantry: false`: - Create a `PantryItem` in Phase 7 (status: sealed, purchaseDate: today) - If `actualPrice` was entered, create a `PriceRecord` - Mark `addedToPantry: true` - Return summary: `{ addedCount, priceRecordsCreated }` ### 9.7 — Price Analytics - `GET /prices/analytics`: ```typescript interface PriceAnalytics { /** Average basket cost per store over the last N trips */ averageBasketByStore: { storeId: string; storeName: string; avgTotal: number; tripCount: number; }[]; /** Products with significant price increases */ priceAlerts: PriceAlert[]; /** Total spending per period */ spendingOverTime: { period: string; total: number }[]; /** Most expensive categories */ spendingByCategory: { category: ProductCategory; total: number; avgPerItem: number }[]; } interface PriceAlert { productId: string; productName: string; storeId: string; storeName: string; previousPrice: number; currentPrice: number; changePercent: number; date: Date; } ``` ### 9.8 — Store Comparison for Shopping List - `GET /shopping-lists/:id/store-comparison`: - For each item in the list, find the cheapest recent price per store - Calculate total list cost per store - Suggest: "Buy everything at Store A: $X" vs "Split between stores: $Y" - Consider: is the savings worth going to multiple stores? ```typescript interface StoreComparisonResult { singleStoreOptions: { storeId: string; storeName: string; estimatedTotal: number; itemsCovered: number; // Not all stores carry all products itemsMissing: string[]; }[]; splitStoreOption?: { stores: { storeId: string; storeName: string; items: string[]; subtotal: number }[]; estimatedTotal: number; savingsVsBestSingleStore: number; }; } ``` ### 9.9 — Receipt Parsing Placeholder - `POST /prices/parse-receipt`: - Accepts image upload - Calls `ILlmProvider.parseReceipt(image)` - Expected return: `{ storeName, date, items[]: { name, price, quantity } }` - Match items to products (fuzzy), match store to stores - With `NoOpLlmProvider`: returns `{ available: false }` ### 9.10 — Web: API Client Services - `packages/web/src/services/shopping-lists.ts` — fetch functions for shopping list endpoints - `packages/web/src/services/prices.ts` — fetch functions for price record endpoints - Unit tests in `packages/web/src/services/__tests__/shopping-lists.test.ts` - Unit tests in `packages/web/src/services/__tests__/prices.test.ts` ### 9.11 — Web UI: Grocery Management - `/shopping-lists` page: - Active lists at top, completed/archived below - "New List" button (manual or from meal plan) - Each list card: name, item count, estimated cost, completion % - `/shopping-lists/:id` page (the "shopping mode"): - Items grouped by category (aisle order) - Each item: checkbox, name, quantity, estimated price - Check off: expand to enter actual price (optional) - Real-time sync indicator ("2 members shopping") - "Done Shopping" button — prompts "Add items to pantry?" - `/stores` page: - Store list with CRUD (already exists from Phase 4, extend if needed) - Per-store: total spent, last visit, product count - `/prices` page (analytics): - Product search — price history line chart (per store, color-coded) - Store comparison table - Spending over time bar chart - Price alerts panel - **Shopping list widget on dashboard**: shows active lists with quick-check functionality - Component tests for all pages and interactive components ### 9.12 — CI Verification - `npm run build` passes - `npm run test:cov` passes (100% lines/functions/statements, 90% branches) - `npm run lint` passes --- ## Acceptance Criteria - [ ] Can create shopping lists manually and from meal plans - [ ] Shopping list items sync in real-time across household members via WebSocket - [ ] Can record prices and view price history per product - [ ] Store comparison recommends cheapest store for a shopping list - [ ] Checked off items can be added to pantry with one action - [ ] Price analytics show spending trends and alerts - [ ] Auto-generated lists from meal plans correctly reflect shopping gap - [ ] Receipt parsing endpoint delegates to LLM provider - [ ] Web UI has component tests for all pages and interactive components - [ ] `npm run build`, `npm run test:cov`, and `npm run lint` all pass --- ## Estimated Effort Large. Real-time shopping sync, price analytics aggregations, store comparison algorithm, and the shopping-to-pantry flow involve significant logic and UI.