feat: slim meal_diary — derive name and nutrition from dish/recipe
Remove denormalized columns (name, calories, protein_g, fat_g, carbs_g) from meal_diary. Name is now resolved via JOIN with dishes/dish_translations; macros are computed as recipe.*_per_serving * portions at query time. - Add dish.Repository.FindOrCreateRecipe: finds or creates a minimal recipe stub seeded with AI-estimated macros - recognition/handler: resolve recipe_id synchronously per candidate; simplify enrichDishInBackground to translations-only - diary/handler: accept dish_id OR name; always resolve recipe_id via FindOrCreateRecipe before INSERT - diary/entity: DishID is now non-nullable string; CreateRequest drops macros - diary/repository: ListByDate and Create use JOIN to return computed macros - ai/types: add RecipeID field to DishCandidate - Update tests and wire_gen accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,8 +39,17 @@ func authorizedRequest(method, target string, body []byte) *http.Request {
|
||||
return request
|
||||
}
|
||||
|
||||
func newHandler(diaryRepo diary.DiaryRepository, dishRepo diary.DishRepository, recipeRepo diary.RecipeRepository) *diary.Handler {
|
||||
return diary.NewHandler(diaryRepo, dishRepo, recipeRepo)
|
||||
}
|
||||
|
||||
func defaultMocks() (*diarymocks.MockDiaryRepository, *diarymocks.MockDishRepository, *diarymocks.MockRecipeRepository) {
|
||||
return &diarymocks.MockDiaryRepository{}, &diarymocks.MockDishRepository{}, &diarymocks.MockRecipeRepository{}
|
||||
}
|
||||
|
||||
func TestGetByDate_MissingQueryParam(t *testing.T) {
|
||||
handler := diary.NewHandler(&diarymocks.MockDiaryRepository{})
|
||||
diaryRepo, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(diaryRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -59,7 +68,8 @@ func TestGetByDate_Success(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
handler := diary.NewHandler(mockRepo)
|
||||
_, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(mockRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -79,7 +89,8 @@ func TestGetByDate_Success(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreate_MissingDate(t *testing.T) {
|
||||
handler := diary.NewHandler(&diarymocks.MockDiaryRepository{})
|
||||
diaryRepo, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(diaryRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"name": "Oatmeal", "meal_type": "breakfast"})
|
||||
@@ -91,8 +102,9 @@ func TestCreate_MissingDate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_MissingName(t *testing.T) {
|
||||
handler := diary.NewHandler(&diarymocks.MockDiaryRepository{})
|
||||
func TestCreate_MissingNameAndDishID(t *testing.T) {
|
||||
diaryRepo, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(diaryRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"date": "2026-03-15", "meal_type": "breakfast"})
|
||||
@@ -105,7 +117,8 @@ func TestCreate_MissingName(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreate_MissingMealType(t *testing.T) {
|
||||
handler := diary.NewHandler(&diarymocks.MockDiaryRepository{})
|
||||
diaryRepo, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(diaryRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
body, _ := json.Marshal(map[string]string{"date": "2026-03-15", "name": "Oatmeal"})
|
||||
@@ -118,20 +131,31 @@ func TestCreate_MissingMealType(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreate_Success(t *testing.T) {
|
||||
mockRepo := &diarymocks.MockDiaryRepository{
|
||||
mockDiaryRepo := &diarymocks.MockDiaryRepository{
|
||||
CreateFn: func(ctx context.Context, userID string, req diary.CreateRequest) (*diary.Entry, error) {
|
||||
return &diary.Entry{
|
||||
ID: "entry-1",
|
||||
Date: req.Date,
|
||||
MealType: req.MealType,
|
||||
Name: req.Name,
|
||||
Name: "Oatmeal",
|
||||
Portions: 1,
|
||||
Source: "manual",
|
||||
DishID: "dish-1",
|
||||
CreatedAt: time.Now(),
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
handler := diary.NewHandler(mockRepo)
|
||||
mockDishRepo := &diarymocks.MockDishRepository{
|
||||
FindOrCreateFn: func(ctx context.Context, name string) (string, bool, error) {
|
||||
return "dish-1", false, nil
|
||||
},
|
||||
}
|
||||
mockRecipeRepo := &diarymocks.MockRecipeRepository{
|
||||
FindOrCreateRecipeFn: func(ctx context.Context, dishID string, calories, proteinG, fatG, carbsG float64) (string, bool, error) {
|
||||
return "recipe-1", false, nil
|
||||
},
|
||||
}
|
||||
handler := newHandler(mockDiaryRepo, mockDishRepo, mockRecipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
body, _ := json.Marshal(diary.CreateRequest{
|
||||
@@ -154,7 +178,8 @@ func TestDelete_NotFound(t *testing.T) {
|
||||
return diary.ErrNotFound
|
||||
},
|
||||
}
|
||||
handler := diary.NewHandler(mockRepo)
|
||||
_, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(mockRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -171,7 +196,8 @@ func TestDelete_Success(t *testing.T) {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
handler := diary.NewHandler(mockRepo)
|
||||
_, dishRepo, recipeRepo := defaultMocks()
|
||||
handler := newHandler(mockRepo, dishRepo, recipeRepo)
|
||||
router := buildRouter(handler, "user-1")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user