fix(validation): scoped duplicate checks for Category and Food#216
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds per-scope duplicate name validation for Category (per menu) and Food (per category), replacing the previous global existsByNameIgnoreCase checks. It addresses issue #156 by introducing repository methods that include the parent scope, and adds a required menuId field on CategoryRequestDTO so the category create flow can be scoped.
Changes:
- Adds
existsByNameIgnoreCaseAndMenu_MenuIdonCategoryRepositoryand uses it inCategoryServiceImpl.createCategory; adds requiredmenuIdtoCategoryRequestDTO. - Adds
existsByNameIgnoreCaseAndCategory_CategoryIdonFoodRepositoryand uses it in bothcreateFoodandupdateFood, replacing the global duplicate-name check. - Adds debug/warn logging around the new duplicate detection paths.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| RestroHub/src/main/java/com/restroly/qrmenu/category/dto/CategoryRequestDTO.java | Adds @NotNull Long menuId so create requests carry the menu scope for duplicate validation. |
| RestroHub/src/main/java/com/restroly/qrmenu/category/repository/CategoryRepository.java | New derived query existsByNameIgnoreCaseAndMenu_MenuId for per-menu duplicate detection. |
| RestroHub/src/main/java/com/restroly/qrmenu/category/service/CategoryServiceImpl.java | Adds per-menu duplicate check in createCategory, throwing DuplicateResourceException. |
| RestroHub/src/main/java/com/restroly/qrmenu/food/repository/FoodRepository.java | New derived query existsByNameIgnoreCaseAndCategory_CategoryId for per-category duplicate detection. |
| RestroHub/src/main/java/com/restroly/qrmenu/food/service/FoodServiceImpl.java | Switches create/update duplicate checks from global name to category-scoped, with updated logs and error messages. |
Comments suppressed due to low confidence (1)
RestroHub/src/main/java/com/restroly/qrmenu/category/service/CategoryServiceImpl.java:52
- The new category is built and saved without any association to the Menu identified by
menuId.CategoryDTO.toEntity(called immediately below) does not set themenufield, and there is nomenuRepository.findById(menuId)lookup orcategory.getMenu().add(menu)call here. As a result:
- The
menuIdfrom the request is used only for the duplicate check and is silently discarded — the persisted category will not actually belong to any menu, so the very nextcreateCategorycall with the same name andmenuIdwill again returnfalsefromexistsByNameIgnoreCaseAndMenu_MenuId(...)and create another duplicate. - A non-existent
menuIdis never validated againstMenuRepository, unlike the existing pattern inBranchServiceImpl.createBranch(RestroHub/src/main/java/com/restroly/qrmenu/branch/service/BranchServiceImpl.java:44-49) andMenuServiceImpl.createMenu(RestroHub/src/main/java/com/restroly/qrmenu/menu/service/MenuServiceImpl.java:46-49).
The create flow should look up the Menu by menuId (throwing ResourceNotFoundException if absent) and attach it to the new Category (e.g. category.getMenu().add(menu) / menu.getCategories().add(category)) before saving, otherwise the scoped duplicate validation cannot work.
if(categoryRepository.existsByNameIgnoreCaseAndMenu_MenuId(
requestDTO.getName(), requestDTO.getMenuId())) {
log.warn("Category with name '{}' already exists in menu '{}'", requestDTO.getName(), requestDTO.getMenuId());
throw new DuplicateResourceException("Category with name '" + requestDTO.getName() + "' already exists");
}
Category category = CategoryDTO.toEntity(
CategoryDTO.builder()
.name(requestDTO.getName())
.description(requestDTO.getDescription())
.isDelete(false) // default value
.updatedDate(LocalDateTime.now())
.build()
);
Category savedCategory = categoryRepository.save(category);
log.info("Category created with ID: {}", savedCategory.getCategoryId());
return CategoryResponseDTO.fromEntity(savedCategory);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (updateDTO.getName() != null && | ||
| !updateDTO.getName().equalsIgnoreCase(existingFood.getName()) && | ||
| foodRepository.existsByNameIgnoreCase(updateDTO.getName())) { | ||
| !updateDTO.getName().equalsIgnoreCase(existingFood.getName()) && | ||
| foodRepository.existsByNameIgnoreCaseAndCategory_CategoryId( | ||
| updateDTO.getName(), existingFood.getCategory().getCategoryId())) { | ||
|
|
||
| log.warn("Attempt to update food with duplicate name: {}", updateDTO.getName()); | ||
| throw new ResourceAlreadyExistsException( | ||
| String.format(FOOD_EXISTS_MSG, updateDTO.getName())); | ||
| log.warn("Attempt to update food with duplicate name '{}' in category '{}'", | ||
| updateDTO.getName(), existingFood.getCategory().getCategoryId()); | ||
| throw new ResourceAlreadyExistsException(String.format(FOOD_EXISTS_MSG, updateDTO.getName())); | ||
| } |
|
Hi @rdodiya I have fixed the changes as per review
This resolves the duplicate validation issue mentioned in the review. |
|
Hi @iRahmanG, While creating categories, we are not assigning them to any menu. Categories are created independently first, and later menus are created which can contain multiple categories belonging to the same branch.
|
|
Hi @rdodiya
Thanks for your feedback. |


Summary
This PR introduces corrected duplicate validation logic to ensure data integrity:
existsByNameIgnoreCaseAndMenu_MenuId.existsByNameIgnoreCaseAndCategory_CategoryId.Why
The earlier PR (#160) attempted similar changes but contained bugs. This new PR provides a clean, corrected implementation with proper repository methods and service logic.
Notes
Close Issue: #156