12 KiB
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
PantryItemandFreshnessRuleMongoDB schemas- Full CRUD API with status transition workflow
- Freshness estimation and urgency scoring
- Scheduled freshness check job (cron) with in-app notifications
- Pantry dashboard web UI with color-coded freshness
- 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 | discardedopened → prepared | consumed | discardedprepared → 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:
CreatePantryItemSchemaUpdatePantryItemSchemaTransitionPantryItemSchema—{ status: ItemStatus, date?: Date, notes?: string }CreateFreshnessRuleSchema
7.2 — Freshness Rule Seed Data
- Seed
FreshnessRulecollection 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):- Query all active pantry items (status: sealed/opened/prepared)
- Recalculate freshness estimates
- Items past expiry → update status to
expired - Items in
urgentorcheck→ 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/statsreturns:
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
@WebSocketGatewaywith household-scoped rooms - Events:
pantry:item-added— when a new item is addedpantry:item-updated— when item status changespantry: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
/pantrypage:- 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/statspage:- 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.