Remove service interfaces — use concrete classes directly

Each domain had a single-implementation interface (e.g. AdminService
interface + AdminServiceImpl). Merged implementation into the service
class and deleted the redundant interfaces per KISS principle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:04:41 +02:00
parent 03b96e8584
commit 9713412d42
21 changed files with 1171 additions and 1595 deletions

View File

@@ -1,270 +0,0 @@
package com.recipeapp.shopping;
import com.recipeapp.auth.UserAccountRepository;
import com.recipeapp.auth.entity.UserAccount;
import com.recipeapp.common.ResourceNotFoundException;
import com.recipeapp.common.ValidationException;
import com.recipeapp.household.HouseholdRepository;
import com.recipeapp.planning.WeekPlanRepository;
import com.recipeapp.planning.entity.WeekPlan;
import com.recipeapp.recipe.IngredientRepository;
import com.recipeapp.recipe.entity.Ingredient;
import com.recipeapp.recipe.entity.RecipeIngredient;
import com.recipeapp.shopping.dto.*;
import com.recipeapp.shopping.entity.ShoppingList;
import com.recipeapp.shopping.entity.ShoppingListItem;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Transactional
public class ShoppingServiceImpl implements ShoppingService {
private final ShoppingListRepository shoppingListRepository;
private final ShoppingListItemRepository shoppingListItemRepository;
private final WeekPlanRepository weekPlanRepository;
private final HouseholdRepository householdRepository;
private final IngredientRepository ingredientRepository;
private final UserAccountRepository userAccountRepository;
public ShoppingServiceImpl(ShoppingListRepository shoppingListRepository,
ShoppingListItemRepository shoppingListItemRepository,
WeekPlanRepository weekPlanRepository,
HouseholdRepository householdRepository,
IngredientRepository ingredientRepository,
UserAccountRepository userAccountRepository) {
this.shoppingListRepository = shoppingListRepository;
this.shoppingListItemRepository = shoppingListItemRepository;
this.weekPlanRepository = weekPlanRepository;
this.householdRepository = householdRepository;
this.ingredientRepository = ingredientRepository;
this.userAccountRepository = userAccountRepository;
}
@Override
public ShoppingListResponse generateFromPlan(UUID householdId, UUID weekPlanId) {
WeekPlan weekPlan = weekPlanRepository.findById(weekPlanId)
.orElseThrow(() -> new ResourceNotFoundException("Week plan not found"));
if (!weekPlan.getHousehold().getId().equals(householdId)) {
throw new ResourceNotFoundException("Week plan not found");
}
var household = weekPlan.getHousehold();
ShoppingList shoppingList = new ShoppingList(household, weekPlan);
shoppingList = shoppingListRepository.save(shoppingList);
// Aggregate ingredients across all slots/recipes
// Key: ingredientId + unit -> merged data
Map<String, MergedIngredient> merged = new LinkedHashMap<>();
for (var slot : weekPlan.getSlots()) {
var recipe = slot.getRecipe();
for (RecipeIngredient ri : recipe.getIngredients()) {
Ingredient ingredient = ri.getIngredient();
// Filter out staples
if (ingredient.isStaple()) {
continue;
}
String key = ingredient.getId().toString() + "|" + ri.getUnit();
merged.computeIfAbsent(key, k -> new MergedIngredient(ingredient, ri.getUnit()))
.addQuantity(ri.getQuantity())
.addRecipeId(recipe.getId());
}
}
// Create shopping list items
for (MergedIngredient mi : merged.values()) {
ShoppingListItem item = new ShoppingListItem(
shoppingList,
mi.ingredient,
null,
mi.totalQuantity,
mi.unit,
mi.recipeIds.stream().distinct().toArray(UUID[]::new)
);
shoppingList.getItems().add(item);
}
shoppingListRepository.save(shoppingList);
return toResponse(shoppingList);
}
@Override
@Transactional(readOnly = true)
public ShoppingListResponse getShoppingList(UUID householdId, UUID shoppingListId) {
ShoppingList list = findList(householdId, shoppingListId);
return toResponse(list);
}
@Override
public PublishResponse publish(UUID householdId, UUID shoppingListId) {
ShoppingList list = findList(householdId, shoppingListId);
if (!"draft".equals(list.getStatus())) {
throw new ValidationException("Shopping list is already published");
}
list.setStatus("published");
list.setPublishedAt(Instant.now());
shoppingListRepository.save(list);
return new PublishResponse(list.getId(), list.getStatus(), list.getPublishedAt());
}
@Override
public ShoppingListItemResponse checkItem(UUID householdId, UUID listId, UUID itemId,
CheckItemRequest request, UUID userId) {
ShoppingList list = findList(householdId, listId);
ShoppingListItem item = findItem(list, itemId);
item.setChecked(request.isChecked());
if (request.isChecked()) {
UserAccount user = userAccountRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
item.setCheckedBy(user);
} else {
item.setCheckedBy(null);
}
shoppingListItemRepository.save(item);
return toItemResponse(item);
}
@Override
public ShoppingListItemResponse addItem(UUID householdId, UUID shoppingListId, AddItemRequest request) {
ShoppingList list = findList(householdId, shoppingListId);
Ingredient ingredient = null;
if (request.ingredientId() != null) {
ingredient = ingredientRepository.findById(request.ingredientId())
.orElseThrow(() -> new ResourceNotFoundException("Ingredient not found"));
}
ShoppingListItem item = new ShoppingListItem(
list,
ingredient,
request.customName(),
request.quantity(),
request.unit(),
new UUID[0]
);
item = shoppingListItemRepository.save(item);
list.getItems().add(item);
return toItemResponse(item);
}
@Override
public void deleteItem(UUID householdId, UUID listId, UUID itemId) {
ShoppingList list = findList(householdId, listId);
if ("published".equals(list.getStatus())) {
throw new ValidationException("Cannot delete items from a published shopping list");
}
ShoppingListItem item = findItem(list, itemId);
list.getItems().remove(item);
shoppingListItemRepository.delete(item);
}
// ── Helpers ──
private ShoppingList findList(UUID householdId, UUID shoppingListId) {
ShoppingList list = shoppingListRepository.findById(shoppingListId)
.orElseThrow(() -> new ResourceNotFoundException("Shopping list not found"));
if (!list.getHousehold().getId().equals(householdId)) {
throw new ResourceNotFoundException("Shopping list not found");
}
return list;
}
private ShoppingListItem findItem(ShoppingList list, UUID itemId) {
return list.getItems().stream()
.filter(i -> i.getId().equals(itemId))
.findFirst()
.orElseThrow(() -> new ResourceNotFoundException("Shopping list item not found"));
}
private ShoppingListResponse toResponse(ShoppingList list) {
List<ShoppingListItemResponse> items = list.getItems().stream()
.map(this::toItemResponse)
.toList();
return new ShoppingListResponse(
list.getId(),
list.getWeekPlan().getId(),
list.getStatus(),
list.getPublishedAt(),
items
);
}
private ShoppingListItemResponse toItemResponse(ShoppingListItem item) {
String name;
ShoppingListItemResponse.CategoryRef categoryRef = null;
UUID ingredientId = null;
if (item.getIngredient() != null) {
ingredientId = item.getIngredient().getId();
name = item.getIngredient().getName();
if (item.getIngredient().getCategory() != null) {
categoryRef = new ShoppingListItemResponse.CategoryRef(
item.getIngredient().getCategory().getId(),
item.getIngredient().getCategory().getName()
);
}
} else {
name = item.getCustomName();
}
return new ShoppingListItemResponse(
item.getId(),
ingredientId,
name,
categoryRef,
item.getQuantity(),
item.getUnit(),
item.isChecked(),
item.getCheckedBy() != null ? item.getCheckedBy().getId() : null,
item.getSourceRecipes() != null ? Arrays.asList(item.getSourceRecipes()) : List.of()
);
}
private static class MergedIngredient {
final Ingredient ingredient;
final String unit;
BigDecimal totalQuantity = BigDecimal.ZERO;
final List<UUID> recipeIds = new ArrayList<>();
MergedIngredient(Ingredient ingredient, String unit) {
this.ingredient = ingredient;
this.unit = unit;
}
MergedIngredient addQuantity(BigDecimal qty) {
if (qty != null) {
this.totalQuantity = this.totalQuantity.add(qty);
}
return this;
}
MergedIngredient addRecipeId(UUID recipeId) {
this.recipeIds.add(recipeId);
return this;
}
}
}