MeshiTrack/docs/phases/phase-9-grocery.md

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

  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

// 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
  • 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:
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-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.