# Phase 7 — Pantry & Fridge Tracking **Goal**: Track the lifecycle of physical food items — purchase, opening, preparation, consumption, or disposal. Estimate freshness/spoilage timelines. Provide a real-time dashboard of what's in the household's storage. **Depends on**: Phase 0, Phase 5 --- ## Deliverables 1. `PantryItem` and `FreshnessRule` MongoDB schemas 2. Full CRUD API with status transition workflow 3. Freshness estimation and urgency scoring 4. Scheduled freshness check job (cron) with in-app notifications 5. Pantry dashboard web UI with color-coded freshness 6. Waste analysis history --- ## Data Model ### PantryItem Schema ```typescript // packages/shared/src/types/pantry.ts export interface PantryItem { id: string; householdId: string; productId: string; productName: string; // Denormalized storageLocation: StorageLocation; quantity: number; unit: ServingUnit; purchaseDate: Date; expirationDate?: Date; // From packaging, if known openedDate?: Date; preparedDate?: Date; status: ItemStatus; freshnessEstimate: FreshnessEstimate; notes?: string; purchasePrice?: number; // Links to grocery tracking (Phase 9) storeId?: string; // Where it was bought createdBy: string; createdAt: Date; updatedAt: Date; } export enum StorageLocation { PANTRY = 'pantry', FRIDGE = 'fridge', FREEZER = 'freezer', COUNTER = 'counter', } export enum ItemStatus { SEALED = 'sealed', OPENED = 'opened', PREPARED = 'prepared', CONSUMED = 'consumed', DISCARDED = 'discarded', EXPIRED = 'expired', } export interface FreshnessEstimate { estimatedExpiryDate: Date; // Computed from rules daysRemaining: number; // Computed urgency: FreshnessUrgency; // Computed source: 'packaging' | 'rule' | 'manual'; } export enum FreshnessUrgency { FRESH = 'fresh', // > 5 days USE_SOON = 'use_soon', // 2-5 days URGENT = 'urgent', // 0-2 days CHECK = 'check', // Past estimated date, may still be ok EXPIRED = 'expired', // Way past date } ``` ### FreshnessRule Schema ```typescript // packages/shared/src/types/freshness.ts export interface FreshnessRule { id: string; householdId?: string; // null = system default category: ProductCategory; storageLocation: StorageLocation; shelfLifeDays: number; // When sealed openedLifeDays: number; // After opening freezerLifeDays?: number; // If moved to freezer spoilageSignsToCheck: string[]; // e.g., ['smell', 'discoloration', 'texture change'] tips?: string; // Storage tips source: 'system' | 'household'; // System defaults vs household overrides } ``` ### Status Transition Rules ``` ┌──────────┐ │ SEALED │ └─────┬─────┘ │ ┌────────┼────────┐ ▼ ▼ ▼ ┌─────────┐ ┌──────┐ ┌──────────┐ │ OPENED │ │CONSUMED│ │DISCARDED │ └────┬────┘ └──────┘ └──────────┘ │ ┌────┼────────┐ ▼ ▼ ▼ ┌──────┐┌──────────┐┌──────────┐ │PREPARED│ │CONSUMED │ │DISCARDED │ └───┬──┘ └──────────┘└──────────┘ │ ├──────────┐ ▼ ▼ ┌──────────┐┌──────────┐ │ CONSUMED ││ DISCARDED│ └──────────┘└──────────┘ ``` Valid transitions: - `sealed → opened | consumed | discarded` - `opened → prepared | consumed | discarded` - `prepared → consumed | discarded` - Any status → `expired` (set by system cron) ### MongoDB Indexes ```javascript { householdId: 1, status: 1, 'freshnessEstimate.urgency': 1 } { householdId: 1, storageLocation: 1, status: 1 } { householdId: 1, productId: 1, status: 1 } { householdId: 1, 'freshnessEstimate.estimatedExpiryDate': 1 } // For cron job { 'freshnessRule.category': 1, 'freshnessRule.storageLocation': 1 } // For rule lookup ``` --- ## API Endpoints ### PantryModule | Method | Path | Description | Auth | | ------ | -------------------------- | ------------------------------------------------------------- | ------ | | GET | `/pantry` | List pantry items (filtered, paginated) | member | | GET | `/pantry/:id` | Get single item | member | | POST | `/pantry` | Add item to pantry | member | | PATCH | `/pantry/:id` | Update item details | member | | POST | `/pantry/:id/transition` | Change status (open, consume, discard, etc.) | member | | POST | `/pantry/batch-transition` | Bulk status change (e.g., mark all as consumed after cooking) | member | | DELETE | `/pantry/:id` | Hard delete (admin) | admin | | GET | `/pantry/expiring-soon` | Items expiring within N days | member | | GET | `/pantry/stats` | Waste analysis summary | member | ### FreshnessRulesModule | Method | Path | Description | Auth | | ------ | ---------------------- | -------------------------------------------- | ------ | | GET | `/freshness-rules` | List rules (system + household overrides) | member | | POST | `/freshness-rules` | Create household override | admin | | PATCH | `/freshness-rules/:id` | Update household rule | admin | | DELETE | `/freshness-rules/:id` | Remove household override (revert to system) | admin | ### Query Parameters for GET `/pantry` ``` ?storageLocation=fridge # Filter by location &status=sealed,opened # Filter by status (comma-separated) &urgency=urgent,use_soon # Filter by freshness urgency &productId=abc123 # Filter by product &sort=-freshnessEstimate.daysRemaining # Sort by urgency (most urgent first) &cursor=abc123 &limit=20 ``` --- ## Tasks ### 7.1 — Shared Types & Validation - Add pantry types to `packages/shared/src/types/pantry.ts` - Add freshness types to `packages/shared/src/types/freshness.ts` - Zod schemas: - `CreatePantryItemSchema` - `UpdatePantryItemSchema` - `TransitionPantryItemSchema` — `{ status: ItemStatus, date?: Date, notes?: string }` - `CreateFreshnessRuleSchema` ### 7.2 — Freshness Rule Seed Data - Seed `FreshnessRule` collection with defaults based on USDA/StillTasty guidelines: | Category | Location | Sealed (days) | Opened (days) | Freezer (days) | | ---------- | -------- | ------------- | ------------- | -------------- | | Dairy | Fridge | 14 | 7 | 90 | | Meat | Fridge | 3 | 2 | 180 | | Poultry | Fridge | 2 | 1 | 270 | | Seafood | Fridge | 2 | 1 | 180 | | Fruits | Counter | 7 | 3 | 270 | | Vegetables | Fridge | 7 | 4 | 270 | | Grains | Pantry | 180 | 90 | 365 | | Legumes | Pantry | 365 | 7 | 365 | | Bakery | Counter | 5 | 3 | 90 | | ... | ... | ... | ... | ... | - Household can override any rule ### 7.3 — Freshness Calculation Service ```typescript class FreshnessService { /** * Given a pantry item and its applicable freshness rule: * 1. If packaging expirationDate exists, use it * 2. Else compute: purchaseDate + shelfLifeDays (sealed) or openedDate + openedLifeDays (opened) * 3. If in freezer, use freezerLifeDays from purchaseDate * 4. Calculate daysRemaining = estimatedExpiryDate - today * 5. Map to urgency: >5 = FRESH, 2-5 = USE_SOON, 0-2 = URGENT, <0 = CHECK/EXPIRED */ calculateFreshness(item: PantryItem, rule: FreshnessRule): FreshnessEstimate; /** * Find the most specific rule: household override > system default * Match by category + storageLocation */ findApplicableRule( householdId: string, category: ProductCategory, location: StorageLocation, ): FreshnessRule; } ``` ### 7.4 — Status Transition Service ```typescript class PantryTransitionService { /** * Validate transition is allowed, apply side effects: * - sealed → opened: set openedDate, recalculate freshness with openedLifeDays * - * → consumed: record consumption date, update quantity * - * → discarded: record discard date, log for waste analysis */ transition(item: PantryItem, newStatus: ItemStatus, metadata?: TransitionMetadata): PantryItem; } ``` ### 7.5 — Freshness Cron Job - NestJS `@Cron('0 6 * * *')` (daily at 6 AM, configurable): 1. Query all active pantry items (status: sealed/opened/prepared) 2. Recalculate freshness estimates 3. Items past expiry → update status to `expired` 4. Items in `urgent` or `check` → create in-app notifications - Notification model (simple for now, expand for push in mobile phase): ```typescript export interface Notification { id: string; householdId: string; userId?: string; // null = all household members type: 'freshness_warning' | 'item_expired'; title: string; body: string; relatedEntityId: string; // PantryItem ID isRead: boolean; createdAt: Date; } ``` ### 7.6 — Waste Analysis - `GET /pantry/stats` returns: ```typescript interface WasteStats { period: { start: Date; end: Date }; totalItemsConsumed: number; totalItemsDiscarded: number; wastePercentage: number; // discarded / (consumed + discarded) * 100 topWastedCategories: { category: ProductCategory; count: number }[]; topWastedProducts: { productId: string; productName: string; count: number }[]; trendVsPreviousPeriod: number; // % change } ``` - Query parameters: `?period=week|month|quarter|year` ### 7.7 — WebSocket Events (Initial) - Set up NestJS `@WebSocketGateway` with household-scoped rooms - Events: - `pantry:item-added` — when a new item is added - `pantry:item-updated` — when item status changes - `pantry:freshness-alert` — when cron detects urgent items - Frontend subscribes on pantry page for real-time updates across household members ### 7.8 — Web UI: Pantry Dashboard - `/pantry` page: - **Storage tabs**: Fridge | Freezer | Pantry | Counter | All - **View modes**: Grid (cards) | List (table) - Each item shows: - Product name, quantity - Freshness indicator: color-coded chip (green/yellow/orange/red) - Days remaining - Status badge - Quick-action buttons: Open | Consume | Discard - **Sort**: by urgency (default), name, purchase date - **Filter**: by urgency level, category - Floating "Add Item" button → modal: - Product autocomplete (from library) - Storage location picker - Purchase date (default today) - Expiration date (optional, from packaging) - Quantity + unit - `/pantry/stats` page: - Waste percentage gauge - Top wasted categories bar chart - Trend line chart (weekly waste over time) - **Notification bell** in top bar: shows freshness warnings, mark as read --- ## Acceptance Criteria - [ ] Can add items to pantry linked to products - [ ] Freshness estimate is calculated on creation and updates - [ ] Status transitions follow valid workflow rules - [ ] Daily cron job flags expiring items and creates notifications - [ ] Pantry dashboard shows items color-coded by freshness urgency - [ ] Waste stats endpoint returns correct aggregation - [ ] WebSocket broadcasts pantry changes to household members - [ ] Freshness rules can be overridden per household - [ ] Items sorted by urgency show most critical first --- ## Estimated Effort Medium-large. Freshness logic, cron job, notifications, and WebSocket add significant complexity beyond basic CRUD.