diff --git a/README.md b/README.md index 76bb2d6c0..b7a24e178 100644 --- a/README.md +++ b/README.md @@ -96,44 +96,85 @@ docker compose -p kitchenpos up -d ## 용어 사전 -| 컨텍스트 | 한글명 | 영문명 | 설명 | -|--------|-----------------|------------------|---------------------------------------------------------------------------------------| -| 공통 | 가격 | Price | 상품 또는 메뉴의 대가로 손님이 지불하는 금액. 한국 원 단위이며 0원 이상이다. | -| 공통 | 비속어 | Profanity | 욕설이나 비하하는 말, 예절에 어긋나게 대상을 낮추거나 품위 없이 천한 말. | -| 공통 | 손님 | Guest | 메뉴를 임의의 주문 유형으로 주문할 수 있는 사람. | -| 공통 | 수량 | Quantity | 특정 유형은 가진 개체들의 수. 예) 메뉴 수량, 주문 수량. A 메뉴를 2개, B 메뉴를 3개 주문했다면 수량은 5개이다. | -| 공통 | 종류 | Cardinality | 특정 유형을 가진 개체들의 고유한 가짓수. 예) 상품 종류, 메뉴 종류. A 메뉴를 2개, B 메뉴를 3개 주문했다면 종류는 2가지이다. | -| 상품 | 상품 | Product | 실제로 손님에게 전달할 수 있는 음식, 음료 등의 물건. | -| 메뉴 | 메뉴 | Menu | 하나 이상의 상품으로 구성된 실제로 손님에게 판매 가능한 단위. | -| 메뉴 | (메뉴) 노출 | Display | 메뉴를 목록 또는 화면에 보여지게 함. | -| 메뉴 | (메뉴) 숨김 | Hide | 메뉴를 목록 또는 화면에 보여지지 않게 함. | -| 메뉴 | 메뉴 상품 | Menu product | 메뉴에 포함되는 상품의 정보로 상품의 식별자와 수량을 갖는다. | -| 메뉴 그룹 | 메뉴 그룹 | Menu group | 메뉴의 집합으로 모든 메뉴는 하나의 메뉴 그룹에 속해야 한다. | -| 주문 테이블 | 빈 테이블 | Empty table | 손님이 없는 주문 테이블. | -| 주문 테이블 | 점유된 테이블 | Occupied table | 손님이 앉은 주문 테이블. | -| 주문 테이블 | 주문 테이블 | Order table | 손님이 앉아서 메뉴를 주문할 수 있는 테이블. 주문 테이블은 손님에 의해 점유되어 있거나 비어 있다. | -| 주문 공통 | (주문) 접수 (상태) | Accepted | 손님이 생성한 주문을 매장에서 확인한 상태. | -| 주문 공통 | (주문) 접수 대기 (상태) | Waiting | 손님이 주문을 생성했지만 아직 매장에서 확인하지 않은 상태. | -| 주문 공통 | 주문 | Order | 손님이 메뉴를 어떤 유형으로 전달받을지 지정하여 요구한 것. 주문 유형, 주문 항목, 주문 상태, 주문 시각, 배달 주소, 주문 테이블 정보를 포함한다. | -| 주문 공통 | 주문 상태 | Order status | 하나의 주문이 처리되는 과정. 하나의 주문은 한 시점에 단 하나의 상태여야 한다(동시에 여러 상태일 수 없다). | -| 주문 공통 | 주문 유형 | Order type | 주문에 포함된 메뉴들이 손님에게 전달되는 방법. 배달, 포장, 매장이 있다. | -| 주문 공통 | 주문 항목 | Order line item | 주문의 일부분으로 하나의 메뉴와 그 수량, 그리고 가격으로 구성된다. 하나의 주문에는 한 개 이상의 주문 항목이 포함되어 있어야 한다. | -| 매장 주문 | 매장 (주문) | Eat in | 주문 테이블을 점유한 손님에게 메뉴가 전달되는 주문 유형. | -| 매장 주문 | 서빙 | Serving | 손님이 주문한 메뉴를 손님에게 손님이 앉아 있는 주문 테이블로 전달하는 행위. | -| 매장 주문 | 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 손님에게 손님이 앉아 있는 주문 테이블로 전달 완료한 상태. | -| 매장 주문 | (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 서빙이 완료된 상태여야 주문이 완료될 수 있다. | -| 배달 주문 | 배달 (주문) | Delivery | 손님이 지정한 배달 주소로 배달 대행사를 통해 메뉴가 전달되는 주문 유형. | -| 배달 주문 | 배달 대행사 | Delivery agency | 어떤 물건 또는 음식 등의 배달 업무를 대행해주는 사업자. | -| 배달 주문 | 배달 완료 (상태) | Delivered | 배달 대행사의 배달원을 통해 손님의 주문 주소로 메뉴가 배달 완료된 상태. 배달 주문인 경우에만 나타날 수 있다. | -| 배달 주문 | 배달 주소 | Delivery address | 손님이 메뉴를 전달 받고 싶어하는 위치를 주소로 나타낸 것. 배달 주문인 경우 반드시 있어야 한다. | -| 배달 주문 | 배달중 (상태) | Delivering | 배달 대행사의 배달원을 통해 손님의 주문 주소로 메뉴가 배달되고 있는 상태. 배달 주문인 경우에만 나타날 수 있다. | -| 배달 주문 | 서빙 | Serving | 손님이 주문한 배달 대행사의 배달원에게 전달하는 행위. | -| 배달 주문 | 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 배달 대행사의 배달원에게 전달 완료한 상태. | -| 배달 주문 | (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 배달이 완료된 상태여야 주문이 완료될 수 있다. | -| 포장 주문 | 서빙 | Serving | 손님이 주문한 메뉴를 손님에게 직접 전달하는 행위. | -| 포장 주문 | 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 손님에게 직접 전달 완료한 상태. | -| 포장 주문 | (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 서빙이 완료된 상태여야 주문이 완료될 수 있다. | -| 포장 주문 | 포장 (주문) | Takeout | 손님이 매장에 방문하여 메뉴를 직접 전달받지만 매장 내에서 주문 테이블을 점유하지는 않는 유형. | +### 공통 + +| 한글명 | 영문명 | 설명 | +|-----|-------------|------------------------------------------------------------------------------| +| 가격 | Price | 상품 또는 메뉴의 대가로 손님이 지불하는 금액. 한국 원 단위이며 0원 이상이다. | +| 비속어 | Profanity | 욕설이나 비하하는 말, 예절에 어긋나게 대상을 낮추거나 품위 없이 천한 말. | +| 손님 | Guest | 메뉴를 임의의 주문 유형으로 주문할 수 있는 사람. | +| 수량 | Quantity | 특정 유형은 가진 개체들의 수. 예) 메뉴 수량, 주문 수량. A 메뉴를 2개, B 메뉴를 3개 주문했다면 수량은 5개이다. | +| 종류 | Cardinality | 특정 유형을 가진 개체들의 고유한 가짓수. 예) 상품 종류, 메뉴 종류. A 메뉴를 2개, B 메뉴를 3개 주문했다면 종류는 2가지이다. | + +### 상품 + +| 한글명 | 영문명 | 설명 | +|-----|---------|---------------------------------| +| 상품 | Product | 실제로 손님에게 전달할 수 있는 음식, 음료 등의 물건. | + +### 메뉴 + +| 한글명 | 영문명 | 설명 | +|----------|-----------------------|------------------------------------------| +| 메뉴 | Menu | 하나 이상의 상품으로 구성된 실제로 손님에게 판매 가능한 단위. | +| (메뉴) 노출 | Display | 메뉴를 목록 또는 화면에 보여지게 함. | +| (메뉴) 숨김 | Hide | 메뉴를 목록 또는 화면에 보여지지 않게 함. | +| 메뉴 상품 | Menu product | 메뉴에 포함되는 상품의 정보로 상품의 식별자와 메뉴 상품 수량을 갖는다. | +| 메뉴 상품 수량 | Menu product quantity | 메뉴에 포함되는 상품의 수량. | +| 소계 | Subtotal | 메뉴에 포함되는 각 상품의 개당 가격에 메뉴 상품 수량을 곱한 가격. | +| 메뉴 그룹 | Menu group | 메뉴의 집합으로 모든 메뉴는 하나의 메뉴 그룹에 속해야 한다. | + +### 매장 주문 + +| 한글명 | 영문명 | 설명 | +|-----------------|-----------------|---------------------------------------------------------------------------------------| +| 빈 테이블 | Empty table | 손님이 없는 주문 테이블. | +| 점유된 테이블 | Occupied table | 손님이 앉은 주문 테이블. | +| 주문 테이블 | Order table | 손님이 앉아서 메뉴를 주문할 수 있는 테이블. 주문 테이블은 손님에 의해 점유되어 있거나 비어 있다. | +| (주문) 접수 (상태) | Accepted | 손님이 생성한 주문을 매장에서 확인한 상태. | +| (주문) 접수 대기 (상태) | Waiting | 손님이 주문을 생성했지만 아직 매장에서 확인하지 않은 상태. | +| 주문 | Order | 손님이 메뉴를 어떤 유형으로 전달받을지 지정하여 요구한 것. 주문 유형, 주문 항목, 주문 상태, 주문 시각, 배달 주소, 주문 테이블 정보를 포함한다. | +| 주문 상태 | Order status | 하나의 주문이 처리되는 과정. 하나의 주문은 한 시점에 단 하나의 상태여야 한다(동시에 여러 상태일 수 없다). | +| 주문 유형 | Order type | 주문에 포함된 메뉴들이 손님에게 전달되는 방법. 배달, 포장, 매장이 있다. | +| 주문 항목 | Order line item | 주문의 일부분으로 하나의 메뉴와 그 수량, 그리고 가격으로 구성된다. 하나의 주문에는 한 개 이상의 주문 항목이 포함되어 있어야 한다. | +| 매장 (주문) | Eat in | 주문 테이블을 점유한 손님에게 메뉴가 전달되는 주문 유형. | +| 서빙 | Serving | 손님이 주문한 메뉴를 손님에게 손님이 앉아 있는 주문 테이블로 전달하는 행위. | +| 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 손님에게 손님이 앉아 있는 주문 테이블로 전달 완료한 상태. | +| (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 서빙이 완료된 상태여야 주문이 완료될 수 있다. | + +### 배달 주문 + +| 한글명 | 영문명 | 설명 | +|-----------------|------------------|---------------------------------------------------------------------------------------| +| (주문) 접수 (상태) | Accepted | 손님이 생성한 주문을 매장에서 확인한 상태. | +| (주문) 접수 대기 (상태) | Waiting | 손님이 주문을 생성했지만 아직 매장에서 확인하지 않은 상태. | +| 주문 | Order | 손님이 메뉴를 어떤 유형으로 전달받을지 지정하여 요구한 것. 주문 유형, 주문 항목, 주문 상태, 주문 시각, 배달 주소, 주문 테이블 정보를 포함한다. | +| 주문 상태 | Order status | 하나의 주문이 처리되는 과정. 하나의 주문은 한 시점에 단 하나의 상태여야 한다(동시에 여러 상태일 수 없다). | +| 주문 유형 | Order type | 주문에 포함된 메뉴들이 손님에게 전달되는 방법. 배달, 포장, 매장이 있다. | +| 주문 항목 | Order line item | 주문의 일부분으로 하나의 메뉴와 그 수량, 그리고 가격으로 구성된다. 하나의 주문에는 한 개 이상의 주문 항목이 포함되어 있어야 한다. | +| 배달 (주문) | Delivery | 손님이 지정한 배달 주소로 배달 대행사를 통해 메뉴가 전달되는 주문 유형. | +| 배달 대행사 | Delivery agency | 어떤 물건 또는 음식 등의 배달 업무를 대행해주는 사업자. | +| 배달 완료 (상태) | Delivered | 배달 대행사의 배달원을 통해 손님의 주문 주소로 메뉴가 배달 완료된 상태. 배달 주문인 경우에만 나타날 수 있다. | +| 배달 주소 | Delivery address | 손님이 메뉴를 전달 받고 싶어하는 위치를 주소로 나타낸 것. 배달 주문인 경우 반드시 있어야 한다. | +| 배달중 (상태) | Delivering | 배달 대행사의 배달원을 통해 손님의 주문 주소로 메뉴가 배달되고 있는 상태. 배달 주문인 경우에만 나타날 수 있다. | +| 서빙 | Serving | 손님이 주문한 배달 대행사의 배달원에게 전달하는 행위. | +| 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 배달 대행사의 배달원에게 전달 완료한 상태. | +| (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 배달이 완료된 상태여야 주문이 완료될 수 있다. | + +### 포장 주문 + +| 한글명 | 영문명 | 설명 | +|-----------------|-----------------|---------------------------------------------------------------------------------------| +| (주문) 접수 (상태) | Accepted | 손님이 생성한 주문을 매장에서 확인한 상태. | +| (주문) 접수 대기 (상태) | Waiting | 손님이 주문을 생성했지만 아직 매장에서 확인하지 않은 상태. | +| 주문 | Order | 손님이 메뉴를 어떤 유형으로 전달받을지 지정하여 요구한 것. 주문 유형, 주문 항목, 주문 상태, 주문 시각, 배달 주소, 주문 테이블 정보를 포함한다. | +| 주문 상태 | Order status | 하나의 주문이 처리되는 과정. 하나의 주문은 한 시점에 단 하나의 상태여야 한다(동시에 여러 상태일 수 없다). | +| 주문 유형 | Order type | 주문에 포함된 메뉴들이 손님에게 전달되는 방법. 배달, 포장, 매장이 있다. | +| 주문 항목 | Order line item | 주문의 일부분으로 하나의 메뉴와 그 수량, 그리고 가격으로 구성된다. 하나의 주문에는 한 개 이상의 주문 항목이 포함되어 있어야 한다. | +| 서빙 | Serving | 손님이 주문한 메뉴를 손님에게 직접 전달하는 행위. | +| 서빙 완료 (상태) | Served | 서빙을 통해 메뉴를 손님에게 직접 전달 완료한 상태. | +| (주문) 완료 (상태) | Completed | 주문 과정이 종결된 상태로 추가적인 작업이 필요하지 않음. 서빙이 완료된 상태여야 주문이 완료될 수 있다. | +| 포장 (주문) | Takeout | 손님이 매장에 방문하여 메뉴를 직접 전달받지만 매장 내에서 주문 테이블을 점유하지는 않는 유형. | ## 모델링 @@ -155,19 +196,24 @@ docker compose -p kitchenpos up -d * `Menu`는 하나의 `MenuGroup`을 갖는다(`Menu`는 하나의 `MenuGroup`에 속한다). * `Menu`는 노출 여부 상태를 갖는다. * `Menu`는 `Price`를 갖는다. - * `Menu`의 `Price`는 메뉴 가격 정책을 따라야 한다. + * `Menu`의 `Price`를 변경할 수 있다. + * `Menu`의 `Price`는 메뉴 노출 정책을 따라야 한다. + * `Menu`의 `Price`는 메뉴 노출 정책을 위반하도록 변경할 수 없다. * `Menu`는 `Menu`에 포함되는 하나 이상의 `MenuProduct`를 갖는다. #### 메뉴 그룹 * `MenuGroup`은 `Name`을 갖는다. -* #### 메뉴 상품 * `MenuProduct`는 `Product`의 식별자를 갖는다. -* `MenuProduct`는 상품의 수량을 갖는다. - * 상품의 수량은 `1` 이상이다. +* `MenuProduct`는 `Product`의 개당 `Price`를 갖는다. +* `MenuProduct`는 `MenuProductQuantity`를 갖는다. + +##### 메뉴 상품 수량 + +* `MenuProductQuantity`은 `1` 이상이다. #### 메뉴 노출 정책 diff --git a/src/main/java/kitchenpos/common/price/Price.java b/src/main/java/kitchenpos/common/price/Price.java index a716d2122..542ae3939 100644 --- a/src/main/java/kitchenpos/common/price/Price.java +++ b/src/main/java/kitchenpos/common/price/Price.java @@ -8,7 +8,7 @@ *
  • 가격은 음수일 수 없다.
  • * */ -public class Price { +public class Price implements Comparable { public final BigDecimal value; @@ -30,6 +30,23 @@ public Price(double value) { this(BigDecimal.valueOf(value)); } + public Price add(Price augend) { + return new Price(this.value.add(augend.value)); + } + + public Price multiply(BigDecimal multiplicand) { + return new Price(this.value.multiply(multiplicand)); + } + + public Price multiply(long multiplicand) { + return this.multiply(BigDecimal.valueOf(multiplicand)); + } + + @Override + public int compareTo(Price o) { + return this.value.compareTo(o.value); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/kitchenpos/menu/api/MenuGroupRestController.java b/src/main/java/kitchenpos/menu/api/MenuGroupRestController.java index 7d719de59..3de01c8e1 100644 --- a/src/main/java/kitchenpos/menu/api/MenuGroupRestController.java +++ b/src/main/java/kitchenpos/menu/api/MenuGroupRestController.java @@ -3,7 +3,8 @@ import java.net.URI; import java.util.List; import kitchenpos.menu.application.MenuGroupService; -import kitchenpos.menu.domain.MenuGroup; +import kitchenpos.menu.tobe.application.dto.CreateMenuGroupCommand; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -21,9 +22,9 @@ public MenuGroupRestController(final MenuGroupService menuGroupService) { } @PostMapping - public ResponseEntity create(@RequestBody final MenuGroup request) { + public ResponseEntity create(@RequestBody final CreateMenuGroupCommand request) { final MenuGroup response = menuGroupService.create(request); - return ResponseEntity.created(URI.create("/api/menu-groups/" + response.getId())) + return ResponseEntity.created(URI.create("/api/menu-groups/" + response.id)) .body(response); } diff --git a/src/main/java/kitchenpos/menu/api/MenuRestController.java b/src/main/java/kitchenpos/menu/api/MenuRestController.java index 754fa4142..77bb702e9 100644 --- a/src/main/java/kitchenpos/menu/api/MenuRestController.java +++ b/src/main/java/kitchenpos/menu/api/MenuRestController.java @@ -4,7 +4,9 @@ import java.util.List; import java.util.UUID; import kitchenpos.menu.application.MenuService; -import kitchenpos.menu.domain.Menu; +import kitchenpos.menu.tobe.application.dto.ChangeMenuPriceCommand; +import kitchenpos.menu.tobe.application.dto.CreateMenuCommand; +import kitchenpos.menu.tobe.domain.entity.Menu; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -17,6 +19,7 @@ @RequestMapping("/api/menus") @RestController public class MenuRestController { + private final MenuService menuService; public MenuRestController(final MenuService menuService) { @@ -24,15 +27,22 @@ public MenuRestController(final MenuService menuService) { } @PostMapping - public ResponseEntity create(@RequestBody final Menu request) { + public ResponseEntity create(@RequestBody final CreateMenuCommand request) { final Menu response = menuService.create(request); - return ResponseEntity.created(URI.create("/api/menus/" + response.getId())) + return ResponseEntity.created(URI.create("/api/menus/" + response.id)) .body(response); } @PutMapping("/{menuId}/price") - public ResponseEntity changePrice(@PathVariable final UUID menuId, @RequestBody final Menu request) { - return ResponseEntity.ok(menuService.changePrice(menuId, request)); + public ResponseEntity changePrice( + @PathVariable final UUID menuId, + @RequestBody final Menu request + ) { + final ChangeMenuPriceCommand command = new ChangeMenuPriceCommand( + menuId, + request.price().value + ); + return ResponseEntity.ok(menuService.changePrice(command)); } @PutMapping("/{menuId}/display") diff --git a/src/main/java/kitchenpos/menu/application/MenuGroupService.java b/src/main/java/kitchenpos/menu/application/MenuGroupService.java index 595398909..7a3a653c4 100644 --- a/src/main/java/kitchenpos/menu/application/MenuGroupService.java +++ b/src/main/java/kitchenpos/menu/application/MenuGroupService.java @@ -1,30 +1,33 @@ package kitchenpos.menu.application; import java.util.List; -import java.util.Objects; import java.util.UUID; -import kitchenpos.menu.domain.MenuGroup; -import kitchenpos.menu.domain.MenuGroupRepository; +import kitchenpos.common.name.Name; +import kitchenpos.common.name.NameFactory; +import kitchenpos.menu.tobe.application.dto.CreateMenuGroupCommand; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class MenuGroupService { + private final MenuGroupRepository menuGroupRepository; - public MenuGroupService(final MenuGroupRepository menuGroupRepository) { + private final NameFactory nameFactory; + + @Autowired + public MenuGroupService(MenuGroupRepository menuGroupRepository, NameFactory nameFactory) { this.menuGroupRepository = menuGroupRepository; + this.nameFactory = nameFactory; } @Transactional - public MenuGroup create(final MenuGroup request) { - final String name = request.getName(); - if (Objects.isNull(name) || name.isEmpty()) { - throw new IllegalArgumentException(); - } - final MenuGroup menuGroup = new MenuGroup(); - menuGroup.setId(UUID.randomUUID()); - menuGroup.setName(name); + public MenuGroup create(final CreateMenuGroupCommand request) { + final Name name = this.nameFactory.create(request.name); + final MenuGroup menuGroup = new MenuGroup(UUID.randomUUID(), name); return menuGroupRepository.save(menuGroup); } diff --git a/src/main/java/kitchenpos/menu/application/MenuService.java b/src/main/java/kitchenpos/menu/application/MenuService.java index 76c0c1a26..c8c217d43 100644 --- a/src/main/java/kitchenpos/menu/application/MenuService.java +++ b/src/main/java/kitchenpos/menu/application/MenuService.java @@ -1,116 +1,94 @@ package kitchenpos.menu.application; -import java.math.BigDecimal; -import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; -import kitchenpos.common.profanity.domain.ProfanityDetectService; -import kitchenpos.menu.domain.Menu; -import kitchenpos.menu.domain.MenuGroup; -import kitchenpos.menu.domain.MenuGroupRepository; -import kitchenpos.menu.domain.MenuProduct; -import kitchenpos.menu.domain.MenuRepository; -import kitchenpos.product.domain.Product; -import kitchenpos.product.domain.ProductRepository; +import kitchenpos.common.name.Name; +import kitchenpos.common.name.NameFactory; +import kitchenpos.common.price.Price; +import kitchenpos.menu.tobe.application.dto.ChangeMenuPriceCommand; +import kitchenpos.menu.tobe.application.dto.CreateMenuCommand; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; +import kitchenpos.menu.tobe.domain.repository.MenuRepository; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import kitchenpos.product.tobe.domain.entity.Product; +import kitchenpos.product.tobe.domain.repository.ProductRepository; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class MenuService { + private final MenuRepository menuRepository; + private final MenuGroupRepository menuGroupRepository; + private final ProductRepository productRepository; - private final ProfanityDetectService profanityDetectService; + private final NameFactory nameFactory; + + @Autowired public MenuService( final MenuRepository menuRepository, final MenuGroupRepository menuGroupRepository, final ProductRepository productRepository, - final ProfanityDetectService profanityDetectService + final NameFactory nameFactory ) { this.menuRepository = menuRepository; this.menuGroupRepository = menuGroupRepository; this.productRepository = productRepository; - this.profanityDetectService = profanityDetectService; + this.nameFactory = nameFactory; } @Transactional - public Menu create(final Menu request) { - final BigDecimal price = request.getPrice(); - if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException(); - } - final MenuGroup menuGroup = menuGroupRepository.findById(request.getMenuGroupId()) + public Menu create(final CreateMenuCommand command) { + final Price price = new Price(command.price); + final MenuGroup menuGroup = menuGroupRepository.findById(command.menuGroupId) .orElseThrow(NoSuchElementException::new); - final List menuProductRequests = request.getMenuProducts(); + final List menuProductRequests = command.menuProducts; if (Objects.isNull(menuProductRequests) || menuProductRequests.isEmpty()) { throw new IllegalArgumentException(); } - final List products = productRepository.findAllByIdIn( + final Map productPrices = productRepository.findAllByIdIn( menuProductRequests.stream() - .map(MenuProduct::getProductId) - .collect(Collectors.toList()) - ); - if (products.size() != menuProductRequests.size()) { - throw new IllegalArgumentException(); - } - final List menuProducts = new ArrayList<>(); - BigDecimal sum = BigDecimal.ZERO; - for (final MenuProduct menuProductRequest : menuProductRequests) { - final long quantity = menuProductRequest.getQuantity(); - if (quantity < 0) { - throw new IllegalArgumentException(); - } - final Product product = productRepository.findById(menuProductRequest.getProductId()) - .orElseThrow(NoSuchElementException::new); - sum = sum.add( - product.getPrice() - .multiply(BigDecimal.valueOf(quantity)) - ); - final MenuProduct menuProduct = new MenuProduct(); - menuProduct.setProduct(product); - menuProduct.setQuantity(quantity); - menuProducts.add(menuProduct); - } - if (price.compareTo(sum) > 0) { - throw new IllegalArgumentException(); - } - final String name = request.getName(); - if (Objects.isNull(name) || profanityDetectService.profanityIn(name)) { + .map(menuProduct -> menuProduct.productId) + .collect(Collectors.toUnmodifiableList()) + ) + .stream() + .collect(Collectors.toMap(product -> product.id, Product::price)); + if (productPrices.size() != menuProductRequests.size()) { throw new IllegalArgumentException(); } - final Menu menu = new Menu(); - menu.setId(UUID.randomUUID()); - menu.setName(name); - menu.setPrice(price); - menu.setMenuGroup(menuGroup); - menu.setDisplayed(request.isDisplayed()); - menu.setMenuProducts(menuProducts); + final List menuProducts = menuProductRequests.stream() + .map(menuProduct -> new MenuProduct( + menuProduct.productId, + productPrices.get(menuProduct.productId), + menuProduct.quantity + )) + .collect(Collectors.toUnmodifiableList()); + final Name name = this.nameFactory.create(command.name); + final Menu menu = new Menu( + UUID.randomUUID(), + name, + command.displayed, + price, + menuGroup, + menuProducts + ); return menuRepository.save(menu); } @Transactional - public Menu changePrice(final UUID menuId, final Menu request) { - final BigDecimal price = request.getPrice(); - if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException(); - } - final Menu menu = menuRepository.findById(menuId) + public Menu changePrice(final ChangeMenuPriceCommand command) { + final Price price = new Price(command.price); + final Menu menu = menuRepository.findById(command.id) .orElseThrow(NoSuchElementException::new); - BigDecimal sum = BigDecimal.ZERO; - for (final MenuProduct menuProduct : menu.getMenuProducts()) { - sum = sum.add( - menuProduct.getProduct() - .getPrice() - .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) - ); - } - if (price.compareTo(sum) > 0) { - throw new IllegalArgumentException(); - } menu.setPrice(price); return menu; } @@ -119,18 +97,7 @@ public Menu changePrice(final UUID menuId, final Menu request) { public Menu display(final UUID menuId) { final Menu menu = menuRepository.findById(menuId) .orElseThrow(NoSuchElementException::new); - BigDecimal sum = BigDecimal.ZERO; - for (final MenuProduct menuProduct : menu.getMenuProducts()) { - sum = sum.add( - menuProduct.getProduct() - .getPrice() - .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) - ); - } - if (menu.getPrice().compareTo(sum) > 0) { - throw new IllegalStateException(); - } - menu.setDisplayed(true); + menu.display(); return menu; } @@ -138,7 +105,7 @@ public Menu display(final UUID menuId) { public Menu hide(final UUID menuId) { final Menu menu = menuRepository.findById(menuId) .orElseThrow(NoSuchElementException::new); - menu.setDisplayed(false); + menu.hide(); return menu; } diff --git a/src/main/java/kitchenpos/menu/domain/MenuGroupRepository.java b/src/main/java/kitchenpos/menu/domain/MenuGroupRepository.java deleted file mode 100644 index 02bdfce93..000000000 --- a/src/main/java/kitchenpos/menu/domain/MenuGroupRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package kitchenpos.menu.domain; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public interface MenuGroupRepository { - MenuGroup save(MenuGroup menuGroup); - - Optional findById(UUID id); - - List findAll(); -} - diff --git a/src/main/java/kitchenpos/menu/infra/JpaMenuGroupRepository.java b/src/main/java/kitchenpos/menu/infra/JpaMenuGroupRepository.java deleted file mode 100644 index 6ce73a878..000000000 --- a/src/main/java/kitchenpos/menu/infra/JpaMenuGroupRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package kitchenpos.menu.infra; - -import java.util.UUID; -import kitchenpos.menu.domain.MenuGroup; -import kitchenpos.menu.domain.MenuGroupRepository; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface JpaMenuGroupRepository extends MenuGroupRepository, JpaRepository { -} diff --git a/src/main/java/kitchenpos/menu/infra/JpaMenuRepository.java b/src/main/java/kitchenpos/menu/infra/JpaMenuRepository.java deleted file mode 100644 index d20335b2b..000000000 --- a/src/main/java/kitchenpos/menu/infra/JpaMenuRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package kitchenpos.menu.infra; - -import java.util.List; -import java.util.UUID; -import kitchenpos.menu.domain.Menu; -import kitchenpos.menu.domain.MenuRepository; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface JpaMenuRepository extends MenuRepository, JpaRepository { - @Query("select m from Menu m join m.menuProducts mp where mp.product.id = :productId") - @Override - List findAllByProductId(@Param("productId") UUID productId); -} diff --git a/src/main/java/kitchenpos/menu/tobe/application/dto/ChangeMenuPriceCommand.java b/src/main/java/kitchenpos/menu/tobe/application/dto/ChangeMenuPriceCommand.java new file mode 100644 index 000000000..ff70dcf8a --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/application/dto/ChangeMenuPriceCommand.java @@ -0,0 +1,16 @@ +package kitchenpos.menu.tobe.application.dto; + +import java.math.BigDecimal; +import java.util.UUID; + +public class ChangeMenuPriceCommand { + + public final UUID id; + + public final BigDecimal price; + + public ChangeMenuPriceCommand(UUID id, BigDecimal price) { + this.id = id; + this.price = price; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuCommand.java b/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuCommand.java new file mode 100644 index 000000000..61c3e76ec --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuCommand.java @@ -0,0 +1,33 @@ +package kitchenpos.menu.tobe.application.dto; + +import java.math.BigDecimal; +import java.util.List; +import java.util.UUID; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; + +public class CreateMenuCommand { + + public final String name; + + public final UUID menuGroupId; + + public final List menuProducts; + + public final boolean displayed; + + public final BigDecimal price; + + public CreateMenuCommand( + final String name, + final UUID menuGroupId, + final List menuProducts, + final boolean displayed, + final BigDecimal price + ) { + this.name = name; + this.menuGroupId = menuGroupId; + this.menuProducts = menuProducts; + this.displayed = displayed; + this.price = price; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuGroupCommand.java b/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuGroupCommand.java new file mode 100644 index 000000000..93eba2898 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/application/dto/CreateMenuGroupCommand.java @@ -0,0 +1,10 @@ +package kitchenpos.menu.tobe.application.dto; + +public class CreateMenuGroupCommand { + + public final String name; + + public CreateMenuGroupCommand(String name) { + this.name = name; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/entity/Menu.java b/src/main/java/kitchenpos/menu/tobe/domain/entity/Menu.java new file mode 100644 index 000000000..4d2cfd804 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/entity/Menu.java @@ -0,0 +1,73 @@ +package kitchenpos.menu.tobe.domain.entity; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import kitchenpos.common.name.Name; +import kitchenpos.common.price.Price; +import kitchenpos.menu.tobe.domain.service.MenuDisplayPolicy; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; + +public class Menu { + + public final UUID id; + + public final Name name; + + public final MenuGroup menuGroup; + + private final List menuProducts; + + private boolean displayed; + + private Price price; + + public Menu( + final UUID id, + final Name name, + final boolean displayed, + final Price price, + final MenuGroup menuGroup, + final List menuProducts + ) { + this.id = id; + this.name = name; + this.displayed = displayed; + this.price = price; + this.menuGroup = menuGroup; + this.menuProducts = menuProducts; + if (!MenuDisplayPolicy.isDisplayable(this)) { + throw new IllegalArgumentException("가격이 메뉴 노출 정책에 부합하지 않습니다"); + } + } + + public boolean displayed() { + return this.displayed; + } + + public Price price() { + return this.price; + } + + public List menuProducts() { + return Collections.unmodifiableList(this.menuProducts); + } + + public void display() { + if (!MenuDisplayPolicy.isDisplayable(this)) { + throw new IllegalStateException("메뉴 노출 정책에 따라 이 메뉴를 노출할 수 없습니다"); + } + this.displayed = true; + } + + public void hide() { + this.displayed = false; + } + + public void setPrice(final Price price) { + if (!MenuDisplayPolicy.isDisplayable(this, price)) { + throw new IllegalArgumentException("메뉴 노출 정책에 따라 가격을 변경할 수 없습니다"); + } + this.price = price; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/entity/MenuGroup.java b/src/main/java/kitchenpos/menu/tobe/domain/entity/MenuGroup.java new file mode 100644 index 000000000..c6bc40912 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/entity/MenuGroup.java @@ -0,0 +1,16 @@ +package kitchenpos.menu.tobe.domain.entity; + +import java.util.UUID; +import kitchenpos.common.name.Name; + +public class MenuGroup { + + public final UUID id; + + public final Name name; + + public MenuGroup(final UUID id, final Name name) { + this.id = id; + this.name = name; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuGroupRepository.java b/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuGroupRepository.java new file mode 100644 index 000000000..ececed87d --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuGroupRepository.java @@ -0,0 +1,15 @@ +package kitchenpos.menu.tobe.domain.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; + +public interface MenuGroupRepository { + + MenuGroup save(final MenuGroup menuGroup); + + Optional findById(final UUID id); + + List findAll(); +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuRepository.java b/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuRepository.java new file mode 100644 index 000000000..e73c7a233 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/repository/MenuRepository.java @@ -0,0 +1,19 @@ +package kitchenpos.menu.tobe.domain.repository; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import kitchenpos.menu.tobe.domain.entity.Menu; + +public interface MenuRepository { + + Menu save(final Menu menu); + + Optional findById(final UUID id); + + List findAll(); + + List findAllByIdIn(final List ids); + + List findAllByProductId(final UUID productId); +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicy.java b/src/main/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicy.java new file mode 100644 index 000000000..e4be16ed2 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicy.java @@ -0,0 +1,31 @@ +package kitchenpos.menu.tobe.domain.service; + + +import kitchenpos.common.price.Price; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import org.springframework.stereotype.Service; + +@Service +public class MenuDisplayPolicy { + + private MenuDisplayPolicy() { + } + + public static boolean isDisplayable(final Menu menu) { + final Price total = getTotalPrice(menu); + return menu.price().compareTo(total) <= 0; + } + + public static boolean isDisplayable(final Menu menu, final Price targetPrice) { + final Price total = getTotalPrice(menu); + return targetPrice.compareTo(total) <= 0; + } + + private static Price getTotalPrice(final Menu menu) { + return menu.menuProducts() + .stream() + .map(MenuProduct::subtotal) + .reduce(new Price(0), Price::add); + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProduct.java b/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProduct.java new file mode 100644 index 000000000..9d4b2710f --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProduct.java @@ -0,0 +1,55 @@ +package kitchenpos.menu.tobe.domain.vo; + +import java.util.UUID; +import kitchenpos.common.price.Price; + +public class MenuProduct { + + public final UUID productId; + + public final Price pricePerUnit; + + public final MenuProductQuantity quantity; + + public MenuProduct( + final UUID productId, + final Price pricePerUnit, + final MenuProductQuantity quantity + ) { + this.productId = productId; + this.pricePerUnit = pricePerUnit; + this.quantity = quantity; + } + + public Price subtotal() { + return this.pricePerUnit.multiply(this.quantity.value); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MenuProduct that = (MenuProduct) o; + + if (!productId.equals(that.productId)) { + return false; + } + if (!pricePerUnit.equals(that.pricePerUnit)) { + return false; + } + return quantity.equals(that.quantity); + } + + @Override + public int hashCode() { + int result = productId.hashCode(); + result = 31 * result + pricePerUnit.hashCode(); + result = 31 * result + quantity.hashCode(); + return result; + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantity.java b/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantity.java new file mode 100644 index 000000000..19e468140 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantity.java @@ -0,0 +1,38 @@ +package kitchenpos.menu.tobe.domain.vo; + +/** + *

    메뉴 상품 수량

    + *
      + *
    • 메뉴 상품별 수량은 1 이상이다.
    • + *
    + */ +public class MenuProductQuantity { + + public final long value; + + public MenuProductQuantity(final long value) { + if (value < 1) { + throw new IllegalArgumentException("MenuProductQuantity는 1보다 작을 수 없습니다"); + } + this.value = value; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + MenuProductQuantity that = (MenuProductQuantity) o; + + return value == that.value; + } + + @Override + public int hashCode() { + return (int) (value ^ (value >>> 32)); + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuDao.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuDao.java new file mode 100644 index 000000000..87995a1fe --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuDao.java @@ -0,0 +1,17 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface JpaMenuDao extends JpaRepository { + + @Query("select m from MenuEntity as m where m.id in :ids") + List findAllByIdIn(final List ids); + + @Query("select m from MenuEntity as m join MenuProductEntity as mp on m.id = mp.menuId where mp.productId = :productId") + List findAllByProductId(final UUID productId); +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupDao.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupDao.java new file mode 100644 index 000000000..15370a2b1 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupDao.java @@ -0,0 +1,9 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface JpaMenuGroupDao extends JpaRepository { +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupRepository.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupRepository.java new file mode 100644 index 000000000..2a4d3e6ee --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuGroupRepository.java @@ -0,0 +1,57 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; +import kitchenpos.menu.tobe.infra.jpa.MenuGroupEntityConverter.MenuGroupEntityToMenuGroupConverter; +import kitchenpos.menu.tobe.infra.jpa.MenuGroupEntityConverter.MenuGroupToMenuGroupEntityConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Repository; + +@Repository +public class JpaMenuGroupRepository implements MenuGroupRepository { + + private final JpaMenuGroupDao jpaMenuGroupDao; + + private final MenuGroupToMenuGroupEntityConverter menuGroupToMenuGroupEntityConverter; + + private final MenuGroupEntityToMenuGroupConverter menuGroupEntityToMenuGroupConverter; + + @Autowired + public JpaMenuGroupRepository( + final JpaMenuGroupDao jpaMenuGroupDao, + final MenuGroupToMenuGroupEntityConverter menuGroupToMenuGroupEntityConverter, + final MenuGroupEntityToMenuGroupConverter menuGroupEntityToMenuGroupConverter + ) { + this.jpaMenuGroupDao = jpaMenuGroupDao; + this.menuGroupToMenuGroupEntityConverter = menuGroupToMenuGroupEntityConverter; + this.menuGroupEntityToMenuGroupConverter = menuGroupEntityToMenuGroupConverter; + } + + @Override + public MenuGroup save(final MenuGroup menuGroup) { + final MenuGroupEntity menuGroupEntity = this.menuGroupToMenuGroupEntityConverter.convert(menuGroup); + final MenuGroupEntity result = this.jpaMenuGroupDao.save(menuGroupEntity); + return this.menuGroupEntityToMenuGroupConverter.convert(result); + } + + @Override + public Optional findById(final UUID id) { + final Optional result = this.jpaMenuGroupDao.findById(id); + if (result.isEmpty()) { + return Optional.empty(); + } + return Optional.of(this.menuGroupEntityToMenuGroupConverter.convert(result.get())); + } + + @Override + public List findAll() { + return this.jpaMenuGroupDao.findAll() + .stream() + .map(this.menuGroupEntityToMenuGroupConverter::convert) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuProductDao.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuProductDao.java new file mode 100644 index 000000000..2f87e4fbe --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuProductDao.java @@ -0,0 +1,17 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface JpaMenuProductDao extends JpaRepository { + + @Query("select mp from MenuProductEntity as mp where mp.menuId = :menuId") + List findAllByMenuId(final UUID menuId); + + @Query("delete from MenuProductEntity as mp where mp.menuId = :menuId") + void deleteByMenuId(final UUID menuId); +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuRepository.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuRepository.java new file mode 100644 index 000000000..d42ea088b --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/JpaMenuRepository.java @@ -0,0 +1,109 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.repository.MenuRepository; +import kitchenpos.menu.tobe.infra.jpa.MenuEntityConverter.MenuEntityToMenuConverter; +import kitchenpos.menu.tobe.infra.jpa.MenuEntityConverter.MenuToMenuEntityConverter; +import kitchenpos.menu.tobe.infra.jpa.MenuProductEntityConverter.MenuProductToMenuProductEntityConverter; +import org.springframework.stereotype.Repository; + +@Repository +public class JpaMenuRepository implements MenuRepository { + + private final JpaMenuDao jpaMenuDao; + + private final JpaMenuGroupDao jpaMenuGroupDao; + + private final JpaMenuProductDao jpaMenuProductDao; + + private final MenuToMenuEntityConverter menuToMenuEntityConverter; + + private final MenuEntityToMenuConverter menuEntityToMenuConverter; + + private final MenuProductToMenuProductEntityConverter menuProductToMenuProductEntityConverter; + + public JpaMenuRepository( + JpaMenuDao jpaMenuDao, + JpaMenuGroupDao jpaMenuGroupDao, + JpaMenuProductDao jpaMenuProductDao, + MenuToMenuEntityConverter menuToMenuEntityConverter, + MenuEntityToMenuConverter menuEntityToMenuConverter, + MenuProductToMenuProductEntityConverter menuProductToMenuProductEntityConverter + ) { + this.jpaMenuDao = jpaMenuDao; + this.jpaMenuGroupDao = jpaMenuGroupDao; + this.jpaMenuProductDao = jpaMenuProductDao; + this.menuToMenuEntityConverter = menuToMenuEntityConverter; + this.menuEntityToMenuConverter = menuEntityToMenuConverter; + this.menuProductToMenuProductEntityConverter = menuProductToMenuProductEntityConverter; + } + + @Override + public Menu save(final Menu menu) { + final MenuEntity menuEntity = this.menuToMenuEntityConverter.convert(menu); + final List menuProducts = this.menuProductToMenuProductEntityConverter.convert( + menu + ); + if (menuEntity.id != null) { + this.jpaMenuProductDao.deleteByMenuId(menu.id); + } + return this.menuEntityToMenuConverter.convert( + this.jpaMenuDao.save(menuEntity), + menu.menuGroup, + this.jpaMenuProductDao.saveAll(menuProducts) + ); + } + + @Override + public Optional findById(final UUID id) { + final MenuEntity menuEntity = this.jpaMenuDao.findById(id) + .orElse(null); + if (menuEntity == null) { + return Optional.empty(); + } + return Optional.of(this.getMenuFromMenuEntity(menuEntity)); + } + + @Override + public List findAll() { + return this.jpaMenuDao.findAll() + .stream() + .map(this::getMenuFromMenuEntity) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List findAllByIdIn(final List ids) { + return this.jpaMenuDao.findAllByIdIn(ids) + .stream() + .map(this::getMenuFromMenuEntity) + .collect(Collectors.toUnmodifiableList()); + } + + @Override + public List findAllByProductId(final UUID productId) { + return this.jpaMenuDao.findAllByProductId(productId) + .stream() + .map(this::getMenuFromMenuEntity) + .collect(Collectors.toUnmodifiableList()); + } + + private Menu getMenuFromMenuEntity(final MenuEntity menuEntity) { + final List menuProductEntities = this.jpaMenuProductDao.findAllByMenuId( + menuEntity.id + ); + final MenuGroupEntity menuGroupEntity = this.jpaMenuGroupDao.findById( + menuEntity.menuGroupId + ) + .orElseThrow(() -> new IllegalStateException("MenuGroup을 찾을 수 없습니다.")); + return this.menuEntityToMenuConverter.convert( + menuEntity, + menuGroupEntity, + menuProductEntities + ); + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntity.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntity.java new file mode 100644 index 000000000..d51694de9 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntity.java @@ -0,0 +1,32 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.math.BigDecimal; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Table(name = "menu") +@Entity +public class MenuEntity { + + @Column(name = "id", columnDefinition = "binary(16)") + @Id + public UUID id; + + @Column(name = "name", nullable = false) + public String name; + + @Column(name = "menu_group_id", columnDefinition = "binary(16)", nullable = false) + public UUID menuGroupId; + + @Column(name = "displayed", nullable = false) + public boolean displayed; + + @Column(name = "price", nullable = false) + public BigDecimal price; + + public MenuEntity() { + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntityConverter.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntityConverter.java new file mode 100644 index 000000000..3bea98523 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuEntityConverter.java @@ -0,0 +1,79 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.List; +import kitchenpos.common.name.NameFactory; +import kitchenpos.common.price.Price; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.infra.jpa.MenuGroupEntityConverter.MenuGroupEntityToMenuGroupConverter; +import kitchenpos.menu.tobe.infra.jpa.MenuProductEntityConverter.MenuProductEntityToMenuProductConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +public class MenuEntityConverter { + + private MenuEntityConverter() { + } + + @Component + public static class MenuToMenuEntityConverter { + + public MenuEntity convert(final Menu menu) { + final MenuEntity menuEntity = new MenuEntity(); + menuEntity.id = menu.id; + menuEntity.name = menu.name.value; + menuEntity.menuGroupId = menu.menuGroup.id; + menuEntity.displayed = menu.displayed(); + menuEntity.price = menu.price().value; + return menuEntity; + } + } + + @Component + public static class MenuEntityToMenuConverter { + + private final MenuGroupEntityToMenuGroupConverter menuGroupEntityToMenuGroupConverter; + + private final MenuProductEntityToMenuProductConverter menuProductEntityToMenuProductConverter; + + private final NameFactory nameFactory; + + @Autowired + public MenuEntityToMenuConverter( + final MenuGroupEntityToMenuGroupConverter menuGroupEntityToMenuGroupConverter, + final MenuProductEntityToMenuProductConverter menuProductEntityToMenuProductConverter, + final NameFactory nameFactory + ) { + this.menuGroupEntityToMenuGroupConverter = menuGroupEntityToMenuGroupConverter; + this.menuProductEntityToMenuProductConverter = menuProductEntityToMenuProductConverter; + this.nameFactory = nameFactory; + } + + public Menu convert( + final MenuEntity menu, + final MenuGroup menuGroup, + final List menuProducts + ) { + return new Menu( + menu.id, + this.nameFactory.create(menu.name), + menu.displayed, + new Price(menu.price), + menuGroup, + this.menuProductEntityToMenuProductConverter.convert(menuProducts) + ); + } + + public Menu convert( + final MenuEntity menu, + final MenuGroupEntity menuGroup, + final List menuProducts + ) { + return this.convert( + menu, + this.menuGroupEntityToMenuGroupConverter.convert(menuGroup), + menuProducts + ); + } + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntity.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntity.java new file mode 100644 index 000000000..9aff8e208 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntity.java @@ -0,0 +1,20 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +@Entity +public class MenuGroupEntity { + + @Column(name = "id", columnDefinition = "binary(16)") + @Id + public UUID id; + + @Column(name = "name", nullable = false) + public String name; + + public MenuGroupEntity() { + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntityConverter.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntityConverter.java new file mode 100644 index 000000000..0ce688219 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuGroupEntityConverter.java @@ -0,0 +1,41 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import kitchenpos.common.name.NameFactory; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +public class MenuGroupEntityConverter { + + private MenuGroupEntityConverter() { + } + + @Component + public static class MenuGroupToMenuGroupEntityConverter { + + public MenuGroupEntity convert(final MenuGroup source) { + final MenuGroupEntity menuGroupEntity = new MenuGroupEntity(); + menuGroupEntity.id = source.id; + menuGroupEntity.name = source.name.value; + return menuGroupEntity; + } + } + + @Component + public static class MenuGroupEntityToMenuGroupConverter { + + private final NameFactory nameFactory; + + @Autowired + public MenuGroupEntityToMenuGroupConverter(final NameFactory nameFactory) { + this.nameFactory = nameFactory; + } + + public MenuGroup convert(final MenuGroupEntity source) { + return new MenuGroup( + source.id, + this.nameFactory.create(source.name) + ); + } + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntity.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntity.java new file mode 100644 index 000000000..5898f8b26 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntity.java @@ -0,0 +1,35 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.math.BigDecimal; +import java.util.UUID; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +@Table(name = "menu_product") +@Entity +public class MenuProductEntity { + + @Column(name = "seq") + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Id + public Long seq; + + @Column(name = "menu_id", columnDefinition = "binary(16)", nullable = false) + public UUID menuId; + + @Column(name = "product_id", columnDefinition = "binary(16)", nullable = false) + public UUID productId; + + @Column(name = "price", nullable = false) + public BigDecimal price; + + @Column(name = "quantity", nullable = false) + public Long quantity; + + public MenuProductEntity() { + } +} diff --git a/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntityConverter.java b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntityConverter.java new file mode 100644 index 000000000..e4e7b8910 --- /dev/null +++ b/src/main/java/kitchenpos/menu/tobe/infra/jpa/MenuProductEntityConverter.java @@ -0,0 +1,57 @@ +package kitchenpos.menu.tobe.infra.jpa; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import kitchenpos.common.price.Price; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import kitchenpos.menu.tobe.domain.vo.MenuProductQuantity; +import org.springframework.stereotype.Component; + +public class MenuProductEntityConverter { + + private MenuProductEntityConverter() { + } + + @Component + public static class MenuProductToMenuProductEntityConverter { + + public MenuProductEntity convert(final MenuProduct menuProduct, final Menu menu) { + final MenuProductEntity menuProductEntity = new MenuProductEntity(); + menuProductEntity.menuId = menu.id; + menuProductEntity.productId = menuProduct.productId; + menuProductEntity.price = menuProduct.pricePerUnit.value; + menuProductEntity.quantity = menuProduct.quantity.value; + return menuProductEntity; + } + + public List convert(final List menuProducts, final Menu menu) { + return menuProducts.stream() + .map(menuProduct -> this.convert(menuProduct, menu)) + .collect(Collectors.toUnmodifiableList()); + } + + public List convert(final Menu menu) { + return this.convert(menu.menuProducts(), menu); + } + } + + @Component + public static class MenuProductEntityToMenuProductConverter { + + public MenuProduct convert(final MenuProductEntity menuProductEntity) { + return new MenuProduct( + menuProductEntity.productId, + new Price(menuProductEntity.price), + new MenuProductQuantity(menuProductEntity.quantity) + ); + } + + public List convert(final Collection menuProductEntities) { + return menuProductEntities.stream() + .map(this::convert) + .collect(Collectors.toUnmodifiableList()); + } + } +} diff --git a/src/main/java/kitchenpos/product/tobe/infra/jpa/ProductEntityConverter.java b/src/main/java/kitchenpos/product/tobe/infra/jpa/ProductEntityConverter.java index af366936e..0c979d6b7 100644 --- a/src/main/java/kitchenpos/product/tobe/infra/jpa/ProductEntityConverter.java +++ b/src/main/java/kitchenpos/product/tobe/infra/jpa/ProductEntityConverter.java @@ -3,7 +3,6 @@ import kitchenpos.common.name.NameFactory; import kitchenpos.common.price.Price; import kitchenpos.product.tobe.domain.entity.Product; -import kitchenpos.util.converter.Converter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -13,10 +12,8 @@ private ProductEntityConverter() { } @Component - public static class ProductToProductEntityConverter - implements Converter { + public static class ProductToProductEntityConverter { - @Override public ProductEntity convert(Product source) { final ProductEntity productEntity = new ProductEntity(); productEntity.id = source.id; @@ -27,8 +24,7 @@ public ProductEntity convert(Product source) { } @Component - public static class ProductEntityToProductConverter - implements Converter { + public static class ProductEntityToProductConverter { private final NameFactory nameFactory; @@ -37,7 +33,6 @@ public ProductEntityToProductConverter(NameFactory nameFactory) { this.nameFactory = nameFactory; } - @Override public Product convert(ProductEntity source) { return new Product( source.id, diff --git a/src/main/java/kitchenpos/util/converter/Converter.java b/src/main/java/kitchenpos/util/converter/Converter.java deleted file mode 100644 index 4ebb25159..000000000 --- a/src/main/java/kitchenpos/util/converter/Converter.java +++ /dev/null @@ -1,6 +0,0 @@ -package kitchenpos.util.converter; - -public interface Converter { - - D convert(final S source); -} diff --git a/src/main/resources/db/migration/V3__refactor_menu_product.sql b/src/main/resources/db/migration/V3__refactor_menu_product.sql new file mode 100644 index 000000000..b829e5d9e --- /dev/null +++ b/src/main/resources/db/migration/V3__refactor_menu_product.sql @@ -0,0 +1,2 @@ +alter table menu_product +add column price decimal(19, 2); diff --git a/src/test/java/kitchenpos/common/price/PriceTest.java b/src/test/java/kitchenpos/common/price/PriceTest.java index d63e04db3..21ec790a9 100644 --- a/src/test/java/kitchenpos/common/price/PriceTest.java +++ b/src/test/java/kitchenpos/common/price/PriceTest.java @@ -1,58 +1,117 @@ package kitchenpos.common.price; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNoException; +import java.math.BigDecimal; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; class PriceTest { - @DisplayName("Price는 0 이상의 정수일 수 있다.") - @ValueSource(longs = { - 0, 59031154, 1793947942, 40534364, 1832014351, - 629019133, 1313690456, 1777618747, 314068119, 256424390, - }) - @ParameterizedTest - void ncevwfii(final long price) { - assertThatNoException().isThrownBy(() -> new Price(price)); - } + @DisplayName("Constructor") + @Nested + class Edzphedq { - @DisplayName("Price는 0 이상의 실수일 수 있다.") - @ValueSource(doubles = { - 0.0, 3792.94, 6223.72, 7920.30, 300.13, - 363.69, 3078.50, 1312.45, 2145.62, 7028.71, - }) - @ParameterizedTest - void ccbiskwr(final double price) { - assertThatNoException().isThrownBy(() -> new Price(price)); - } + @DisplayName("Price는 0 이상의 정수일 수 있다.") + @ValueSource(longs = { + 0, 59031154, 1793947942, 40534364, 1832014351, + 629019133, 1313690456, 1777618747, 314068119, 256424390, + }) + @ParameterizedTest + void ncevwfii(final long price) { + assertThatNoException().isThrownBy(() -> new Price(price)); + } + + @DisplayName("Price는 0 이상의 실수일 수 있다.") + @ValueSource(doubles = { + 0.0, 3792.94, 6223.72, 7920.30, 300.13, + 363.69, 3078.50, 1312.45, 2145.62, 7028.71, + }) + @ParameterizedTest + void ccbiskwr(final double price) { + assertThatNoException().isThrownBy(() -> new Price(price)); + } + + @DisplayName("Price는 null일 수 없다.") + @Test + void fgktnvgm() { + assertThatIllegalArgumentException().isThrownBy(() -> new Price(null)); + } - @DisplayName("Price는 null일 수 없다.") - @Test - void fgktnvgm() { - assertThatIllegalArgumentException().isThrownBy(() -> new Price(null)); + @DisplayName("Price는 음의 정수일 수 없다.") + @ValueSource(longs = { + -137303079, -1004122297, -273047245, -1923277843, -507393273, + -849940561, -1049949352, -162065211, -585202570, -1898829030, + }) + @ParameterizedTest + void xjnyumit(final long price) { + assertThatIllegalArgumentException().isThrownBy(() -> new Price(price)); + } + + @DisplayName("Price는 음의 실수일 수 없다.") + @ValueSource(doubles = { + -7132.27, -576.64, -823.34, -5673.90, -3244.43, + -8709.46, -1575.65, -1861.43, -772.70, -8426.98, + }) + @ParameterizedTest + void xjnyumit(final double price) { + assertThatIllegalArgumentException().isThrownBy(() -> new Price(price)); + } } - @DisplayName("Price는 음의 정수일 수 없다.") - @ValueSource(longs = { - -137303079, -1004122297, -273047245, -1923277843, -507393273, - -849940561, -1049949352, -162065211, -585202570, -1898829030, - }) - @ParameterizedTest - void xjnyumit(final long price) { - assertThatIllegalArgumentException().isThrownBy(() -> new Price(price)); + @DisplayName("add()") + @Nested + class Sbzrrxpt { + + @DisplayName("더한 결과가 정확해야 한다.") + @ValueSource(doubles = { + 6371.44, 4658.70, 885.35, 331.00, 3989.34, + 758.08, 7988.55, 8136.68, 2548.28, 6164.34, + }) + @ParameterizedTest + void nzbkndwy(final double price) { + final Price initial = new Price(1000); + final Price augend = new Price(price); + + final Price actual = initial.add(augend); + + final BigDecimal expected = BigDecimal.valueOf(1000).add(BigDecimal.valueOf(price)); + + Assertions.assertAll( + () -> assertThat(actual.value).isEqualTo(expected), + () -> assertThat(actual).isEqualTo(new Price(expected)) + ); + } } - @DisplayName("Price는 음의 실수일 수 없다.") - @ValueSource(doubles = { - -7132.27, -576.64, -823.34, -5673.90, -3244.43, - -8709.46, -1575.65, -1861.43, -772.70, -8426.98, - }) - @ParameterizedTest - void xjnyumit(final double price) { - assertThatIllegalArgumentException().isThrownBy(() -> new Price(price)); + @DisplayName("multiply()") + @Nested + class Ducojvvs { + + @DisplayName("곱한 결과가 정확해야 한다.") + @ValueSource(doubles = { + 5389.35, 4783.53, 6250.83, 3838.19, 5885.34, + 4750.17, 9516.70, 9372.23, 7657.34, 5658.99, + }) + @ParameterizedTest + void njzemqit(final double price) { + final Price initial = new Price(price); + final BigDecimal multiplicand = BigDecimal.valueOf(2); + + final Price actual = initial.multiply(multiplicand); + + final BigDecimal expected = BigDecimal.valueOf(price).multiply(multiplicand); + + Assertions.assertAll( + () -> assertThat(actual.value).isEqualTo(expected), + () -> assertThat(actual).isEqualTo(new Price(expected)) + ); + } } } diff --git a/src/test/java/kitchenpos/menu/Fixtures.java b/src/test/java/kitchenpos/menu/Fixtures.java new file mode 100644 index 000000000..9f1b9052b --- /dev/null +++ b/src/test/java/kitchenpos/menu/Fixtures.java @@ -0,0 +1,81 @@ +package kitchenpos.menu; + +import java.util.Arrays; +import java.util.Random; +import java.util.UUID; +import kitchenpos.common.name.NameFactory; +import kitchenpos.common.price.Price; +import kitchenpos.common.profanity.FakeProfanityDetectService; +import kitchenpos.common.profanity.domain.ProfanityDetectService; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import kitchenpos.menu.tobe.domain.vo.MenuProductQuantity; +import kitchenpos.product.tobe.domain.entity.Product; + +public class Fixtures { + + private static final ProfanityDetectService profanityDetectService = new FakeProfanityDetectService(); + + private static final NameFactory nameFactory = new NameFactory(profanityDetectService); + + public static Menu menu( + final long price, + final boolean displayed, + final MenuProduct... menuProducts + ) { + return new Menu( + UUID.randomUUID(), + nameFactory.create("후라이드+후라이드"), + displayed, + new Price(price), + menuGroup(), + Arrays.asList(menuProducts) + ); + } + + public static Menu menu() { + return menu(19_000L, true, menuProduct()); + } + + public static Menu menu(final long price, final MenuProduct... menuProducts) { + return menu(price, false, menuProducts); + } + + public static MenuGroup menuGroup() { + return menuGroup("두마리메뉴"); + } + + public static MenuGroup menuGroup(final String name) { + return new MenuGroup(UUID.randomUUID(), nameFactory.create(name)); + } + + public static MenuProduct menuProduct(final Price price, final MenuProductQuantity quantity) { + return new MenuProduct( + UUID.randomUUID(), + price, + quantity + ); + } + + public static MenuProduct menuProduct(final long price, final long quantity) { + return menuProduct( + new Price(price), + new MenuProductQuantity(quantity) + ); + } + + public static MenuProduct menuProduct() { + return menuProduct( + new Price(10_000), + new MenuProductQuantity(new Random().nextInt(10)) + ); + } + + public static MenuProduct menuProduct(final Product product, final long quantity) { + return menuProduct( + product.price(), + new MenuProductQuantity(quantity) + ); + } +} diff --git a/src/test/java/kitchenpos/menu/InMemoryMenuGroupRepository.java b/src/test/java/kitchenpos/menu/InMemoryMenuGroupRepository.java index 3649ff430..0f621b8f2 100644 --- a/src/test/java/kitchenpos/menu/InMemoryMenuGroupRepository.java +++ b/src/test/java/kitchenpos/menu/InMemoryMenuGroupRepository.java @@ -6,15 +6,16 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; -import kitchenpos.menu.domain.MenuGroup; -import kitchenpos.menu.domain.MenuGroupRepository; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; public class InMemoryMenuGroupRepository implements MenuGroupRepository { + private final Map menuGroups = new HashMap<>(); @Override public MenuGroup save(final MenuGroup menuGroup) { - menuGroups.put(menuGroup.getId(), menuGroup); + menuGroups.put(menuGroup.id, menuGroup); return menuGroup; } diff --git a/src/test/java/kitchenpos/menu/InMemoryMenuRepository.java b/src/test/java/kitchenpos/menu/InMemoryMenuRepository.java index 5e7b89f20..25dd82297 100644 --- a/src/test/java/kitchenpos/menu/InMemoryMenuRepository.java +++ b/src/test/java/kitchenpos/menu/InMemoryMenuRepository.java @@ -7,15 +7,16 @@ import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; -import kitchenpos.menu.domain.Menu; -import kitchenpos.menu.domain.MenuRepository; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.repository.MenuRepository; public class InMemoryMenuRepository implements MenuRepository { + private final Map menus = new HashMap<>(); @Override public Menu save(final Menu menu) { - menus.put(menu.getId(), menu); + menus.put(menu.id, menu); return menu; } @@ -33,7 +34,7 @@ public List findAll() { public List findAllByIdIn(final List ids) { return menus.values() .stream() - .filter(menu -> ids.contains(menu.getId())) + .filter(menu -> ids.contains(menu.id)) .collect(Collectors.toList()); } @@ -41,7 +42,10 @@ public List findAllByIdIn(final List ids) { public List findAllByProductId(final UUID productId) { return menus.values() .stream() - .filter(menu -> menu.getMenuProducts().stream().anyMatch(menuProduct -> menuProduct.getProduct().getId().equals(productId))) + .filter(menu -> menu.menuProducts() + .stream() + .anyMatch(menuProduct -> menuProduct.productId.equals(productId)) + ) .collect(Collectors.toList()); } } diff --git a/src/test/java/kitchenpos/menu/application/MenuGroupServiceTest.java b/src/test/java/kitchenpos/menu/application/MenuGroupServiceTest.java index adc932e24..0894b6cdb 100644 --- a/src/test/java/kitchenpos/menu/application/MenuGroupServiceTest.java +++ b/src/test/java/kitchenpos/menu/application/MenuGroupServiceTest.java @@ -1,51 +1,47 @@ package kitchenpos.menu.application; -import static kitchenpos.Fixtures.menuGroup; +import static kitchenpos.menu.Fixtures.menuGroup; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; import java.util.List; +import kitchenpos.common.name.NameFactory; +import kitchenpos.common.profanity.FakeProfanityDetectService; +import kitchenpos.common.profanity.domain.ProfanityDetectService; import kitchenpos.menu.InMemoryMenuGroupRepository; -import kitchenpos.menu.domain.MenuGroup; -import kitchenpos.menu.domain.MenuGroupRepository; +import kitchenpos.menu.tobe.application.dto.CreateMenuGroupCommand; +import kitchenpos.menu.tobe.domain.entity.MenuGroup; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; class MenuGroupServiceTest { + private MenuGroupRepository menuGroupRepository; + private MenuGroupService menuGroupService; @BeforeEach void setUp() { menuGroupRepository = new InMemoryMenuGroupRepository(); - menuGroupService = new MenuGroupService(menuGroupRepository); + final ProfanityDetectService profanityDetectService = new FakeProfanityDetectService(); + final NameFactory nameFactory = new NameFactory(profanityDetectService); + menuGroupService = new MenuGroupService(menuGroupRepository, nameFactory); } @DisplayName("메뉴 그룹을 등록할 수 있다.") @Test void create() { - final MenuGroup expected = createMenuGroupRequest("두마리메뉴"); - final MenuGroup actual = menuGroupService.create(expected); + final CreateMenuGroupCommand command = createMenuGroupCommand("두마리메뉴"); + final MenuGroup actual = menuGroupService.create(command); assertThat(actual).isNotNull(); assertAll( - () -> assertThat(actual.getId()).isNotNull(), - () -> assertThat(actual.getName()).isEqualTo(expected.getName()) + () -> assertThat(actual.id).isNotNull(), + () -> assertThat(actual.name.value).isEqualTo(command.name) ); } - @DisplayName("메뉴 그룹의 이름이 올바르지 않으면 등록할 수 없다.") - @NullAndEmptySource - @ParameterizedTest - void create(final String name) { - final MenuGroup expected = createMenuGroupRequest(name); - assertThatThrownBy(() -> menuGroupService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - @DisplayName("메뉴 그룹의 목록을 조회할 수 있다.") @Test void findAll() { @@ -54,9 +50,7 @@ void findAll() { assertThat(actual).hasSize(1); } - private MenuGroup createMenuGroupRequest(final String name) { - final MenuGroup menuGroup = new MenuGroup(); - menuGroup.setName(name); - return menuGroup; + private CreateMenuGroupCommand createMenuGroupCommand(final String name) { + return new CreateMenuGroupCommand(name); } } diff --git a/src/test/java/kitchenpos/menu/application/MenuServiceTest.java b/src/test/java/kitchenpos/menu/application/MenuServiceTest.java index e9bfe94e1..65e18586e 100644 --- a/src/test/java/kitchenpos/menu/application/MenuServiceTest.java +++ b/src/test/java/kitchenpos/menu/application/MenuServiceTest.java @@ -1,10 +1,10 @@ package kitchenpos.menu.application; import static kitchenpos.Fixtures.INVALID_ID; -import static kitchenpos.Fixtures.menu; -import static kitchenpos.Fixtures.menuGroup; -import static kitchenpos.Fixtures.menuProduct; -import static kitchenpos.Fixtures.product; +import static kitchenpos.menu.Fixtures.menu; +import static kitchenpos.menu.Fixtures.menuGroup; +import static kitchenpos.menu.Fixtures.menuProduct; +import static kitchenpos.product.Fixtures.product; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; @@ -15,70 +15,77 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.UUID; +import kitchenpos.common.name.NameFactory; +import kitchenpos.common.price.Price; import kitchenpos.common.profanity.FakeProfanityDetectService; import kitchenpos.common.profanity.domain.ProfanityDetectService; import kitchenpos.menu.InMemoryMenuGroupRepository; import kitchenpos.menu.InMemoryMenuRepository; -import kitchenpos.menu.domain.Menu; -import kitchenpos.menu.domain.MenuGroupRepository; -import kitchenpos.menu.domain.MenuProduct; -import kitchenpos.menu.domain.MenuRepository; +import kitchenpos.menu.tobe.application.dto.ChangeMenuPriceCommand; +import kitchenpos.menu.tobe.application.dto.CreateMenuCommand; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.repository.MenuGroupRepository; +import kitchenpos.menu.tobe.domain.repository.MenuRepository; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import kitchenpos.menu.tobe.domain.vo.MenuProductQuantity; import kitchenpos.product.InMemoryProductRepository; -import kitchenpos.product.domain.Product; -import kitchenpos.product.domain.ProductRepository; +import kitchenpos.product.tobe.domain.entity.Product; +import kitchenpos.product.tobe.domain.repository.ProductRepository; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; class MenuServiceTest { + private MenuRepository menuRepository; - private MenuGroupRepository menuGroupRepository; - private ProductRepository productRepository; - private ProfanityDetectService profanityDetectService; + private MenuService menuService; + private UUID menuGroupId; + private Product product; @BeforeEach void setUp() { menuRepository = new InMemoryMenuRepository(); - menuGroupRepository = new InMemoryMenuGroupRepository(); - productRepository = new InMemoryProductRepository(); - profanityDetectService = new FakeProfanityDetectService(); + final MenuGroupRepository menuGroupRepository = new InMemoryMenuGroupRepository(); + final ProductRepository productRepository = new InMemoryProductRepository(); + final ProfanityDetectService profanityDetectService = new FakeProfanityDetectService(); + final NameFactory nameFactory = new NameFactory(profanityDetectService); menuService = new MenuService( menuRepository, menuGroupRepository, productRepository, - profanityDetectService + nameFactory ); - menuGroupId = menuGroupRepository.save(menuGroup()).getId(); + menuGroupId = menuGroupRepository.save(menuGroup()).id; product = productRepository.save(product("후라이드", 16_000L)); } @DisplayName("1개 이상의 등록된 상품으로 메뉴를 등록할 수 있다.") @Test void create() { - final Menu expected = createMenuRequest( + final CreateMenuCommand command = createMenuCommand( "후라이드+후라이드", 19_000L, menuGroupId, true, - createMenuProductRequest(product.getId(), 2L) + createMenuProductRequest(product.id, 2L) ); - final Menu actual = menuService.create(expected); + final Menu actual = menuService.create(command); assertThat(actual).isNotNull(); assertAll( - () -> assertThat(actual.getId()).isNotNull(), - () -> assertThat(actual.getName()).isEqualTo(expected.getName()), - () -> assertThat(actual.getPrice()).isEqualTo(expected.getPrice()), - () -> assertThat(actual.getMenuGroup().getId()).isEqualTo(expected.getMenuGroupId()), - () -> assertThat(actual.isDisplayed()).isEqualTo(expected.isDisplayed()), - () -> assertThat(actual.getMenuProducts()).hasSize(1) + () -> assertThat(actual.id).isNotNull(), + () -> assertThat(actual.name.value).isEqualTo(command.name), + () -> assertThat(actual.price().value).isEqualTo(command.price), + () -> assertThat(actual.menuGroup.id).isEqualTo(command.menuGroupId), + () -> assertThat(actual.displayed()).isEqualTo(command.displayed), + () -> assertThat(actual.menuProducts()).hasSize(1) ); } @@ -86,14 +93,14 @@ void create() { @MethodSource("menuProducts") @ParameterizedTest void create(final List menuProducts) { - final Menu expected = createMenuRequest( + final CreateMenuCommand command = createMenuCommand( "후라이드+후라이드", 19_000L, menuGroupId, true, menuProducts ); - assertThatThrownBy(() -> menuService.create(expected)) + assertThatThrownBy(() -> menuService.create(command)) .isInstanceOf(IllegalArgumentException.class); } @@ -105,47 +112,17 @@ private static List menuProducts() { ); } - @DisplayName("메뉴에 속한 상품의 수량은 0개 이상이어야 한다.") - @Test - void createNegativeQuantity() { - final Menu expected = createMenuRequest( - "후라이드+후라이드", - 19_000L, - menuGroupId, - true, - createMenuProductRequest(product.getId(), -1L) - ); - assertThatThrownBy(() -> menuService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("메뉴의 가격이 올바르지 않으면 등록할 수 없다.") - @ValueSource(strings = "-1000") - @NullSource - @ParameterizedTest - void create(final BigDecimal price) { - final Menu expected = createMenuRequest( - "후라이드+후라이드", - price, - menuGroupId, - true, - createMenuProductRequest(product.getId(), 2L) - ); - assertThatThrownBy(() -> menuService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - @DisplayName("메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다.") @Test void createExpensiveMenu() { - final Menu expected = createMenuRequest( + final CreateMenuCommand command = createMenuCommand( "후라이드+후라이드", 33_000L, menuGroupId, true, - createMenuProductRequest(product.getId(), 2L) + createMenuProductRequest(product.id, 2L) ); - assertThatThrownBy(() -> menuService.create(expected)) + assertThatThrownBy(() -> menuService.create(command)) .isInstanceOf(IllegalArgumentException.class); } @@ -153,55 +130,26 @@ void createExpensiveMenu() { @NullSource @ParameterizedTest void create(final UUID menuGroupId) { - final Menu expected = createMenuRequest( + final CreateMenuCommand command = createMenuCommand( "후라이드+후라이드", 19_000L, menuGroupId, true, - createMenuProductRequest(product.getId(), 2L) + createMenuProductRequest(product.id, 2L) ); - assertThatThrownBy(() -> menuService.create(expected)) + assertThatThrownBy(() -> menuService.create(command)) .isInstanceOf(NoSuchElementException.class); } - @DisplayName("메뉴의 이름이 올바르지 않으면 등록할 수 없다.") - @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) - @NullSource - @ParameterizedTest - void create(final String name) { - final Menu expected = createMenuRequest( - name, - 19_000L, - menuGroupId, - true, - createMenuProductRequest(product.getId(), 2L) - ); - assertThatThrownBy(() -> menuService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - @DisplayName("메뉴의 가격을 변경할 수 있다.") @Test void changePrice() { final UUID menuId = menuRepository.save( menu(19_000L, menuProduct(product, 2L)) - ).getId(); - final Menu expected = changePriceRequest(16_000L); - final Menu actual = menuService.changePrice(menuId, expected); - assertThat(actual.getPrice()).isEqualTo(expected.getPrice()); - } - - @DisplayName("메뉴의 가격이 올바르지 않으면 변경할 수 없다.") - @ValueSource(strings = "-1000") - @NullSource - @ParameterizedTest - void changePrice(final BigDecimal price) { - final UUID menuId = menuRepository.save( - menu(19_000L, menuProduct(product, 2L)) - ).getId(); - final Menu expected = changePriceRequest(price); - assertThatThrownBy(() -> menuService.changePrice(menuId, expected)) - .isInstanceOf(IllegalArgumentException.class); + ).id; + final ChangeMenuPriceCommand command = changeMenuPriceCommand(menuId, 16_000L); + final Menu actual = menuService.changePrice(command); + assertThat(actual.price().value).isEqualTo(command.price); } @DisplayName("메뉴에 속한 상품 금액의 합은 메뉴의 가격보다 크거나 같아야 한다.") @@ -209,9 +157,9 @@ void changePrice(final BigDecimal price) { void changePriceToExpensive() { final UUID menuId = menuRepository.save( menu(19_000L, menuProduct(product, 2L)) - ).getId(); - final Menu expected = changePriceRequest(33_000L); - assertThatThrownBy(() -> menuService.changePrice(menuId, expected)) + ).id; + final ChangeMenuPriceCommand command = changeMenuPriceCommand(menuId, 33_000L); + assertThatThrownBy(() -> menuService.changePrice(command)) .isInstanceOf(IllegalArgumentException.class); } @@ -220,17 +168,18 @@ void changePriceToExpensive() { void display() { final UUID menuId = menuRepository.save( menu(19_000L, false, menuProduct(product, 2L)) - ).getId(); + ).id; final Menu actual = menuService.display(menuId); - assertThat(actual.isDisplayed()).isTrue(); + assertThat(actual.displayed()).isTrue(); } + @Disabled @DisplayName("메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 높을 경우 메뉴를 노출할 수 없다.") @Test void displayExpensiveMenu() { final UUID menuId = menuRepository.save( menu(33_000L, false, menuProduct(product, 2L)) - ).getId(); + ).id; assertThatThrownBy(() -> menuService.display(menuId)) .isInstanceOf(IllegalStateException.class); } @@ -240,9 +189,9 @@ void displayExpensiveMenu() { void hide() { final UUID menuId = menuRepository.save( menu(19_000L, true, menuProduct(product, 2L)) - ).getId(); + ).id; final Menu actual = menuService.hide(menuId); - assertThat(actual.isDisplayed()).isFalse(); + assertThat(actual.displayed()).isFalse(); } @DisplayName("메뉴의 목록을 조회할 수 있다.") @@ -253,14 +202,14 @@ void findAll() { assertThat(actual).hasSize(1); } - private Menu createMenuRequest( + private CreateMenuCommand createMenuCommand( final String name, final long price, final UUID menuGroupId, final boolean displayed, final MenuProduct... menuProducts ) { - return createMenuRequest( + return createMenuCommand( name, BigDecimal.valueOf(price), menuGroupId, @@ -269,24 +218,24 @@ private Menu createMenuRequest( ); } - private Menu createMenuRequest( + private CreateMenuCommand createMenuCommand( final String name, final BigDecimal price, final UUID menuGroupId, final boolean displayed, final MenuProduct... menuProducts ) { - return createMenuRequest(name, price, menuGroupId, displayed, Arrays.asList(menuProducts)); + return createMenuCommand(name, price, menuGroupId, displayed, Arrays.asList(menuProducts)); } - private Menu createMenuRequest( + private CreateMenuCommand createMenuCommand( final String name, final long price, final UUID menuGroupId, final boolean displayed, final List menuProducts ) { - return createMenuRequest( + return createMenuCommand( name, BigDecimal.valueOf(price), menuGroupId, @@ -295,36 +244,44 @@ private Menu createMenuRequest( ); } - private Menu createMenuRequest( + private CreateMenuCommand createMenuCommand( final String name, final BigDecimal price, final UUID menuGroupId, final boolean displayed, final List menuProducts ) { - final Menu menu = new Menu(); - menu.setName(name); - menu.setPrice(price); - menu.setMenuGroupId(menuGroupId); - menu.setDisplayed(displayed); - menu.setMenuProducts(menuProducts); - return menu; + return new CreateMenuCommand( + name, + menuGroupId, + menuProducts, + displayed, + price + ); } private static MenuProduct createMenuProductRequest(final UUID productId, final long quantity) { - final MenuProduct menuProduct = new MenuProduct(); - menuProduct.setProductId(productId); - menuProduct.setQuantity(quantity); - return menuProduct; + return new MenuProduct( + productId, + new Price(10000), + new MenuProductQuantity(quantity) + ); } - private Menu changePriceRequest(final long price) { - return changePriceRequest(BigDecimal.valueOf(price)); + private ChangeMenuPriceCommand changeMenuPriceCommand( + final UUID menuId, + final BigDecimal price + ) { + return new ChangeMenuPriceCommand( + menuId, + price + ); } - private Menu changePriceRequest(final BigDecimal price) { - final Menu menu = new Menu(); - menu.setPrice(price); - return menu; + private ChangeMenuPriceCommand changeMenuPriceCommand( + final UUID menuId, + final long price + ) { + return this.changeMenuPriceCommand(menuId, BigDecimal.valueOf(price)); } } diff --git a/src/test/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicyTest.java b/src/test/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicyTest.java new file mode 100644 index 000000000..2260a2d59 --- /dev/null +++ b/src/test/java/kitchenpos/menu/tobe/domain/service/MenuDisplayPolicyTest.java @@ -0,0 +1,60 @@ +package kitchenpos.menu.tobe.domain.service; + +import static kitchenpos.menu.Fixtures.menu; +import static kitchenpos.menu.Fixtures.menuProduct; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import kitchenpos.menu.tobe.domain.entity.Menu; +import kitchenpos.menu.tobe.domain.vo.MenuProduct; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class MenuDisplayPolicyTest { + + @Disabled + @DisplayName("메뉴의 가격이 포함된 상품 가격 합보다 크면 메뉴를 노출할 수 없다.") + @Test + void azprdbfl() { + final MenuProduct menuProduct1 = menuProduct(10000, 1); + final MenuProduct menuProduct2 = menuProduct(20000, 2); + + final List menus = new Random().longs(10, 1, 50000) + .map(price -> price + 50000) + .mapToObj(price -> menu(price, menuProduct1, menuProduct2)) + .collect(Collectors.toUnmodifiableList()); + + assertThat(menus) + .filteredOn(MenuDisplayPolicy::isDisplayable) + .isEmpty(); + } + + @DisplayName("메뉴의 가격이 포함된 상품 가격 합보다 같으면 메뉴를 노출할 수 있다.") + @Test + void eyergzia() { + final MenuProduct menuProduct = menuProduct(10000, 1); + final MenuProduct menuProduct2 = menuProduct(20000, 2); + + final Menu menu = menu(50000, menuProduct, menuProduct2); + + assertThat(MenuDisplayPolicy.isDisplayable(menu)).isTrue(); + } + + @DisplayName("메뉴의 가격이 포함된 상품 가격 합보다 작으면 메뉴를 노출할 수 있다.") + @Test + void jvceaber() { + final MenuProduct menuProduct1 = menuProduct(10000, 1); + final MenuProduct menuProduct2 = menuProduct(20000, 2); + + final List menus = new Random().longs(10, 0, 50000) + .mapToObj(price -> menu(price, menuProduct1, menuProduct2)) + .collect(Collectors.toUnmodifiableList()); + + assertThat(menus) + .filteredOn((menu) -> !MenuDisplayPolicy.isDisplayable(menu)) + .isEmpty(); + } +} diff --git a/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantityTest.java b/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantityTest.java new file mode 100644 index 000000000..6519e581b --- /dev/null +++ b/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductQuantityTest.java @@ -0,0 +1,32 @@ +package kitchenpos.menu.tobe.domain.vo; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MenuProductQuantityTest { + + @DisplayName("MenuProductQuantity는 1 이상의 정수일 수 있다.") + @ValueSource(longs = { + 1, 395848125, 1252547819, 491598008, 1632439888, + 1351529542, 192099596, 1574050269, 208629295, 330560803, + }) + @ParameterizedTest + void yvvbgqpw(final long menuProductQuantity) { + assertThatNoException().isThrownBy(() -> new MenuProductQuantity(menuProductQuantity)); + } + + @DisplayName("MenuProductQuantity는 1 미만의 정수일 수 없다.") + @ValueSource(longs = { + 0, -384956592, -265755600, -583220825, -1589482440, + -718419631, -163086986, -1222040361, -498175820, -816502739, + }) + @ParameterizedTest + void dhdrhvkk(final long menuProductQuantity) { + assertThatIllegalArgumentException().isThrownBy(() -> + new MenuProductQuantity(menuProductQuantity)); + } +} diff --git a/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductTest.java b/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductTest.java new file mode 100644 index 000000000..0831be506 --- /dev/null +++ b/src/test/java/kitchenpos/menu/tobe/domain/vo/MenuProductTest.java @@ -0,0 +1,34 @@ +package kitchenpos.menu.tobe.domain.vo; + +import static kitchenpos.menu.Fixtures.menuProduct; +import static kitchenpos.product.Fixtures.product; +import static org.assertj.core.api.Assertions.assertThat; + +import kitchenpos.common.price.Price; +import kitchenpos.product.tobe.domain.entity.Product; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class MenuProductTest { + + @DisplayName("subtotal()") + @Nested + class Cvxueewc { + + @DisplayName("소계가 올바르게 계산되어야 한다.") + @ValueSource(longs = { + 16, 8, 32, 4, 1, + 15, 13, 3, 31, 18, + }) + @ParameterizedTest + void czxovwxu(final long quantity) { + final long pricePerUnit = 10000; + final Product product = product("상품", pricePerUnit); + final MenuProduct menuProduct = menuProduct(product, quantity); + + assertThat(menuProduct.subtotal()).isEqualTo(new Price(pricePerUnit * quantity)); + } + } +}