MeshiTrack/docs/phases/phase-5-product-library.md

7.7 KiB

Phase 5 — Product Library

Goal: A searchable catalog of food products with nutrition data, reusable across the entire food tracking domain. Products are the atomic building blocks for recipes, pantry items, and shopping lists.

Depends on: Phase 0 (auth, households, shared types). Can reuse Store infrastructure from Phase 4.


Deliverables

  1. Product MongoDB schema and full CRUD API
  2. Full-text search with filters
  3. Barcode lookup via Open Food Facts
  4. Bulk import (CSV/JSON)
  5. Product library web UI (search, add, edit)
  6. LLM provider interface (ILlmProvider) with no-op implementation
  7. "Smart Add" endpoint placeholder

Data Model

Product Schema

// packages/shared/src/types/product.ts
export interface Product {
  id: string;
  householdId: string;
  name: string;
  brand?: string;
  barcode?: string;
  category: ProductCategory;
  servingSize: number;
  servingUnit: ServingUnit;
  nutrition: NutritionInfo;
  tags: string[];
  imageUrl?: string;
  isPublic: boolean; // Visible to all households (for shared catalog)
  source: ProductSource; // 'manual' | 'barcode_lookup' | 'llm' | 'import'
  createdBy: string; // userId
  createdAt: Date;
  updatedAt: Date;
}

export interface NutritionInfo {
  calories: number; // kcal per serving
  protein: number; // grams
  carbs: number; // grams
  fat: number; // grams
  fiber?: number; // grams
  sugar?: number; // grams
  sodium?: number; // mg
  saturatedFat?: number; // grams
  cholesterol?: number; // mg
}

export enum ProductCategory {
  DAIRY = 'dairy',
  MEAT = 'meat',
  POULTRY = 'poultry',
  SEAFOOD = 'seafood',
  FRUITS = 'fruits',
  VEGETABLES = 'vegetables',
  GRAINS = 'grains',
  LEGUMES = 'legumes',
  NUTS_SEEDS = 'nuts_seeds',
  OILS_FATS = 'oils_fats',
  CONDIMENTS = 'condiments',
  SPICES = 'spices',
  BEVERAGES = 'beverages',
  SNACKS = 'snacks',
  FROZEN = 'frozen',
  CANNED = 'canned',
  BAKERY = 'bakery',
  DELI = 'deli',
  SUPPLEMENTS = 'supplements',
  OTHER = 'other',
}

export enum ServingUnit {
  GRAMS = 'g',
  MILLILITERS = 'ml',
  OUNCES = 'oz',
  CUPS = 'cup',
  TABLESPOONS = 'tbsp',
  TEASPOONS = 'tsp',
  PIECES = 'piece',
  SLICES = 'slice',
}

export enum ProductSource {
  MANUAL = 'manual',
  BARCODE_LOOKUP = 'barcode_lookup',
  LLM = 'llm',
  IMPORT = 'import',
}

MongoDB Indexes

// Text index for search
{ name: 'text', brand: 'text', tags: 'text' }

// Compound indexes
{ householdId: 1, category: 1 }
{ householdId: 1, barcode: 1 }       // unique within household
{ householdId: 1, name: 1, brand: 1 } // near-unique for dedup

API Endpoints

ProductsModule

Method Path Description Auth
GET /products List/search products (paginated) member
GET /products/:id Get single product member
POST /products Create product member
PATCH /products/:id Update product member
DELETE /products/:id Soft-delete product admin
GET /products/barcode/:code Lookup by barcode (local → Open Food Facts) member
POST /products/import Bulk import from CSV/JSON admin
POST /products/smart-add LLM-powered add from text/image member

Query Parameters for GET /products

?q=chicken                    # Full-text search
&category=meat                # Filter by category
&tags=organic,fresh           # Filter by tags (AND)
&cursor=abc123                # Cursor-based pagination
&limit=20                     # Page size (max 100)
&sort=name|-updatedAt         # Sort field, prefix - for desc

Response Shape

interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    cursor: string | null; // null = last page
    hasMore: boolean;
    total: number;
  };
}

Tasks

5.1 — Shared Types & Validation

  • Add all types above to packages/shared/src/types/product.ts
  • Add enums to packages/shared/src/enums/
  • Create Zod schemas:
    • CreateProductSchema — validates create payload
    • UpdateProductSchema — partial, validates update payload
    • ProductQuerySchema — validates query params

5.2 — Mongoose Schema & Repository

  • packages/api/src/modules/products/schemas/product.schema.ts
  • ProductRepository with:
    • findByHousehold(householdId, query) — supports text search, filters, cursor pagination
    • findByBarcode(householdId, barcode)
    • create(data)
    • update(id, householdId, data)
    • softDelete(id, householdId)
    • bulkCreate(items[])

5.3 — Barcode Lookup Service

  • BarcodeService:
    • First check local DB for matching barcode
    • If not found, query Open Food Facts API (https://world.openfoodfacts.org/api/v2/product/{barcode})
    • Map OFF response to Product shape
    • Cache results in local DB with source: 'barcode_lookup'

5.4 — LLM Provider Interface

  • packages/api/src/modules/llm/interfaces/llm-provider.interface.ts:
export interface ILlmProvider {
  extractNutrition(input: {
    text?: string;
    image?: Buffer;
  }): Promise<NutritionExtractionResult | null>;
  parseRecipe(text: string): Promise<ParsedRecipe | null>;
  parseRecipeFromUrl(url: string): Promise<ParsedRecipe | null>;
  parseReceipt(image: Buffer): Promise<ParsedReceipt | null>;
  suggestMealPlan(context: MealPlanContext): Promise<MealPlanSuggestion | null>;
  parseNaturalLanguage(text: string): Promise<StructuredAction | null>;
}

export const LLM_PROVIDER = Symbol('LLM_PROVIDER');
  • NoOpLlmProvider: implements interface, returns null for all methods, logs a warning
  • LlmModule: provides LLM_PROVIDER via factory, selectable by env var LLM_PROVIDER_TYPE

5.5 — Smart Add Endpoint

  • POST /products/smart-add accepts { text?: string, image?: file }
  • Calls ILlmProvider.extractNutrition()
  • If LLM returns data, pre-fill a product and return to client for review (not auto-saved)
  • If LLM unavailable (NoOpLlmProvider), return { available: false, message: 'LLM not configured' }

5.6 — Import Endpoint

  • POST /products/import accepts multipart CSV or JSON file
  • Validate each row against CreateProductSchema
  • Return summary: { imported: N, skipped: M, errors: [...] }
  • CSV column mapping: name, brand, barcode, category, servingSize, servingUnit, calories, protein, carbs, fat, ...

5.7 — Web UI: Product Library

  • /products page:
    • Search bar with debounced full-text search
    • Category filter dropdown
    • Tag filter chips
    • Product grid/list view (toggle)
    • Each product card shows: name, brand, category icon, calories/serving
  • Add/Edit product modal:
    • Form fields for all product properties
    • Nutrition input section with per-serving values
    • Barcode field with "Lookup" button
    • "Smart Add" tab (text input or image upload)
  • Import dialog: file upload with preview and error display

Acceptance Criteria

  • Can create, read, update, delete products via API
  • Full-text search returns relevant results
  • Barcode lookup fetches from Open Food Facts when not in local DB
  • Bulk import processes a CSV with 100+ products
  • Web UI allows searching, filtering, adding, and editing products
  • ILlmProvider interface is defined and injectable
  • Smart Add endpoint returns graceful "not available" with NoOp provider
  • All product queries are scoped to householdId

Estimated Effort

Medium. Straightforward CRUD with search; barcode integration adds some complexity.