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:
@@ -32,12 +32,6 @@ public class ShoppingListController {
|
|||||||
return shoppingService.getShoppingList(householdId, id);
|
return shoppingService.getShoppingList(householdId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/v1/shopping-lists/{id}/publish")
|
|
||||||
public PublishResponse publish(@PathVariable UUID id, Principal principal) {
|
|
||||||
UUID householdId = householdResolver.resolve(principal.getName());
|
|
||||||
return shoppingService.publish(householdId, id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PatchMapping("/v1/shopping-lists/{listId}/items/{itemId}")
|
@PatchMapping("/v1/shopping-lists/{listId}/items/{itemId}")
|
||||||
public ShoppingListItemResponse checkItem(@PathVariable UUID listId,
|
public ShoppingListItemResponse checkItem(@PathVariable UUID listId,
|
||||||
@PathVariable UUID itemId,
|
@PathVariable UUID itemId,
|
||||||
|
|||||||
@@ -1,21 +1,246 @@
|
|||||||
package com.recipeapp.shopping;
|
package com.recipeapp.shopping;
|
||||||
|
|
||||||
|
import com.recipeapp.auth.UserAccountRepository;
|
||||||
|
import com.recipeapp.auth.entity.UserAccount;
|
||||||
|
import com.recipeapp.common.ResourceNotFoundException;
|
||||||
|
import com.recipeapp.household.HouseholdRepository;
|
||||||
|
import com.recipeapp.planning.WeekPlanRepository;
|
||||||
|
import com.recipeapp.planning.entity.WeekPlan;
|
||||||
|
import com.recipeapp.recipe.IngredientRepository;
|
||||||
|
import com.recipeapp.recipe.entity.Ingredient;
|
||||||
|
import com.recipeapp.recipe.entity.RecipeIngredient;
|
||||||
import com.recipeapp.shopping.dto.*;
|
import com.recipeapp.shopping.dto.*;
|
||||||
|
import com.recipeapp.shopping.entity.ShoppingList;
|
||||||
|
import com.recipeapp.shopping.entity.ShoppingListItem;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.math.BigDecimal;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public interface ShoppingService {
|
@Service
|
||||||
|
@Transactional
|
||||||
|
public class ShoppingService {
|
||||||
|
|
||||||
ShoppingListResponse generateFromPlan(UUID householdId, UUID weekPlanId);
|
private final ShoppingListRepository shoppingListRepository;
|
||||||
|
private final ShoppingListItemRepository shoppingListItemRepository;
|
||||||
|
private final WeekPlanRepository weekPlanRepository;
|
||||||
|
private final HouseholdRepository householdRepository;
|
||||||
|
private final IngredientRepository ingredientRepository;
|
||||||
|
private final UserAccountRepository userAccountRepository;
|
||||||
|
|
||||||
ShoppingListResponse getShoppingList(UUID householdId, UUID shoppingListId);
|
public ShoppingService(ShoppingListRepository shoppingListRepository,
|
||||||
|
ShoppingListItemRepository shoppingListItemRepository,
|
||||||
|
WeekPlanRepository weekPlanRepository,
|
||||||
|
HouseholdRepository householdRepository,
|
||||||
|
IngredientRepository ingredientRepository,
|
||||||
|
UserAccountRepository userAccountRepository) {
|
||||||
|
this.shoppingListRepository = shoppingListRepository;
|
||||||
|
this.shoppingListItemRepository = shoppingListItemRepository;
|
||||||
|
this.weekPlanRepository = weekPlanRepository;
|
||||||
|
this.householdRepository = householdRepository;
|
||||||
|
this.ingredientRepository = ingredientRepository;
|
||||||
|
this.userAccountRepository = userAccountRepository;
|
||||||
|
}
|
||||||
|
|
||||||
PublishResponse publish(UUID householdId, UUID shoppingListId);
|
|
||||||
|
|
||||||
ShoppingListItemResponse checkItem(UUID householdId, UUID listId, UUID itemId,
|
public ShoppingListResponse generateFromPlan(UUID householdId, UUID weekPlanId) {
|
||||||
CheckItemRequest request, UUID userId);
|
WeekPlan weekPlan = weekPlanRepository.findById(weekPlanId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Week plan not found"));
|
||||||
|
|
||||||
ShoppingListItemResponse addItem(UUID householdId, UUID shoppingListId, AddItemRequest request);
|
if (!weekPlan.getHousehold().getId().equals(householdId)) {
|
||||||
|
throw new ResourceNotFoundException("Week plan not found");
|
||||||
|
}
|
||||||
|
|
||||||
void deleteItem(UUID householdId, UUID listId, UUID itemId);
|
var household = weekPlan.getHousehold();
|
||||||
|
|
||||||
|
ShoppingList shoppingList = new ShoppingList(household, weekPlan);
|
||||||
|
shoppingList = shoppingListRepository.save(shoppingList);
|
||||||
|
|
||||||
|
// Aggregate ingredients across all slots/recipes
|
||||||
|
// Key: ingredientId + unit -> merged data
|
||||||
|
Map<String, MergedIngredient> merged = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (var slot : weekPlan.getSlots()) {
|
||||||
|
var recipe = slot.getRecipe();
|
||||||
|
for (RecipeIngredient ri : recipe.getIngredients()) {
|
||||||
|
Ingredient ingredient = ri.getIngredient();
|
||||||
|
|
||||||
|
// Filter out staples
|
||||||
|
if (ingredient.isStaple()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String key = ingredient.getId().toString() + "|" + ri.getUnit();
|
||||||
|
merged.computeIfAbsent(key, k -> new MergedIngredient(ingredient, ri.getUnit()))
|
||||||
|
.addQuantity(ri.getQuantity())
|
||||||
|
.addRecipeId(recipe.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create shopping list items
|
||||||
|
for (MergedIngredient mi : merged.values()) {
|
||||||
|
ShoppingListItem item = new ShoppingListItem(
|
||||||
|
shoppingList,
|
||||||
|
mi.ingredient,
|
||||||
|
null,
|
||||||
|
mi.totalQuantity,
|
||||||
|
mi.unit,
|
||||||
|
mi.recipeIds.stream().distinct().toArray(UUID[]::new)
|
||||||
|
);
|
||||||
|
shoppingList.getItems().add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
shoppingListRepository.save(shoppingList);
|
||||||
|
|
||||||
|
return toResponse(shoppingList);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public ShoppingListResponse getShoppingList(UUID householdId, UUID shoppingListId) {
|
||||||
|
ShoppingList list = findList(householdId, shoppingListId);
|
||||||
|
return toResponse(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ShoppingListItemResponse checkItem(UUID householdId, UUID listId, UUID itemId,
|
||||||
|
CheckItemRequest request, UUID userId) {
|
||||||
|
ShoppingList list = findList(householdId, listId);
|
||||||
|
ShoppingListItem item = findItem(list, itemId);
|
||||||
|
|
||||||
|
item.setChecked(request.isChecked());
|
||||||
|
|
||||||
|
if (request.isChecked()) {
|
||||||
|
UserAccount user = userAccountRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("User not found"));
|
||||||
|
item.setCheckedBy(user);
|
||||||
|
} else {
|
||||||
|
item.setCheckedBy(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
shoppingListItemRepository.save(item);
|
||||||
|
return toItemResponse(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ShoppingListItemResponse addItem(UUID householdId, UUID shoppingListId, AddItemRequest request) {
|
||||||
|
ShoppingList list = findList(householdId, shoppingListId);
|
||||||
|
|
||||||
|
Ingredient ingredient = null;
|
||||||
|
if (request.ingredientId() != null) {
|
||||||
|
ingredient = ingredientRepository.findById(request.ingredientId())
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Ingredient not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
ShoppingListItem item = new ShoppingListItem(
|
||||||
|
list,
|
||||||
|
ingredient,
|
||||||
|
request.customName(),
|
||||||
|
request.quantity(),
|
||||||
|
request.unit(),
|
||||||
|
new UUID[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
item = shoppingListItemRepository.save(item);
|
||||||
|
list.getItems().add(item);
|
||||||
|
|
||||||
|
return toItemResponse(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void deleteItem(UUID householdId, UUID listId, UUID itemId) {
|
||||||
|
ShoppingList list = findList(householdId, listId);
|
||||||
|
ShoppingListItem item = findItem(list, itemId);
|
||||||
|
list.getItems().remove(item);
|
||||||
|
shoppingListItemRepository.delete(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ──
|
||||||
|
|
||||||
|
private ShoppingList findList(UUID householdId, UUID shoppingListId) {
|
||||||
|
ShoppingList list = shoppingListRepository.findById(shoppingListId)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Shopping list not found"));
|
||||||
|
|
||||||
|
if (!list.getHousehold().getId().equals(householdId)) {
|
||||||
|
throw new ResourceNotFoundException("Shopping list not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShoppingListItem findItem(ShoppingList list, UUID itemId) {
|
||||||
|
return list.getItems().stream()
|
||||||
|
.filter(i -> i.getId().equals(itemId))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Shopping list item not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShoppingListResponse toResponse(ShoppingList list) {
|
||||||
|
List<ShoppingListItemResponse> items = list.getItems().stream()
|
||||||
|
.map(this::toItemResponse)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return new ShoppingListResponse(
|
||||||
|
list.getId(),
|
||||||
|
list.getWeekPlan().getId(),
|
||||||
|
items
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShoppingListItemResponse toItemResponse(ShoppingListItem item) {
|
||||||
|
String name;
|
||||||
|
ShoppingListItemResponse.CategoryRef categoryRef = null;
|
||||||
|
UUID ingredientId = null;
|
||||||
|
|
||||||
|
if (item.getIngredient() != null) {
|
||||||
|
ingredientId = item.getIngredient().getId();
|
||||||
|
name = item.getIngredient().getName();
|
||||||
|
if (item.getIngredient().getCategory() != null) {
|
||||||
|
categoryRef = new ShoppingListItemResponse.CategoryRef(
|
||||||
|
item.getIngredient().getCategory().getId(),
|
||||||
|
item.getIngredient().getCategory().getName()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
name = item.getCustomName();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ShoppingListItemResponse(
|
||||||
|
item.getId(),
|
||||||
|
ingredientId,
|
||||||
|
name,
|
||||||
|
categoryRef,
|
||||||
|
item.getQuantity(),
|
||||||
|
item.getUnit(),
|
||||||
|
item.isChecked(),
|
||||||
|
item.getCheckedBy() != null ? item.getCheckedBy().getId() : null,
|
||||||
|
item.getSourceRecipes() != null ? Arrays.asList(item.getSourceRecipes()) : List.of()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MergedIngredient {
|
||||||
|
final Ingredient ingredient;
|
||||||
|
final String unit;
|
||||||
|
BigDecimal totalQuantity = BigDecimal.ZERO;
|
||||||
|
final List<UUID> recipeIds = new ArrayList<>();
|
||||||
|
|
||||||
|
MergedIngredient(Ingredient ingredient, String unit) {
|
||||||
|
this.ingredient = ingredient;
|
||||||
|
this.unit = unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergedIngredient addQuantity(BigDecimal qty) {
|
||||||
|
if (qty != null) {
|
||||||
|
this.totalQuantity = this.totalQuantity.add(qty);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergedIngredient addRecipeId(UUID recipeId) {
|
||||||
|
this.recipeIds.add(recipeId);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.recipeapp.shopping.dto;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public record PublishResponse(UUID id, String status, Instant publishedAt) {}
|
|
||||||
@@ -1,13 +1,10 @@
|
|||||||
package com.recipeapp.shopping.dto;
|
package com.recipeapp.shopping.dto;
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public record ShoppingListResponse(
|
public record ShoppingListResponse(
|
||||||
UUID id,
|
UUID id,
|
||||||
UUID weekPlanId,
|
UUID weekPlanId,
|
||||||
String status,
|
|
||||||
Instant publishedAt,
|
|
||||||
List<ShoppingListItemResponse> items
|
List<ShoppingListItemResponse> items
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.recipeapp.shopping.entity;
|
|||||||
import com.recipeapp.household.entity.Household;
|
import com.recipeapp.household.entity.Household;
|
||||||
import com.recipeapp.planning.entity.WeekPlan;
|
import com.recipeapp.planning.entity.WeekPlan;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -24,12 +23,6 @@ public class ShoppingList {
|
|||||||
@JoinColumn(name = "week_plan_id", nullable = false)
|
@JoinColumn(name = "week_plan_id", nullable = false)
|
||||||
private WeekPlan weekPlan;
|
private WeekPlan weekPlan;
|
||||||
|
|
||||||
@Column(nullable = false, length = 10)
|
|
||||||
private String status = "draft";
|
|
||||||
|
|
||||||
@Column(name = "published_at")
|
|
||||||
private Instant publishedAt;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "shoppingList", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "shoppingList", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<ShoppingListItem> items = new ArrayList<>();
|
private List<ShoppingListItem> items = new ArrayList<>();
|
||||||
|
|
||||||
@@ -43,9 +36,5 @@ public class ShoppingList {
|
|||||||
public UUID getId() { return id; }
|
public UUID getId() { return id; }
|
||||||
public Household getHousehold() { return household; }
|
public Household getHousehold() { return household; }
|
||||||
public WeekPlan getWeekPlan() { return weekPlan; }
|
public WeekPlan getWeekPlan() { return weekPlan; }
|
||||||
public String getStatus() { return status; }
|
|
||||||
public void setStatus(String status) { this.status = status; }
|
|
||||||
public Instant getPublishedAt() { return publishedAt; }
|
|
||||||
public void setPublishedAt(Instant publishedAt) { this.publishedAt = publishedAt; }
|
|
||||||
public List<ShoppingListItem> getItems() { return items; }
|
public List<ShoppingListItem> getItems() { return items; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Shopping lists are now always live — no draft/publish workflow.
|
||||||
|
ALTER TABLE shopping_list DROP COLUMN status;
|
||||||
|
ALTER TABLE shopping_list DROP COLUMN published_at;
|
||||||
@@ -3,7 +3,6 @@ package com.recipeapp.shopping;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.recipeapp.common.GlobalExceptionHandler;
|
import com.recipeapp.common.GlobalExceptionHandler;
|
||||||
import com.recipeapp.common.ValidationException;
|
|
||||||
import com.recipeapp.recipe.HouseholdResolver;
|
import com.recipeapp.recipe.HouseholdResolver;
|
||||||
import com.recipeapp.shopping.dto.*;
|
import com.recipeapp.shopping.dto.*;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
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 org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ class ShoppingListControllerTest {
|
|||||||
ITEM_ID, UUID.randomUUID(), "Tomatoes",
|
ITEM_ID, UUID.randomUUID(), "Tomatoes",
|
||||||
new ShoppingListItemResponse.CategoryRef(UUID.randomUUID(), "Produce"),
|
new ShoppingListItemResponse.CategoryRef(UUID.randomUUID(), "Produce"),
|
||||||
new BigDecimal("4.00"), "pcs", false, null, List.of(UUID.randomUUID()));
|
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(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||||
when(shoppingService.generateFromPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response);
|
when(shoppingService.generateFromPlan(HOUSEHOLD_ID, PLAN_ID)).thenReturn(response);
|
||||||
@@ -65,13 +63,12 @@ class ShoppingListControllerTest {
|
|||||||
.principal(() -> "sarah@example.com"))
|
.principal(() -> "sarah@example.com"))
|
||||||
.andExpect(status().isCreated())
|
.andExpect(status().isCreated())
|
||||||
.andExpect(jsonPath("$.id").value(LIST_ID.toString()))
|
.andExpect(jsonPath("$.id").value(LIST_ID.toString()))
|
||||||
.andExpect(jsonPath("$.status").value("draft"))
|
|
||||||
.andExpect(jsonPath("$.items[0].name").value("Tomatoes"));
|
.andExpect(jsonPath("$.items[0].name").value("Tomatoes"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getShoppingListShouldReturn200() throws Exception {
|
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(householdResolver.resolve("sarah@example.com")).thenReturn(HOUSEHOLD_ID);
|
||||||
when(shoppingService.getShoppingList(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
|
when(shoppingService.getShoppingList(HOUSEHOLD_ID, LIST_ID)).thenReturn(response);
|
||||||
@@ -83,20 +80,6 @@ class ShoppingListControllerTest {
|
|||||||
.andExpect(jsonPath("$.weekPlanId").value(PLAN_ID.toString()));
|
.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
|
@Test
|
||||||
void checkItemShouldReturn200() throws Exception {
|
void checkItemShouldReturn200() throws Exception {
|
||||||
var response = new ShoppingListItemResponse(
|
var response = new ShoppingListItemResponse(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.recipeapp.shopping;
|
|||||||
import com.recipeapp.auth.UserAccountRepository;
|
import com.recipeapp.auth.UserAccountRepository;
|
||||||
import com.recipeapp.auth.entity.UserAccount;
|
import com.recipeapp.auth.entity.UserAccount;
|
||||||
import com.recipeapp.common.ResourceNotFoundException;
|
import com.recipeapp.common.ResourceNotFoundException;
|
||||||
import com.recipeapp.common.ValidationException;
|
|
||||||
import com.recipeapp.household.HouseholdRepository;
|
import com.recipeapp.household.HouseholdRepository;
|
||||||
import com.recipeapp.household.entity.Household;
|
import com.recipeapp.household.entity.Household;
|
||||||
import com.recipeapp.planning.WeekPlanRepository;
|
import com.recipeapp.planning.WeekPlanRepository;
|
||||||
@@ -41,7 +40,7 @@ class ShoppingServiceTest {
|
|||||||
@Mock private IngredientRepository ingredientRepository;
|
@Mock private IngredientRepository ingredientRepository;
|
||||||
@Mock private UserAccountRepository userAccountRepository;
|
@Mock private UserAccountRepository userAccountRepository;
|
||||||
|
|
||||||
@InjectMocks private ShoppingServiceImpl shoppingService;
|
@InjectMocks private ShoppingService shoppingService;
|
||||||
|
|
||||||
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
private static final UUID HOUSEHOLD_ID = UUID.randomUUID();
|
||||||
private static final LocalDate WEEK_START = LocalDate.of(2026, 4, 6);
|
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());
|
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
||||||
|
|
||||||
assertThat(result.status()).isEqualTo("draft");
|
|
||||||
assertThat(result.items()).hasSize(2); // tomatoes + cheese (salt filtered)
|
assertThat(result.items()).hasSize(2); // tomatoes + cheese (salt filtered)
|
||||||
|
|
||||||
var tomatoItem = result.items().stream()
|
var tomatoItem = result.items().stream()
|
||||||
@@ -170,36 +168,6 @@ class ShoppingServiceTest {
|
|||||||
assertThat(result.items().getFirst().name()).isEqualTo("Tomatoes");
|
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 ──
|
// ── Check Item ──
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -267,22 +235,6 @@ class ShoppingServiceTest {
|
|||||||
assertThat(list.getItems()).isEmpty();
|
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 ──
|
// ── Household mismatch ──
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -415,15 +367,6 @@ class ShoppingServiceTest {
|
|||||||
.isInstanceOf(ResourceNotFoundException.class);
|
.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 ──
|
// ── Generate from plan with empty slots ──
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -442,7 +385,6 @@ class ShoppingServiceTest {
|
|||||||
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
ShoppingListResponse result = shoppingService.generateFromPlan(HOUSEHOLD_ID, plan.getId());
|
||||||
|
|
||||||
assertThat(result.items()).isEmpty();
|
assertThat(result.items()).isEmpty();
|
||||||
assertThat(result.status()).isEqualTo("draft");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Item with category ──
|
// ── Item with category ──
|
||||||
|
|||||||
Reference in New Issue
Block a user