Remove shopping list draft/publish workflow — lists are always live

Shopping lists no longer go through a draft → published lifecycle.
They are immediately usable upon generation from a week plan.

Removed: status/published_at columns (V021 migration), publish endpoint,
PublishResponse DTO, delete-item guard, and 4 related tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:03:54 +02:00
parent 8221a1fd41
commit 03b96e8584
8 changed files with 240 additions and 113 deletions

View File

@@ -3,7 +3,6 @@ package com.recipeapp.shopping;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.recipeapp.common.GlobalExceptionHandler;
import com.recipeapp.common.ValidationException;
import com.recipeapp.recipe.HouseholdResolver;
import com.recipeapp.shopping.dto.*;
import org.junit.jupiter.api.BeforeEach;
@@ -17,7 +16,6 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@@ -56,7 +54,7 @@ class ShoppingListControllerTest {
ITEM_ID, UUID.randomUUID(), "Tomatoes",
new ShoppingListItemResponse.CategoryRef(UUID.randomUUID(), "Produce"),
new BigDecimal("4.00"), "pcs", false, null, List.of(UUID.randomUUID()));
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, "draft", null, List.of(item));
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, List.of(item));
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(shoppingService.generateFromPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response);
@@ -65,13 +63,12 @@ class ShoppingListControllerTest {
.principal(() -> "sarah@example.com"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(LIST_ID.toString()))
.andExpect(jsonPath("$.status").value("draft"))
.andExpect(jsonPath("$.items[0].name").value("Tomatoes"));
}
@Test
void getShoppingListShouldReturn200() throws Exception {
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, "draft", null, List.of());
var response = new ShoppingListResponse(LIST_ID, PLAN_ID, List.of());
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(shoppingService.getShoppingList(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
@@ -83,20 +80,6 @@ class ShoppingListControllerTest {
.andExpect(jsonPath("$.weekPlanId").value(PLAN_ID.toString()));
}
@Test
void publishShouldReturn200() throws Exception {
var now = Instant.now();
var response = new PublishResponse(LIST_ID, "published", now);
when(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
when(shoppingService.publish(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
mockMvc.perform(post("/v1/shopping-lists/{id}/publish", LIST_ID)
.principal(() -> "sarah@example.com"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("published"));
}
@Test
void checkItemShouldReturn200() throws Exception {
var response = new ShoppingListItemResponse(

View File

@@ -3,7 +3,6 @@ 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.household.entity.Household;
import com.recipeapp.planning.WeekPlanRepository;
@@ -41,7 +40,7 @@ class ShoppingServiceTest {
@Mock private IngredientRepository ingredientRepository;
@Mock private UserAccountRepository userAccountRepository;
@InjectMocks private ShoppingServiceImpl shoppingService;
@InjectMocks private ShoppingService shoppingService;
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6);
@@ -128,7 +127,6 @@ class ShoppingServiceTest {
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
assertThat(result.status()).isEqualTo("draft");
assertThat(result.items()).hasSize(2); // tomatoes + cheese (salt filtered)
var tomatoItem = result.items().stream()
@@ -170,36 +168,6 @@ class ShoppingServiceTest {
assertThat(result.items().getFirst().name()).isEqualTo("Tomatoes");
}
// ── Publish ──
@Test
void publishShouldSetStatusAndTimestamp() {
var household = testHousehold();
var plan = testWeekPlan(household);
var list = testShoppingList(household, plan);
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
when(shoppingListRepository.save(any(ShoppingList.class))).thenAnswer(i -> i.getArgument(0));
PublishResponse result = shoppingService.publish(HOUSEHOLD_ID, list.getId());
assertThat(result.status()).isEqualTo("published");
assertThat(result.publishedAt()).isNotNull();
}
@Test
void publishShouldThrowWhenAlreadyPublished() {
var household = testHousehold();
var plan = testWeekPlan(household);
var list = testShoppingList(household, plan);
list.setStatus("published");
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
assertThatThrownBy(() -> shoppingService.publish(HOUSEHOLD_ID, list.getId()))
.isInstanceOf(ValidationException.class);
}
// ── Check Item ──
@Test
@@ -267,22 +235,6 @@ class ShoppingServiceTest {
assertThat(list.getItems()).isEmpty();
}
@Test
void deleteItemShouldThrowWhenListIsPublished() {
var household = testHousehold();
var plan = testWeekPlan(household);
var list = testShoppingList(household, plan);
list.setStatus("published");
var ingredient = testIngredient(household, "Tomatoes", false);
var item = testItem(list, ingredient, new BigDecimal("5.00"), "pcs");
list.getItems().add(item);
when(shoppingListRepository.findById(list.getId())).thenReturn(Optional.of(list));
assertThatThrownBy(() -> shoppingService.deleteItem(HOUSEHOLD_ID, list.getId(), item.getId()))
.isInstanceOf(ValidationException.class);
}
// ── Household mismatch ──
@Test
@@ -415,15 +367,6 @@ class ShoppingServiceTest {
.isInstanceOf(ResourceNotFoundException.class);
}
@Test
void publishShouldThrowWhenListNotFound() {
var listId = UUID.randomUUID();
when(shoppingListRepository.findById(listId)).thenReturn(Optional.empty());
assertThatThrownBy(() -> shoppingService.publish(HOUSEHOLD_ID, listId))
.isInstanceOf(ResourceNotFoundException.class);
}
// ── Generate from plan with empty slots ──
@Test
@@ -442,7 +385,6 @@ class ShoppingServiceTest {
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
assertThat(result.items()).isEmpty();
assertThat(result.status()).isEqualTo("draft");
}
// ── Item with category ──