Setup initial project

This commit is contained in:
Aerilyn Weber 2026-03-27 14:50:34 +09:00
commit db79af06f7
119 changed files with 20761 additions and 0 deletions

View file

@ -0,0 +1,346 @@
# 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.