12 KiB
12 KiB
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
Store,PriceRecord,ShoppingListMongoDB schemas- Shopping list CRUD with real-time sync (WebSocket)
- Auto-generate shopping lists from meal plan gaps
- Price entry and history tracking
- Price analytics: cheapest store per product, per shopping list, trends
- Shopping → Pantry flow (checked items → add to pantry)
- Web UI: shopping lists, price history charts, store comparison
Data Model
Store Schema
// 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
// 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
// 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
// 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
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
- Events:
- Optimistic updates on frontend with server reconciliation
9.5 — Auto-Generate from Meal Plan
POST /shopping-lists/from-meal-plan/:planId:- Call Phase 8's shopping gap analysis for the meal plan
- For each item in
needToBuy:- Create a
ShoppingItemlinked to the product - Call
PriceService.estimatePrice()to pre-fill estimated price - Set
categoryfor store aisle grouping
- Create a
- Optionally group by cheapest store per item
- 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
PantryItemin Phase 7 (status: sealed, purchaseDate: today) - If
actualPricewas entered, create aPriceRecord - Mark
addedToPantry: true
- Create a
- Return summary:
{ addedCount, priceRecordsCreated }
- For each checked (purchased) item with
9.7 — Price Analytics
GET /prices/analytics:
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?
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-listspage:- 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/:idpage (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?"
/storespage:- Store list with CRUD
- Per-store: total spent, last visit, product count
/pricespage (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.