Technical Design TD-001 — Prepared Foods & Leftovers
| Field | Value |
| ID | TD-001 |
| Title | Prepared Foods & Leftovers |
| Status | Draft |
| Owner | @souperk |
| Audience | contributors |
| Created | 2026-02-07 |
| Last Updated | 2026-02-07 |
| Reviewers | TBD |
| Approvers | TBD |
| Tracking | TBD |
| Related | v0.2 Roadmap, TD-002 |
1. Summary
- Cooking a recipe currently consumes inventory but creates no record of the resulting meal
- Users cannot track prepared food portions, storage locations, or expiry
- This TD introduces a
PreparedFood aggregate with activity tracking for portion consumption and discard - Enables the "what to eat soon" recommendation flow in v0.7
2. Context and Problem Statement
- After cooking a recipe, the system records inventory consumption but nothing about the output
- Users have no visibility into what prepared food exists, where it's stored, or when it expires
- Leftovers cannot be tracked; users must mentally track portion counts and freshness
2.1 Goals
- Track prepared foods with portions, storage location, and expiry window
- Record portion consumption and discard events with timestamps
- Optionally create a
PreparedFood when cooking a recipe - Provide an expiry cleanup view combining inventory batches and prepared foods
2.2 Out of Scope
- Quick meal templates (see TD-002)
- Nutrition data for prepared foods (v0.5)
- Automated expiry notifications (future)
2.3 Assumptions and Constraints
- Prepared foods are always created manually or via the cook-recipe flow
- Expiry is user-provided or derived from a configurable default window
- Storage locations reuse the existing
StorageLocation enum
3. Design Overview
- New
PreparedFood aggregate root with PreparedFoodActivity children PreparedFoodService managing lifecycle (create, consume portion, discard, list) - Cook-recipe flow gains an optional flag to create a
PreparedFood from the recipe output - Unified expiry endpoint returns both inventory batches and prepared foods sorted by expiry
3.1 Key Components
| Component | Purpose |
PreparedFood | Aggregate tracking a cooked meal with portions and expiry |
PreparedFoodActivity | Child entity recording portion consumption and discard events |
PreparedFoodStatus | Enum: ACTIVE, FINISHED, DISCARDED |
PreparedFoodActivityType | Enum: CONSUMED, DISCARDED |
PreparedFoodRepository | Data access for prepared foods |
PreparedFoodService | Domain service with @transactional methods |
PreparedFoodRouter | FastAPI router at /prepared-food |
3.2 Architecture
graph TB
Router[PreparedFoodRouter] --> Service[PreparedFoodService]
Service --> Repo[PreparedFoodRepository]
Service --> InvService[InventoryService]
Repo --> DB[(PostgreSQL)]
InvService --> InvRepo[InventoryBatchRepository]
3.3 Interface Impact (Before → After)
| Area | Before | After |
| Cook recipe | Consumes inventory, returns activity logs | Optionally also creates PreparedFood |
| Expiring-soon | Returns only inventory batches | Returns batches + prepared foods |
| Inventory cleanup | Not available | New endpoint filtering by location |
4. Implementation Details
4.1 Core Contracts and APIs
PreparedFood entity:
class PreparedFood(BaseAggregate):
name: Mapped[str]
recipe_id: Mapped[int | None] # optional link to recipe
user_id: Mapped[str]
portions_total: Mapped[int]
portions_remaining: Mapped[int]
storage_location: Mapped[StorageLocation]
prepared_at: Mapped[datetime]
expires_at: Mapped[datetime | None]
status: Mapped[PreparedFoodStatus]
activities: Mapped[list[PreparedFoodActivity]]
PreparedFoodActivity entity:
class PreparedFoodActivity(BaseEntity, AuditMixin):
prepared_food_id: Mapped[int]
activity_type: Mapped[PreparedFoodActivityType]
portions: Mapped[int]
notes: Mapped[str | None]
API endpoints:
| Method | Path | Description |
POST | /prepared-food | Create prepared food |
GET | /prepared-food | List prepared foods (filter by status, location) |
GET | /prepared-food/{id} | Get single prepared food |
POST | /prepared-food/{id}/consume | Consume portions |
POST | /prepared-food/{id}/discard | Discard (remaining portions) |
DELETE | /prepared-food/{id} | Delete prepared food |
GET | /inventory/cleanup | Unified expiry view (batches + prepared foods) |
4.2 Component Architecture
PreparedFood extends BaseAggregate with standard column helpers PreparedFoodActivity extends BaseEntity + AuditMixin - Parent-child via
relationship(cascade="all, delete-orphan", lazy="selectin") - Repository extends
BaseRepository
4.3 Integration Points
- Cook recipe flow:
InventoryService.cook_recipe gains optional create_prepared_food: bool parameter - Expiry cleanup: New query joining
InventoryBatch and PreparedFood filtered by expiry window and location
4.4 Error Handling and Edge Cases
| Error | When | Response |
PreparedFoodNotFoundError | ID doesn't exist | 404 |
InsufficientPortionsError | Consuming more portions than remaining | 400 |
PreparedFoodNotActiveError | Consuming from finished/discarded food | 400 |
5. Semantics
- When
portions_remaining reaches 0, status transitions to FINISHED - Discard sets status to
DISCARDED regardless of remaining portions expires_at is informational; expired foods remain ACTIVE until explicitly consumed or discarded - A prepared food created from cooking a recipe stores the
recipe_id for reference but is independent of the recipe
6. Examples
6.1 Create Prepared Food via Cook Recipe
# POST /recipe/{id}/cook with create_prepared_food=true
{
"user_id": "user-123",
"create_prepared_food": true,
"prepared_food_name": "Chicken Stir Fry",
"portions": 4,
"storage_location": "FRIDGE",
"expires_at": "2026-02-10T00:00:00Z"
}
6.2 Consume Portions
# POST /prepared-food/{id}/consume
{
"portions": 1,
"notes": "Lunch"
}
7. Test Matrix
- Unit: PreparedFood entity logic (portion tracking, status transitions)
- Integration: Full CRUD via API, cook→prepared food flow, expiry cleanup query
- Edge cases: Consume more than remaining, consume from finished/discarded, discard with 0 remaining
8. Acceptance Criteria
9. Rollout and Migration
9.1 Phased Plan
Phase 1: PreparedFood aggregate, repository, service, router, migration v07
Phase 2: Integration with cook-recipe flow
Phase 3: Unified expiry cleanup endpoint
9.2 Migration Strategy
- New tables only (
prepared_foods, prepared_food_activities) - New enums (
prepared_food_status, prepared_food_activity_type) - No changes to existing tables
10. Risks and Mitigations
| Risk | Mitigation |
| Expiry cleanup query performance with large datasets | Index on expires_at + status columns |
| Prepared food creation adds latency to cook flow | Make it optional (default off), separate transaction if needed |
11. Progress
12. Changelog
| Date | Change |
| 2026-02-07 | Initial draft |