Public Access
feat: implement Phase 2 - Alembic migrations, Pydantic schemas, and real API endpoints
- Add initial Alembic migration with full PostgreSQL schema (enums, tables, indexes, constraints) - Add seed data migration with basic ingredients (70+) and family profile - Add Pydantic schemas for all models (FamilyProfile, Recipe, MealPlan, etc.) - Implement /api/profile endpoints (CRUD, family member management) - Implement /api/recipes endpoints (CRUD, ingredients, filtering) - Implement /api/meals endpoints (meal plans, voting, approval tokens) - Implement /api/pantry endpoints (CRUD for home pantry) - Implement /api/shopping-list endpoints (aggregation, print-ready HTML) - Implement /api/admin endpoints (scrape trigger, logs, stats) - Update ORIENTATION.md with Phase 2 progress
This commit is contained in:
+182
-10
@@ -1,20 +1,192 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from app.database import get_db
|
||||
from app.models import (
|
||||
MealPlan, MealPlanItem, MealPlanVote, Recipe,
|
||||
FamilyProfile, FamilyMember, ApprovalToken,
|
||||
MealPlanStatus, MealPlanItemStatus, MealType, ApprovalTokenStatus
|
||||
)
|
||||
from app.schemas import (
|
||||
MealPlanResponse, MealPlanCreate,
|
||||
MealPlanItemResponse, VoteRequest, VoteResponse
|
||||
)
|
||||
from uuid import UUID
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/planned")
|
||||
@router.get("/planned", response_model=Optional[MealPlanResponse])
|
||||
def get_planned_meals(db: Session = Depends(get_db)):
|
||||
return {"message": "Planned meals endpoint - not yet implemented"}
|
||||
profile = db.query(FamilyProfile).first()
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="Family profile not found")
|
||||
|
||||
meal_plan = db.query(MealPlan).filter(
|
||||
MealPlan.family_profile_id == profile.id
|
||||
).order_by(MealPlan.week_start_date.desc()).first()
|
||||
|
||||
if not meal_plan:
|
||||
return None
|
||||
|
||||
return meal_plan
|
||||
|
||||
|
||||
@router.post("/{meal_id}/approve")
|
||||
def approve_meal(meal_id: str, db: Session = Depends(get_db)):
|
||||
return {"message": f"Approve meal {meal_id} - not yet implemented"}
|
||||
@router.post("/", response_model=MealPlanResponse)
|
||||
def create_meal_plan(meal_plan_data: MealPlanCreate, db: Session = Depends(get_db)):
|
||||
profile = db.query(FamilyProfile).first()
|
||||
if not profile:
|
||||
raise HTTPException(status_code=404, detail="Family profile not found")
|
||||
|
||||
existing = db.query(MealPlan).filter(
|
||||
MealPlan.family_profile_id == profile.id,
|
||||
MealPlan.week_start_date == meal_plan_data.week_start_date
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Meal plan for this week already exists")
|
||||
|
||||
db_meal_plan = MealPlan(
|
||||
family_profile_id=profile.id,
|
||||
week_start_date=meal_plan_data.week_start_date,
|
||||
status=MealPlanStatus[meal_plan_data.status.value.upper()],
|
||||
approval_deadline=meal_plan_data.approval_deadline,
|
||||
notes=meal_plan_data.notes
|
||||
)
|
||||
|
||||
db.add(db_meal_plan)
|
||||
db.flush()
|
||||
|
||||
for item_data in meal_plan_data.items:
|
||||
db_item = MealPlanItem(
|
||||
meal_plan_id=db_meal_plan.id,
|
||||
recipe_id=item_data.recipe_id,
|
||||
day_of_week=item_data.day_of_week,
|
||||
meal_type=MealType[item_data.meal_type.value.upper()],
|
||||
estimated_cost=item_data.estimated_cost
|
||||
)
|
||||
db.add(db_item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_meal_plan)
|
||||
return db_meal_plan
|
||||
|
||||
|
||||
@router.post("/{meal_id}/deny")
|
||||
def deny_meal(meal_id: str, db: Session = Depends(get_db)):
|
||||
return {"message": f"Deny meal {meal_id} - not yet implemented"}
|
||||
@router.get("/{meal_plan_id}", response_model=MealPlanResponse)
|
||||
def get_meal_plan(meal_plan_id: UUID, db: Session = Depends(get_db)):
|
||||
meal_plan = db.query(MealPlan).filter(MealPlan.id == meal_plan_id).first()
|
||||
if not meal_plan:
|
||||
raise HTTPException(status_code=404, detail="Meal plan not found")
|
||||
return meal_plan
|
||||
|
||||
|
||||
@router.post("/{meal_plan_id}/lock")
|
||||
def lock_meal_plan(meal_plan_id: UUID, db: Session = Depends(get_db)):
|
||||
meal_plan = db.query(MealPlan).filter(MealPlan.id == meal_plan_id).first()
|
||||
if not meal_plan:
|
||||
raise HTTPException(status_code=404, detail="Meal plan not found")
|
||||
|
||||
meal_plan.status = MealPlanStatus.LOCKED
|
||||
db.commit()
|
||||
return {"message": "Meal plan locked", "status": meal_plan.status.value}
|
||||
|
||||
|
||||
@router.get("/items/{item_id}/vote/{token}")
|
||||
def get_vote_page(item_id: UUID, token: str, db: Session = Depends(get_db)):
|
||||
approval_token = db.query(ApprovalToken).filter(ApprovalToken.token == token).first()
|
||||
if not approval_token:
|
||||
raise HTTPException(status_code=404, detail="Invalid token")
|
||||
|
||||
if approval_token.meal_plan_item_id != item_id:
|
||||
raise HTTPException(status_code=400, detail="Token not valid for this meal")
|
||||
|
||||
if approval_token.status != ApprovalTokenStatus.ACTIVE:
|
||||
raise HTTPException(status_code=400, detail="Token has already been used or expired")
|
||||
|
||||
if approval_token.expires_at < datetime.now():
|
||||
raise HTTPException(status_code=400, detail="Token has expired")
|
||||
|
||||
item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Meal plan item not found")
|
||||
|
||||
return {
|
||||
"item_id": str(item_id),
|
||||
"family_member_id": str(approval_token.family_member_id),
|
||||
"meal_plan_item": item
|
||||
}
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/vote/{token}", response_model=VoteResponse)
|
||||
def submit_vote(item_id: UUID, token: str, vote_data: VoteRequest, db: Session = Depends(get_db)):
|
||||
approval_token = db.query(ApprovalToken).filter(ApprovalToken.token == token).first()
|
||||
if not approval_token:
|
||||
raise HTTPException(status_code=404, detail="Invalid token")
|
||||
|
||||
if approval_token.meal_plan_item_id != item_id:
|
||||
raise HTTPException(status_code=400, detail="Token not valid for this meal")
|
||||
|
||||
if approval_token.status != ApprovalTokenStatus.ACTIVE:
|
||||
raise HTTPException(status_code=400, detail="Token has already been used or expired")
|
||||
|
||||
if approval_token.expires_at < datetime.now():
|
||||
approval_token.status = ApprovalTokenStatus.EXPIRED
|
||||
db.commit()
|
||||
raise HTTPException(status_code=400, detail="Token has expired")
|
||||
|
||||
existing_vote = db.query(MealPlanVote).filter(
|
||||
MealPlanVote.meal_plan_item_id == item_id,
|
||||
MealPlanVote.family_member_id == approval_token.family_member_id
|
||||
).first()
|
||||
|
||||
if existing_vote:
|
||||
raise HTTPException(status_code=400, detail="You have already voted on this meal")
|
||||
|
||||
vote = MealPlanVote(
|
||||
meal_plan_item_id=item_id,
|
||||
family_member_id=approval_token.family_member_id,
|
||||
vote=vote_data.vote
|
||||
)
|
||||
db.add(vote)
|
||||
|
||||
approval_token.status = ApprovalTokenStatus.USED
|
||||
approval_token.used_at = datetime.now()
|
||||
|
||||
item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first()
|
||||
if not vote_data.vote and vote_data.denial_reason:
|
||||
item.approval_status = MealPlanItemStatus.DENIED
|
||||
item.denial_reason = vote_data.denial_reason
|
||||
item.denial_details = vote_data.denial_details
|
||||
|
||||
db.commit()
|
||||
db.refresh(vote)
|
||||
return vote
|
||||
|
||||
|
||||
@router.get("/items/{item_id}", response_model=MealPlanItemResponse)
|
||||
def get_meal_item(item_id: UUID, db: Session = Depends(get_db)):
|
||||
item = db.query(MealPlanItem).options(joinedload(MealPlanItem.recipe)).filter(
|
||||
MealPlanItem.id == item_id
|
||||
).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Meal plan item not found")
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/swap")
|
||||
def swap_meal_item(item_id: UUID, new_recipe_id: UUID, db: Session = Depends(get_db)):
|
||||
item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Meal plan item not found")
|
||||
|
||||
new_recipe = db.query(Recipe).filter(Recipe.id == new_recipe_id).first()
|
||||
if not new_recipe:
|
||||
raise HTTPException(status_code=404, detail="New recipe not found")
|
||||
|
||||
item.recipe_id = new_recipe_id
|
||||
item.approval_status = MealPlanItemStatus.PENDING
|
||||
item.denial_reason = None
|
||||
item.denial_details = None
|
||||
|
||||
db.commit()
|
||||
return {"message": "Meal swapped", "item": item}
|
||||
Reference in New Issue
Block a user