Implement Recipe, Planning, Shopping, Pantry, and Admin domains

Outside-in TDD for all 5 remaining domains (128 tests total):
- Recipe: CRUD, ingredients autocomplete/patch, tags, categories (27 tests)
- Planning: week plans, slots, confirm, suggestions, variety score, cooking logs (24 tests)
- Shopping: generate from plan, publish, check/add/remove items (15 tests)
- Pantry: CRUD with expiry sorting (11 tests)
- Admin: user management, password reset, audit logging (13 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 21:56:51 +02:00
parent 4f457303d8
commit 9ec703abcd
88 changed files with 5267 additions and 0 deletions

View File

@@ -0,0 +1,79 @@
package com.recipeapp.recipe;
import com.recipeapp.common.ApiResponse;
import com.recipeapp.recipe.dto.*;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/v1/recipes")
public class RecipeController {
private final RecipeService recipeService;
private final HouseholdResolver householdResolver;
public RecipeController(RecipeService recipeService, HouseholdResolver householdResolver) {
this.recipeService = recipeService;
this.householdResolver = householdResolver;
}
@GetMapping
public ResponseEntity<ApiResponse<List<RecipeSummaryResponse>>> listRecipes(
Principal principal,
@RequestParam(required = false) String search,
@RequestParam(required = false) String effort,
@RequestParam(required = false) Boolean isChildFriendly,
@RequestParam(name = "cookTimeMin.lte", required = false) Integer cookTimeMaxMin,
@RequestParam(required = false) String sort,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "0") int offset) {
UUID householdId = householdResolver.resolve(principal.getName());
List<RecipeSummaryResponse> recipes = recipeService.listRecipes(
householdId, search, effort, isChildFriendly, cookTimeMaxMin, sort, limit, offset);
long total = recipeService.countRecipes(
householdId, search, effort, isChildFriendly, cookTimeMaxMin);
var pagination = new ApiResponse.Pagination(total, limit, offset, offset + limit < total);
var meta = new ApiResponse.Meta(pagination);
return ResponseEntity.ok(ApiResponse.success(recipes, meta));
}
@GetMapping("/{id}")
public RecipeDetailResponse getRecipe(Principal principal, @PathVariable UUID id) {
UUID householdId = householdResolver.resolve(principal.getName());
return recipeService.getRecipe(householdId, id);
}
@PostMapping
public ResponseEntity<RecipeDetailResponse> createRecipe(
Principal principal,
@Valid @RequestBody RecipeCreateRequest request) {
UUID householdId = householdResolver.resolve(principal.getName());
RecipeDetailResponse detail = recipeService.createRecipe(householdId, request);
return ResponseEntity.created(URI.create("/v1/recipes/" + detail.id())).body(detail);
}
@PutMapping("/{id}")
public RecipeDetailResponse updateRecipe(
Principal principal,
@PathVariable UUID id,
@Valid @RequestBody RecipeCreateRequest request) {
UUID householdId = householdResolver.resolve(principal.getName());
return recipeService.updateRecipe(householdId, id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteRecipe(Principal principal, @PathVariable UUID id) {
UUID householdId = householdResolver.resolve(principal.getName());
recipeService.deleteRecipe(householdId, id);
}
}