Setup initial project
This commit is contained in:
commit
db79af06f7
119 changed files with 20761 additions and 0 deletions
357
docs/phases/phase-9-grocery.md
Normal file
357
docs/phases/phase-9-grocery.md
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
# 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 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
|
||||
- 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
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
Large. Real-time shopping sync, price analytics aggregations, store comparison algorithm, and the shopping-to-pantry flow involve significant logic and UI.
|
||||
Loading…
Add table
Add a link
Reference in a new issue