Rewrite variety score and suggestions with configurable scoring
- Add VarietyScoreConfig entity, repository, and V020 migration for per-household scoring weights and configurable tag types - Rewrite getVarietyScore: tag-type repeats on consecutive days, non-staple ingredient overlaps, cooking log history, plan duplicates - Rewrite getSuggestions: simulate variety score for each candidate, add tag filter (AND, case-insensitive) and configurable topN param - Update SuggestionResponse to return simulatedScore instead of fitReasons/warnings, update VarietyScoreResponse to new shape - Seed default VarietyScoreConfig on household creation - Extend test suite across all domains (+270 tests, all passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ class PlanningServiceTest {
|
||||
@Mock private RecipeRepository recipeRepository;
|
||||
@Mock private HouseholdRepository householdRepository;
|
||||
@Mock private UserAccountRepository userAccountRepository;
|
||||
@Mock private VarietyScoreConfigRepository varietyScoreConfigRepository;
|
||||
|
||||
@InjectMocks private PlanningServiceImpl planningService;
|
||||
|
||||
@@ -277,4 +278,169 @@ class PlanningServiceTest {
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.getFirst().recipeName()).isEqualTo("Spaghetti");
|
||||
}
|
||||
|
||||
// ── Plan not found / household mismatch ──
|
||||
|
||||
@Test
|
||||
void getWeekPlanShouldThrowWhenPlanBelongsToDifferentHousehold() {
|
||||
var otherHousehold = new Household("Other family", null);
|
||||
setId(otherHousehold, Household.class, UUID.randomUUID());
|
||||
var plan = new WeekPlan(otherHousehold, WEEK_START);
|
||||
setId(plan, WeekPlan.class, UUID.randomUUID());
|
||||
|
||||
// getWeekPlan uses findByHouseholdIdAndWeekStart so it won't find it for wrong household
|
||||
when(weekPlanRepository.findByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.getWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSlotShouldThrowWhenPlanNotFound() {
|
||||
var planId = UUID.randomUUID();
|
||||
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, planId,
|
||||
new CreateSlotRequest(WEEK_START, UUID.randomUUID())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSlotShouldThrowWhenPlanHouseholdMismatch() {
|
||||
var otherHousehold = new Household("Other family", null);
|
||||
setId(otherHousehold, Household.class, UUID.randomUUID());
|
||||
var plan = new WeekPlan(otherHousehold, WEEK_START);
|
||||
setId(plan, WeekPlan.class, UUID.randomUUID());
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
|
||||
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
||||
new CreateSlotRequest(WEEK_START, UUID.randomUUID())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSlotShouldThrowWhenRecipeNotFound() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var recipeId = UUID.randomUUID();
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(recipeRepository.findByIdAndHouseholdIdAndDeletedAtIsNull(recipeId, HOUSEHOLD_ID))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.addSlot(HOUSEHOLD_ID, plan.getId(),
|
||||
new CreateSlotRequest(WEEK_START, recipeId)))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateSlotShouldThrowWhenSlotNotFound() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var slotId = UUID.randomUUID();
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(weekPlanSlotRepository.findById(slotId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.updateSlot(HOUSEHOLD_ID, plan.getId(), slotId,
|
||||
new UpdateSlotRequest(UUID.randomUUID())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSlotShouldThrowWhenSlotNotFound() {
|
||||
var household = testHousehold();
|
||||
var plan = testWeekPlan(household);
|
||||
var slotId = UUID.randomUUID();
|
||||
|
||||
when(weekPlanRepository.findById(plan.getId())).thenReturn(Optional.of(plan));
|
||||
when(weekPlanSlotRepository.findById(slotId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.deleteSlot(HOUSEHOLD_ID, plan.getId(), slotId))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmPlanShouldThrowWhenPlanNotFound() {
|
||||
var planId = UUID.randomUUID();
|
||||
when(weekPlanRepository.findById(planId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.confirmPlan(HOUSEHOLD_ID, planId))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
// ── Cooking log edge cases ──
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldDefaultToTodayWhenCookedOnNull() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
var user = new UserAccount("sarah@example.com", "Sarah", "hashed");
|
||||
setId(user, UserAccount.class, UUID.randomUUID());
|
||||
|
||||
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(userAccountRepository.findById(user.getId())).thenReturn(Optional.of(user));
|
||||
when(cookingLogRepository.save(any(CookingLog.class))).thenAnswer(i -> {
|
||||
CookingLog cl = i.getArgument(0);
|
||||
setId(cl, CookingLog.class, UUID.randomUUID());
|
||||
return cl;
|
||||
});
|
||||
|
||||
CookingLogResponse result = planningService.createCookingLog(HOUSEHOLD_ID, user.getId(),
|
||||
new CreateCookingLogRequest(recipe.getId(), null));
|
||||
|
||||
assertThat(result.cookedOn()).isEqualTo(LocalDate.now());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldThrowWhenRecipeNotFound() {
|
||||
var recipeId = UUID.randomUUID();
|
||||
when(recipeRepository.findById(recipeId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, UUID.randomUUID(),
|
||||
new CreateCookingLogRequest(recipeId, LocalDate.now())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldThrowWhenHouseholdNotFound() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
|
||||
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, UUID.randomUUID(),
|
||||
new CreateCookingLogRequest(recipe.getId(), LocalDate.now())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCookingLogShouldThrowWhenUserNotFound() {
|
||||
var household = testHousehold();
|
||||
var recipe = testRecipe(household, "Spaghetti");
|
||||
var userId = UUID.randomUUID();
|
||||
|
||||
when(recipeRepository.findById(recipe.getId())).thenReturn(Optional.of(recipe));
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.of(household));
|
||||
when(userAccountRepository.findById(userId)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.createCookingLog(HOUSEHOLD_ID, userId,
|
||||
new CreateCookingLogRequest(recipe.getId(), LocalDate.now())))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
|
||||
// ── Create week plan edge cases ──
|
||||
|
||||
@Test
|
||||
void createWeekPlanShouldThrowWhenHouseholdNotFound() {
|
||||
when(weekPlanRepository.existsByHouseholdIdAndWeekStart(HOUSEHOLD_ID, WEEK_START)).thenReturn(false);
|
||||
when(householdRepository.findById(HOUSEHOLD_ID)).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> planningService.createWeekPlan(HOUSEHOLD_ID, WEEK_START))
|
||||
.isInstanceOf(ResourceNotFoundException.class);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user