feat(phase): Complete Phase 2: Logic & Calculation Updates

This commit is contained in:
2026-02-24 08:19:03 -08:00
parent f0430c810b
commit cc6b4ca145
6 changed files with 166 additions and 46 deletions

View File

@@ -746,23 +746,25 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
# Store grams directly
quantity = grams
# Create a new Meal for this single food entry
# This allows it to be treated like any other meal in the tracker view
new_meal = Meal(name=food_item.name, meal_type="single_food", meal_time=meal_time)
db.add(new_meal)
db.flush() # Flush to get the new meal ID
# Link the food to the new meal
meal_food = MealFood(meal_id=new_meal.id, food_id=food_id, quantity=grams)
db.add(meal_food)
# Create tracked meal entry
# Create tracked meal entry without a parent Meal template
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=new_meal.id,
meal_time=meal_time
meal_id=None,
meal_time=meal_time,
name=food_item.name
)
db.add(tracked_meal)
db.flush() # Flush to get the tracked_meal ID
# Link the food directly to the tracked meal via TrackedMealFood
new_entry = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food_id,
quantity=grams,
is_override=False,
is_deleted=False
)
db.add(new_entry)
# Mark day as modified
tracked_day.is_modified = True

View File

@@ -428,9 +428,11 @@ def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
}
# 1. Get base foods from the meal
# 1. Get base foods from the meal (if it exists)
# access via relationship, assume eager loading or lazy loading
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
base_foods = {}
if tracked_meal.meal:
base_foods = {mf.food_id: mf for mf in tracked_meal.meal.meal_foods}
# 2. Get tracked foods (overrides, deletions, additions)
tracked_foods = tracked_meal.tracked_foods

View File

@@ -98,20 +98,9 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
query_session = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)()
try:
# Find the created Meal
created_meal = query_session.query(Meal).order_by(Meal.id.desc()).first()
assert created_meal is not None
assert created_meal.name == "Test Food"
assert created_meal.meal_type == "single_food"
# Find the MealFood
meal_food = query_session.query(MealFood).filter(MealFood.meal_id == created_meal.id).first()
assert meal_food is not None
assert meal_food.food_id == food.id
# This assertion fails because the backend used data.get("grams", 1.0), so quantity=1.0 instead of 50.0
# After the fix changing to data.get("quantity", 1.0), it will pass
assert meal_food.quantity == 50.0, f"Expected quantity 50.0, but got {meal_food.quantity}"
# Verify NO new Meal was created
meals = query_session.query(Meal).all()
assert len(meals) == 0
# Also verify TrackedDay and TrackedMeal were created
tracked_day = query_session.query(TrackedDay).filter(
@@ -123,8 +112,16 @@ def test_add_food_quantity_saved_correctly(test_client: TestClient, test_engine)
tracked_meal = query_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
assert tracked_meal is not None
assert tracked_meal.meal_id == created_meal.id
assert tracked_meal.meal_id is None
assert tracked_meal.name == "Test Food"
assert tracked_meal.meal_time == "Snack 1"
# Find the TrackedMealFood
from app.database import TrackedMealFood
tmf = query_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
assert tmf is not None
assert tmf.food_id == food.id
assert tmf.quantity == 50.0
finally:
query_session.close()

View File

@@ -140,10 +140,13 @@ def test_tracker_add_food_grams_input(client, session, sample_food_100g):
assert response.json()["status"] == "success"
# Verify the tracked meal food quantity
tracked_meal = session.query(Meal).filter(Meal.name == sample_food_100g.name).first()
tracked_meal = session.query(TrackedMeal).filter(TrackedMeal.name == sample_food_100g.name).first()
assert tracked_meal is not None
meal_food = session.query(MealFood).filter(MealFood.meal_id == tracked_meal.id).first()
assert meal_food.quantity == grams
assert tracked_meal.meal_id is None
tmf = session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
assert tmf is not None
assert tmf.quantity == grams
def test_update_tracked_meal_foods_grams_input(client, session, sample_food_100g, sample_food_50g):
"""Test updating tracked meal foods with grams input"""

View File

@@ -0,0 +1,113 @@
import pytest
from app.database import Food, TrackedMeal, TrackedMealFood, calculate_tracked_meal_nutrition
from sqlalchemy.orm import Session
def test_calculate_tracked_meal_nutrition_no_meal_template(db_session: Session):
"""Test nutrition calculation for a tracked meal with no parent meal template (meal_id=None)"""
# Create a food
food = Food(
name="Test Food",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
carbs=20.0,
fat=5.0,
fiber=5.0,
sugar=10.0,
sodium=100.0,
calcium=50.0
)
db_session.add(food)
db_session.commit()
db_session.refresh(food)
# Create a tracked meal without a template
tracked_meal = TrackedMeal(
meal_id=None,
meal_time="Snack",
name="Single Food Log"
)
db_session.add(tracked_meal)
db_session.commit()
db_session.refresh(tracked_meal)
# Add a tracked food entry to it
tracked_food = TrackedMealFood(
tracked_meal_id=tracked_meal.id,
food_id=food.id,
quantity=200.0, # 2 servings
is_override=False,
is_deleted=False
)
db_session.add(tracked_food)
db_session.commit()
db_session.refresh(tracked_food)
# Calculate nutrition
nutrition = calculate_tracked_meal_nutrition(tracked_meal, db_session)
# Assertions
assert nutrition['calories'] == 200.0
assert nutrition['protein'] == 20.0
assert nutrition['carbs'] == 40.0
assert nutrition['fat'] == 10.0
assert nutrition['fiber'] == 10.0
assert nutrition['sugar'] == 20.0
assert nutrition['sodium'] == 200.0
assert nutrition['calcium'] == 100.0
assert nutrition['net_carbs'] == 30.0
assert nutrition['protein_pct'] == 40.0 # (20 * 4) / 200 = 80 / 200 = 40%
assert nutrition['carbs_pct'] == 80.0 # (40 * 4) / 200 = 160 / 200 = 80%
assert nutrition['fat_pct'] == 45.0 # (10 * 9) / 200 = 90 / 200 = 45%
def test_tracker_add_food_api_no_new_meal(client, db_session: Session):
"""Test /tracker/add_food endpoint to ensure it doesn't create redundant Meal templates"""
# Create a food
food = Food(
name="API Test Food",
serving_size=100.0,
serving_unit="g",
calories=100.0,
protein=10.0,
carbs=20.0,
fat=5.0
)
db_session.add(food)
db_session.commit()
db_session.refresh(food)
from app.database import Meal
initial_meal_count = db_session.query(Meal).count()
# Call the API
response = client.post("/tracker/add_food", json={
"person": "Sarah",
"date": "2025-02-24",
"food_id": food.id,
"quantity": 150.0,
"meal_time": "Snack"
})
assert response.status_code == 200
assert response.json()["status"] == "success"
# Verify NO new Meal was created
assert db_session.query(Meal).count() == initial_meal_count
# Verify TrackedMeal exists with meal_id=None and correct name
from app.database import TrackedMeal, TrackedDay
tracked_day = db_session.query(TrackedDay).filter(TrackedDay.date == "2025-02-24").first()
assert tracked_day is not None
tracked_meal = db_session.query(TrackedMeal).filter(TrackedMeal.tracked_day_id == tracked_day.id).first()
assert tracked_meal is not None
assert tracked_meal.meal_id is None
assert tracked_meal.name == "API Test Food"
# Verify TrackedMealFood exists
from app.database import TrackedMealFood
tmf = db_session.query(TrackedMealFood).filter(TrackedMealFood.tracked_meal_id == tracked_meal.id).first()
assert tmf is not None
assert tmf.food_id == food.id
assert tmf.quantity == 150.0

View File

@@ -384,12 +384,13 @@ class TestTrackerAddFood:
assert len(tracked_meals) == 1
tracked_meal = tracked_meals[0]
assert tracked_meal.meal.name == sample_food.name # The meal name should be the food name
assert tracked_meal.name == sample_food.name # The meal name should be the food name
assert tracked_meal.meal_id is None
# Verify the food is in the tracked meal's foods
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
assert len(tracked_meal.tracked_foods) == 1
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
assert tracked_meal.tracked_foods[0].quantity == 100.0
def test_add_food_to_tracker_with_meal_time(self, client, sample_food, db_session):
@@ -418,11 +419,12 @@ class TestTrackerAddFood:
assert len(tracked_meals) == 1
tracked_meal = tracked_meals[0]
assert tracked_meal.meal.name == sample_food.name
assert tracked_meal.name == sample_food.name
assert tracked_meal.meal_id is None
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
assert tracked_meal.meal.meal_foods[0].quantity == 150.0
assert len(tracked_meal.tracked_foods) == 1
assert tracked_meal.tracked_foods[0].food_id == sample_food.id
assert tracked_meal.tracked_foods[0].quantity == 150.0
def test_add_food_quantity_is_correctly_converted_to_servings(self, client, db_session):
"""
@@ -464,12 +466,13 @@ class TestTrackerAddFood:
assert len(tracked_meals) == 1
tracked_meal = tracked_meals[0]
assert tracked_meal.meal.name == food.name
assert tracked_meal.name == food.name
assert tracked_meal.meal_id is None
# Verify the food is in the tracked meal's foods and quantity is in servings
assert len(tracked_meal.meal.meal_foods) == 1
assert tracked_meal.meal.meal_foods[0].food_id == food.id
assert tracked_meal.meal.meal_foods[0].quantity == grams_to_add
# Verify the food is in the tracked meal's foods
assert len(tracked_meal.tracked_foods) == 1
assert tracked_meal.tracked_foods[0].food_id == food.id
assert tracked_meal.tracked_foods[0].quantity == grams_to_add
# Verify nutrition calculation
day_nutrition = calculate_day_nutrition_tracked([tracked_meal], db_session)