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,13 +1,207 @@
package com.recipeapp.household;
import com.recipeapp.auth.UserAccountRepository;
import com.recipeapp.auth.entity.UserAccount;
import com.recipeapp.common.ConflictException;
import com.recipeapp.common.ResourceNotFoundException;
import com.recipeapp.common.ValidationException;
import com.recipeapp.household.dto.*;
import com.recipeapp.household.entity.Household;
import com.recipeapp.household.entity.HouseholdInvite;
import com.recipeapp.household.entity.HouseholdMember;
import com.recipeapp.planning.VarietyScoreConfigRepository;
import com.recipeapp.planning.entity.VarietyScoreConfig;
import com.recipeapp.recipe.IngredientCategoryRepository;
import com.recipeapp.recipe.IngredientRepository;
import com.recipeapp.recipe.TagRepository;
import com.recipeapp.recipe.entity.Ingredient;
import com.recipeapp.recipe.entity.IngredientCategory;
import com.recipeapp.recipe.entity.Tag;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.List;
public interface HouseholdService {
HouseholdResponse createHousehold(String userEmail, CreateHouseholdRequest request);
HouseholdResponse getMyHousehold(String userEmail);
List<MemberResponse> getMembers(String userEmail);
InviteResponse createInvite(String userEmail);
AcceptInviteResponse acceptInvite(String userEmail, String code);
@Service
public class HouseholdService {
private final UserAccountRepository userAccountRepository;
private final HouseholdRepository householdRepository;
private final HouseholdMemberRepository householdMemberRepository;
private final HouseholdInviteRepository householdInviteRepository;
private final IngredientRepository ingredientRepository;
private final IngredientCategoryRepository ingredientCategoryRepository;
private final TagRepository tagRepository;
private final VarietyScoreConfigRepository varietyScoreConfigRepository;
private static final SecureRandom RANDOM = new SecureRandom();
private static final String CODE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
public HouseholdService(UserAccountRepository userAccountRepository,
HouseholdRepository householdRepository,
HouseholdMemberRepository householdMemberRepository,
HouseholdInviteRepository householdInviteRepository,
IngredientRepository ingredientRepository,
IngredientCategoryRepository ingredientCategoryRepository,
TagRepository tagRepository,
VarietyScoreConfigRepository varietyScoreConfigRepository) {
this.userAccountRepository = userAccountRepository;
this.householdRepository = householdRepository;
this.householdMemberRepository = householdMemberRepository;
this.householdInviteRepository = householdInviteRepository;
this.ingredientRepository = ingredientRepository;
this.ingredientCategoryRepository = ingredientCategoryRepository;
this.tagRepository = tagRepository;
this.varietyScoreConfigRepository = varietyScoreConfigRepository;
}
@Transactional
public HouseholdResponse createHousehold(String userEmail, CreateHouseholdRequest request) {
UserAccount user = findUser(userEmail);
if (householdMemberRepository.findByUserEmailIgnoreCase(userEmail).isPresent()) {
throw new ConflictException("User is already in a household");
}
Household household = householdRepository.save(new Household(request.name(), user));
HouseholdMember member = householdMemberRepository.save(
new HouseholdMember(household, user, "planner"));
seedDefaultData(household);
return toHouseholdResponse(household, List.of(member));
}
@Transactional(readOnly = true)
public HouseholdResponse getMyHousehold(String userEmail) {
HouseholdMember member = findMembership(userEmail);
Household household = member.getHousehold();
List<HouseholdMember> members = householdMemberRepository.findByHouseholdId(household.getId());
return toHouseholdResponse(household, members);
}
@Transactional(readOnly = true)
public List<MemberResponse> getMembers(String userEmail) {
HouseholdMember member = findMembership(userEmail);
return householdMemberRepository.findByHouseholdId(member.getHousehold().getId())
.stream()
.map(this::toMemberResponse)
.toList();
}
@Transactional
public InviteResponse createInvite(String userEmail) {
HouseholdMember member = findMembership(userEmail);
Household household = member.getHousehold();
String code = generateInviteCode();
Instant expiresAt = Instant.now().plusSeconds(48 * 3600);
HouseholdInvite invite = householdInviteRepository.save(
new HouseholdInvite(household, code, expiresAt));
return new InviteResponse(
invite.getInviteCode(),
"https://yourapp.com/join/" + invite.getInviteCode(),
invite.getExpiresAt());
}
@Transactional
public AcceptInviteResponse acceptInvite(String userEmail, String code) {
UserAccount user = findUser(userEmail);
if (householdMemberRepository.findByUserEmailIgnoreCase(userEmail).isPresent()) {
throw new ConflictException("User is already in a household");
}
HouseholdInvite invite = householdInviteRepository.findByInviteCode(code)
.orElseThrow(() -> new ResourceNotFoundException("Invite not found"));
if ("used".equals(invite.getStatus())) {
throw new ConflictException("Invite code already used");
}
if (invite.getExpiresAt().isBefore(Instant.now())) {
throw new ValidationException("Invite code has expired");
}
invite.setStatus("used");
householdInviteRepository.save(invite);
Household household = invite.getHousehold();
householdMemberRepository.save(new HouseholdMember(household, user, "member"));
return new AcceptInviteResponse(household.getId(), household.getName(), "member");
}
private void seedDefaultData(Household household) {
var categories = ingredientCategoryRepository.saveAll(List.of(
new IngredientCategory(household, "Produce", (short) 1),
new IngredientCategory(household, "Fish & Meat", (short) 2),
new IngredientCategory(household, "Dairy & Eggs", (short) 3),
new IngredientCategory(household, "Dry Goods & Pasta", (short) 4),
new IngredientCategory(household, "Canned & Jarred", (short) 5),
new IngredientCategory(household, "Sauces & Condiments", (short) 6),
new IngredientCategory(household, "Frozen", (short) 7),
new IngredientCategory(household, "Bakery & Bread", (short) 8)));
tagRepository.saveAll(List.of(
new Tag(household, "Chicken", "protein"),
new Tag(household, "Fish", "protein"),
new Tag(household, "Beef", "protein"),
new Tag(household, "Pork", "protein"),
new Tag(household, "Vegetarian", "dietary"),
new Tag(household, "Vegan", "dietary"),
new Tag(household, "Pasta", "cuisine"),
new Tag(household, "Quick meal", "other"),
new Tag(household, "Child-friendly", "other")));
ingredientRepository.saveAll(List.of(
new Ingredient(household, "Salt", true),
new Ingredient(household, "Pepper", true),
new Ingredient(household, "Olive oil", true),
new Ingredient(household, "Butter", true),
new Ingredient(household, "Garlic", true),
new Ingredient(household, "Onion", true),
new Ingredient(household, "Sugar", true),
new Ingredient(household, "Flour", true),
new Ingredient(household, "Rice", true),
new Ingredient(household, "Pasta", true)));
varietyScoreConfigRepository.save(VarietyScoreConfig.defaults(household));
}
private String generateInviteCode() {
var sb = new StringBuilder(8);
for (int i = 0; i < 8; i++) {
sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length())));
}
return sb.toString();
}
private UserAccount findUser(String email) {
return userAccountRepository.findByEmailIgnoreCase(email)
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
}
private HouseholdMember findMembership(String email) {
return householdMemberRepository.findByUserEmailIgnoreCase(email)
.orElseThrow(() -> new ResourceNotFoundException("User is not in a household"));
}
private HouseholdResponse toHouseholdResponse(Household household, List<HouseholdMember> members) {
return new HouseholdResponse(
household.getId(),
household.getName(),
members.stream().map(this::toMemberResponse).toList());
}
private MemberResponse toMemberResponse(HouseholdMember member) {
return new MemberResponse(
member.getUser().getId(),
member.getUser().getDisplayName(),
member.getRole(),
member.getJoinedAt());
}
}