MeshiTrack/docs/phases/phase-7-pantry.md

12 KiB

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

// 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

// 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

{ 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

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

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):
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:
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.