373 lines
13 KiB
Markdown
373 lines
13 KiB
Markdown
# 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<PriceRecord>;
|
|
|
|
/** 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<PriceRecord[]>;
|
|
|
|
/** Compare current prices across stores for a product */
|
|
compareStores(productId: string, householdId: string): Promise<StoreComparison[]>;
|
|
|
|
/** Estimate price for a product based on recent history */
|
|
estimatePrice(productId: string, householdId: string, storeId?: string): Promise<number | null>;
|
|
|
|
/** Detect significant price changes */
|
|
detectPriceChanges(householdId: string): Promise<PriceAlert[]>;
|
|
}
|
|
```
|
|
|
|
### 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.
|