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:
@@ -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(
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
Reference in New Issue
Block a user