From 16b70bd818e7e9583a085b4ba972a34133331f96 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Sat, 4 Apr 2026 18:35:31 +0200 Subject: [PATCH] feat(shopping): add GET /v1/shopping-lists endpoint and planner-only guard New week-based lookup endpoint with optional weekStart param (defaults to current week). Generate endpoint now enforced with @RequiresHouseholdRole. Co-Authored-By: Claude Sonnet 4.6 --- .../shopping/ShoppingListController.java | 16 ++++++++++++ .../shopping/ShoppingListControllerTest.java | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/backend/src/main/java/com/recipeapp/shopping/ShoppingListController.java b/backend/src/main/java/com/recipeapp/shopping/ShoppingListController.java index b5fd800..3589d4b 100644 --- a/backend/src/main/java/com/recipeapp/shopping/ShoppingListController.java +++ b/backend/src/main/java/com/recipeapp/shopping/ShoppingListController.java @@ -1,11 +1,14 @@ package com.recipeapp.shopping; +import com.recipeapp.common.RequiresHouseholdRole; +import com.recipeapp.common.ResourceNotFoundException; import com.recipeapp.recipe.HouseholdResolver; import com.recipeapp.shopping.dto.*; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.*; import java.security.Principal; +import java.time.LocalDate; import java.util.UUID; @RestController @@ -19,8 +22,21 @@ public class ShoppingListController { this.householdResolver = householdResolver; } + @GetMapping("/v1/shopping-lists") + public ShoppingListResponse getByWeekStart( + @RequestParam(required = false) LocalDate weekStart, + Principal principal) { + UUID householdId = householdResolver.resolve(principal.getName()); + ShoppingListResponse response = shoppingService.getByWeekStart(householdId, weekStart); + if (response == null) { + throw new ResourceNotFoundException("No shopping list for this week"); + } + return response; + } + @PostMapping("/v1/week-plans/{id}/shopping-list") @ResponseStatus(HttpStatus.CREATED) + @RequiresHouseholdRole("planner") public ShoppingListResponse generateFromPlan(@PathVariable UUID id, Principal principal) { UUID householdId = householdResolver.resolve(principal.getName()); return shoppingService.generateFromPlan(householdId, id); diff --git a/backend/src/test/java/com/recipeapp/shopping/ShoppingListControllerTest.java b/backend/src/test/java/com/recipeapp/shopping/ShoppingListControllerTest.java index cdfdbe9..a09e619 100644 --- a/backend/src/test/java/com/recipeapp/shopping/ShoppingListControllerTest.java +++ b/backend/src/test/java/com/recipeapp/shopping/ShoppingListControllerTest.java @@ -49,6 +49,32 @@ class ShoppingListControllerTest { .build(); } + @Test + void getByWeekStartShouldReturn200() throws Exception { + var response = new ShoppingListResponse(LIST_ID, PLAN_ID, Instant.now(), 3, List.of()); + + when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); + when(shoppingService.getByWeekStart(eq(HOUSEHOLD_ID), any())).thenReturn(response); + + mockMvc.perform(get("/v1/shopping-lists") + .param("weekStart", "2026-04-06") + .principal(() -> "sarah@example.com")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(LIST_ID.toString())) + .andExpect(jsonPath("$.filteredStaplesCount").value(3)); + } + + @Test + void getByWeekStartShouldReturn404WhenNoListExists() throws Exception { + when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID); + when(shoppingService.getByWeekStart(eq(HOUSEHOLD_ID), any())).thenReturn(null); + + mockMvc.perform(get("/v1/shopping-lists") + .param("weekStart", "2026-04-06") + .principal(() -> "sarah@example.com")) + .andExpect(status().isNotFound()); + } + @Test void generateFromPlanShouldReturn201() throws Exception { var recipeId = UUID.randomUUID();