Setup initial project
This commit is contained in:
commit
db79af06f7
119 changed files with 20761 additions and 0 deletions
254
docs/phases/phase-5-product-library.md
Normal file
254
docs/phases/phase-5-product-library.md
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
# 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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```typescript
|
||||
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`:
|
||||
|
||||
```typescript
|
||||
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue