TICKET-004: Day summary endpoint

- GET /api/log/summary?date= with totals vs historical target
- Nutrition math consolidated in services/nutrition.py (weight vs count)
- Null nutrition contributes 0; meals contribute 0 (TODO TICKET-007)
- Full suite green (104 passed)
This commit is contained in:
Craig
2026-07-26 13:27:03 +01:00
parent 87d7eca468
commit 5372e8cbca
5 changed files with 482 additions and 2 deletions
+55 -1
View File
@@ -15,7 +15,15 @@ from sqlalchemy import func, select
from sqlalchemy.orm import Session, joinedload
from models import DailyLogEntry, Food
from schemas import LogEntryCreate, LogEntryRead, LogEntryUpdate
from schemas import (
DaySummaryNutrition,
DaySummaryResponse,
LogEntryCreate,
LogEntryRead,
LogEntryUpdate,
)
from services import nutrition
from services import targets as targets_svc
def _now() -> datetime:
@@ -125,3 +133,49 @@ class FoodNotAvailableError(Exception):
def __init__(self, food_id: int):
super().__init__(f"Food {food_id} not available for logging")
self.food_id = food_id
# ── Day Summary (TICKET-004) ─────────────────────────────────────────────────
def get_day_summary(db: Session, lookup_date: date) -> DaySummaryResponse:
"""Compute nutrition totals for a date vs. the applicable target.
1. Loads all log entries for the date with food data eagerly joined.
2. For each entry, scales the food's per-unit nutrition to the logged
quantity via nutrition.entry_nutrition() — weight-type foods get
(qty/100)× scaling, count-type foods get qty× scaling (§2.1).
3. Meal entries (is_meal=True) currently contribute 0 because their
per_unit fields are null per the CHECK constraint.
TODO: TICKET-007 — replace with recursive component summation.
4. Looks up the target covering the date via the half-open interval
lookup from targets.get_target_for_date(). Returns null if none.
Returns a DaySummaryResponse with summed totals and the applicable
target (or null).
"""
entries = db.scalars(
select(DailyLogEntry)
.where(DailyLogEntry.date == lookup_date)
.options(joinedload(DailyLogEntry.food))
).all()
# Sum nutrition across all entries for the date.
# TODO: TICKET-007 — meal entries currently contribute 0 because their
# per_unit fields are null per the CHECK constraint. Real meal nutrition
# will be derived by recursively summing component foods' nutrition.
# When that lands, replace the flat entry_nutrition() call with a
# meal-aware sum function from services/nutrition.py.
totals = {field: 0.0 for field in nutrition.NUTRITION_FIELDS}
for entry in entries:
entry_nut = nutrition.entry_nutrition(entry.food, entry.quantity)
for field, value in entry_nut.items():
totals[field] += value
target = targets_svc.get_target_for_date(db, lookup_date)
return DaySummaryResponse(
date=lookup_date,
totals=DaySummaryNutrition(**totals),
target=target,
)