diff --git a/README.md b/README.md index 4a91e3f71..2439978f4 100644 --- a/README.md +++ b/README.md @@ -199,3 +199,13 @@ docker compose -p kitchenpos up -d - `Order`는 접수 대기 ➜ 접수 ➜ 서빙 ➜ 계산 완료 순서로 진행된다. - `OrderLineItem`는 가격과 수량을 가진다. - `OrderLineItem`의 수량은 1보다 커야 한다. + + +#### step1 요구사항 + +- 키친포스의 요구 사항과 용어 사전, 모델링을 기반으로 상품 CONTEXT를 리팩터링한다. +- 상품 CONTEXT의 도메인 계층만 먼저 구현한다. +- products 패키지 밑에 tobe.domain 패키지를 만들고 거기서부터 구현을 시작한다. +- 용어 사전과 모델링이 부자연스럽거나 불완전하거나 잘못된 경우 지속적으로 수정한다. +- 새로운 모델에 맞게끔 클래스, 메서드, 모듈의 이름을 다시 지으면서 코드를 리팩터링한다. +- REPOSITORY 구현 시 자신에게 익숙하고 편한 것을 선택하여 진행한다. diff --git a/build.gradle.kts b/build.gradle.kts index 67ab16154..bace56f3b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,7 @@ group = "camp.nextstep.edu" version = "0.0.1-SNAPSHOT" java { - sourceCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_17 } repositories { diff --git a/src/main/java/kitchenpos/menus/application/MenuService.java b/src/main/java/kitchenpos/menus/application/MenuService.java index abefa5bcf..88ef31359 100644 --- a/src/main/java/kitchenpos/menus/application/MenuService.java +++ b/src/main/java/kitchenpos/menus/application/MenuService.java @@ -5,9 +5,9 @@ import kitchenpos.menus.domain.MenuGroupRepository; import kitchenpos.menus.domain.MenuProduct; import kitchenpos.menus.domain.MenuRepository; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.asis.domain.ProductRepository; +import kitchenpos.products.asis.infra.PurgomalumClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/kitchenpos/menus/domain/MenuProduct.java b/src/main/java/kitchenpos/menus/domain/MenuProduct.java index b47ca26cb..14addb1fb 100644 --- a/src/main/java/kitchenpos/menus/domain/MenuProduct.java +++ b/src/main/java/kitchenpos/menus/domain/MenuProduct.java @@ -10,7 +10,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; -import kitchenpos.products.domain.Product; +import kitchenpos.products.asis.domain.Product; import java.util.UUID; diff --git a/src/main/java/kitchenpos/products/application/ProductService.java b/src/main/java/kitchenpos/products/asis/application/ProductService.java similarity index 92% rename from src/main/java/kitchenpos/products/application/ProductService.java rename to src/main/java/kitchenpos/products/asis/application/ProductService.java index 20cf63996..a4edbe9d4 100644 --- a/src/main/java/kitchenpos/products/application/ProductService.java +++ b/src/main/java/kitchenpos/products/asis/application/ProductService.java @@ -1,11 +1,11 @@ -package kitchenpos.products.application; +package kitchenpos.products.asis.application; import kitchenpos.menus.domain.Menu; import kitchenpos.menus.domain.MenuProduct; import kitchenpos.menus.domain.MenuRepository; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.asis.domain.ProductRepository; +import kitchenpos.products.asis.infra.PurgomalumClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/kitchenpos/products/domain/JpaProductRepository.java b/src/main/java/kitchenpos/products/asis/domain/JpaProductRepository.java similarity index 81% rename from src/main/java/kitchenpos/products/domain/JpaProductRepository.java rename to src/main/java/kitchenpos/products/asis/domain/JpaProductRepository.java index 90b069779..5faa9c9c4 100644 --- a/src/main/java/kitchenpos/products/domain/JpaProductRepository.java +++ b/src/main/java/kitchenpos/products/asis/domain/JpaProductRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.products.asis.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/kitchenpos/products/domain/Product.java b/src/main/java/kitchenpos/products/asis/domain/Product.java similarity index 95% rename from src/main/java/kitchenpos/products/domain/Product.java rename to src/main/java/kitchenpos/products/asis/domain/Product.java index ee2a7dfa9..0cdf3eb11 100644 --- a/src/main/java/kitchenpos/products/domain/Product.java +++ b/src/main/java/kitchenpos/products/asis/domain/Product.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.products.asis.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/kitchenpos/products/domain/ProductRepository.java b/src/main/java/kitchenpos/products/asis/domain/ProductRepository.java similarity index 86% rename from src/main/java/kitchenpos/products/domain/ProductRepository.java rename to src/main/java/kitchenpos/products/asis/domain/ProductRepository.java index 3637e4232..074dcfe8e 100644 --- a/src/main/java/kitchenpos/products/domain/ProductRepository.java +++ b/src/main/java/kitchenpos/products/asis/domain/ProductRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.products.asis.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java b/src/main/java/kitchenpos/products/asis/infra/DefaultPurgomalumClient.java similarity index 95% rename from src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java rename to src/main/java/kitchenpos/products/asis/infra/DefaultPurgomalumClient.java index 87dba885c..6c9afdeba 100644 --- a/src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java +++ b/src/main/java/kitchenpos/products/asis/infra/DefaultPurgomalumClient.java @@ -1,4 +1,4 @@ -package kitchenpos.products.infra; +package kitchenpos.products.asis.infra; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Component; diff --git a/src/main/java/kitchenpos/products/infra/PurgomalumClient.java b/src/main/java/kitchenpos/products/asis/infra/PurgomalumClient.java similarity index 67% rename from src/main/java/kitchenpos/products/infra/PurgomalumClient.java rename to src/main/java/kitchenpos/products/asis/infra/PurgomalumClient.java index 4002a2bb8..7c5f6b503 100644 --- a/src/main/java/kitchenpos/products/infra/PurgomalumClient.java +++ b/src/main/java/kitchenpos/products/asis/infra/PurgomalumClient.java @@ -1,4 +1,4 @@ -package kitchenpos.products.infra; +package kitchenpos.products.asis.infra; public interface PurgomalumClient { boolean containsProfanity(String text); diff --git a/src/main/java/kitchenpos/products/tobe/application/ProductService.java b/src/main/java/kitchenpos/products/tobe/application/ProductService.java new file mode 100644 index 000000000..674c50050 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/application/ProductService.java @@ -0,0 +1,74 @@ +package kitchenpos.products.tobe.application; + +import java.math.BigDecimal; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.UUID; +import kitchenpos.menus.domain.Menu; +import kitchenpos.menus.domain.MenuProduct; +import kitchenpos.menus.domain.MenuRepository; +import kitchenpos.products.tobe.application.dto.ProductCreateRequest; +import kitchenpos.products.tobe.application.dto.ProductPriceUpdateRequest; +import kitchenpos.products.tobe.application.dto.ProductResponse; +import kitchenpos.products.tobe.domain.Product; +import kitchenpos.products.tobe.domain.ProductRepository; +import kitchenpos.products.tobe.domain.PurgomalumClient; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ProductService { + + private final ProductRepository jpaProductRepository; + private final MenuRepository menuRepository; + private final PurgomalumClient purgomalumClient; + + public ProductService( + final ProductRepository productRepository, + final MenuRepository menuRepository, + final PurgomalumClient purgomalumClient + ) { + this.jpaProductRepository = productRepository; + this.menuRepository = menuRepository; + this.purgomalumClient = purgomalumClient; + } + + @Transactional + public ProductResponse create(ProductCreateRequest request) { + Product product = jpaProductRepository.save(Product.of(request.getName(), request.getPrice(), purgomalumClient)); + + return ProductResponse.toResponse(product); + } + + @Transactional + public ProductResponse changePrice(UUID productId, ProductPriceUpdateRequest request) { + + Product product = jpaProductRepository.findById(productId) + .orElseThrow(NoSuchElementException::new); + product.changePrice(request.getPrice()); + + final List menus = menuRepository.findAllByProductId(productId); + for (final Menu menu : menus) { + 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) { + menu.setDisplayed(false); + } + } + return ProductResponse.toResponse(product); + } + + @Transactional(readOnly = true) + public List findAll() { + return jpaProductRepository.findAll() + .stream() + .map(ProductResponse::toResponse) + .toList(); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/application/dto/ProductCreateRequest.java b/src/main/java/kitchenpos/products/tobe/application/dto/ProductCreateRequest.java new file mode 100644 index 000000000..376a9efe5 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/application/dto/ProductCreateRequest.java @@ -0,0 +1,21 @@ +package kitchenpos.products.tobe.application.dto; + +import java.math.BigDecimal; + +public class ProductCreateRequest { + private final String name; + private final BigDecimal price; + + public ProductCreateRequest(String name, BigDecimal price) { + this.name = name; + this.price = price; + } + + public String getName() { + return name; + } + + public BigDecimal getPrice() { + return price; + } +} diff --git a/src/main/java/kitchenpos/products/tobe/application/dto/ProductPriceUpdateRequest.java b/src/main/java/kitchenpos/products/tobe/application/dto/ProductPriceUpdateRequest.java new file mode 100644 index 000000000..6ec7cda13 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/application/dto/ProductPriceUpdateRequest.java @@ -0,0 +1,15 @@ +package kitchenpos.products.tobe.application.dto; + +import java.math.BigDecimal; + +public class ProductPriceUpdateRequest { + private final BigDecimal price; + + public ProductPriceUpdateRequest(BigDecimal price) { + this.price = price; + } + + public BigDecimal getPrice() { + return price; + } +} diff --git a/src/main/java/kitchenpos/products/tobe/application/dto/ProductResponse.java b/src/main/java/kitchenpos/products/tobe/application/dto/ProductResponse.java new file mode 100644 index 000000000..55567734c --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/application/dto/ProductResponse.java @@ -0,0 +1,33 @@ +package kitchenpos.products.tobe.application.dto; + +import java.math.BigDecimal; +import java.util.UUID; +import kitchenpos.products.tobe.domain.Product; + +public class ProductResponse { + private final UUID id; + private final String name; + private final BigDecimal price; + + public ProductResponse(UUID id, String name, BigDecimal price) { + this.id = id; + this.name = name; + this.price = price; + } + + public UUID getId() { + return id; + } + + public String getName() { + return name; + } + + public BigDecimal getPrice() { + return price; + } + + public static ProductResponse toResponse(Product product) { + return new ProductResponse(product.getId(), product.getName(), product.getPrice()); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/domain/Product.java b/src/main/java/kitchenpos/products/tobe/domain/Product.java new file mode 100644 index 000000000..c824346f9 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/Product.java @@ -0,0 +1,55 @@ +package kitchenpos.products.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.math.BigDecimal; +import java.util.UUID; + +@Table(name = "product") +@Entity +public class Product { + + @Column(name = "id", columnDefinition = "binary(16)") + @Id + private UUID id; + + @Embedded + private ProductName name; + + @Embedded + private ProductPrice price; + + protected Product() { + } + + private Product(ProductName name, ProductPrice price) { + this.id = UUID.randomUUID(); + this.name = name; + this.price = price; + } + + public static Product of(String name, BigDecimal price, PurgomalumClient purgomalumClient) { + return new Product(ProductName.from(name, purgomalumClient), + ProductPrice.from(price)); + } + + public UUID getId() { + return id; + } + + public String getName() { + return name.getName(); + } + + public BigDecimal getPrice() { + return price.getPrice(); + } + + public void changePrice(BigDecimal price) { + this.price = ProductPrice.from(price); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/domain/ProductName.java b/src/main/java/kitchenpos/products/tobe/domain/ProductName.java new file mode 100644 index 000000000..535fa9e55 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/ProductName.java @@ -0,0 +1,35 @@ +package kitchenpos.products.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import kitchenpos.products.tobe.exception.ProductNameRequiredException; +import kitchenpos.products.tobe.exception.ProfanityException; + +@Embeddable +public class ProductName { + + @Column(name = "name", nullable = false) + private String name; + + protected ProductName() { + } + + private ProductName(String name) { + this.name = name; + } + + public static ProductName from(String name, PurgomalumClient purgomalumClient) { + if (name == null || name.isEmpty()) { + throw new ProductNameRequiredException(); + } + + if (purgomalumClient.containsProfanity(name)) { + throw new ProfanityException(); + } + return new ProductName(name); + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/kitchenpos/products/tobe/domain/ProductPrice.java b/src/main/java/kitchenpos/products/tobe/domain/ProductPrice.java new file mode 100644 index 000000000..33caff960 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/ProductPrice.java @@ -0,0 +1,52 @@ +package kitchenpos.products.tobe.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import java.math.BigDecimal; +import java.util.Objects; +import kitchenpos.products.tobe.exception.InvalidPriceException; + +@Embeddable +public class ProductPrice { + + @Column(name = "price", nullable = false) + private BigDecimal price; + + protected ProductPrice() {} + + private ProductPrice(BigDecimal price) { + this.price = price; + } + + public static ProductPrice from(BigDecimal price) { + validate(price); + return new ProductPrice(price); + } + + private static void validate(BigDecimal price) { + if (price == null || price.compareTo(BigDecimal.ZERO) < 0) { + throw new InvalidPriceException(); + } + } + + public BigDecimal getPrice() { + return price; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProductPrice that = (ProductPrice) o; + return Objects.equals(price, that.price); + } + + @Override + public int hashCode() { + return Objects.hashCode(price); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/domain/ProductRepository.java b/src/main/java/kitchenpos/products/tobe/domain/ProductRepository.java new file mode 100644 index 000000000..a41daecdb --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/ProductRepository.java @@ -0,0 +1,16 @@ +package kitchenpos.products.tobe.domain; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface ProductRepository { + Product save(Product product); + + Optional findById(UUID id); + + List findAll(); + + List findAllByIdIn(List ids); +} + diff --git a/src/main/java/kitchenpos/products/tobe/domain/PurgomalumClient.java b/src/main/java/kitchenpos/products/tobe/domain/PurgomalumClient.java new file mode 100644 index 000000000..d2bed1485 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/domain/PurgomalumClient.java @@ -0,0 +1,5 @@ +package kitchenpos.products.tobe.domain; + +public interface PurgomalumClient { + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/products/tobe/exception/InvalidPriceException.java b/src/main/java/kitchenpos/products/tobe/exception/InvalidPriceException.java new file mode 100644 index 000000000..d4b7b4a23 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/exception/InvalidPriceException.java @@ -0,0 +1,11 @@ +package kitchenpos.products.tobe.exception; + +public class InvalidPriceException extends IllegalArgumentException { + public InvalidPriceException(String message) { + super(message); + } + + public InvalidPriceException() { + super("가격은 0 이상이어야 합니다."); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/exception/ProductNameRequiredException.java b/src/main/java/kitchenpos/products/tobe/exception/ProductNameRequiredException.java new file mode 100644 index 000000000..747132bb2 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/exception/ProductNameRequiredException.java @@ -0,0 +1,11 @@ +package kitchenpos.products.tobe.exception; + +public class ProductNameRequiredException extends IllegalArgumentException { + public ProductNameRequiredException(String message) { + super(message); + } + + public ProductNameRequiredException() { + super("상품명은 필수로 입력해야 합니다."); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/exception/ProfanityException.java b/src/main/java/kitchenpos/products/tobe/exception/ProfanityException.java new file mode 100644 index 000000000..2068d3803 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/exception/ProfanityException.java @@ -0,0 +1,11 @@ +package kitchenpos.products.tobe.exception; + +public class ProfanityException extends IllegalArgumentException { + public ProfanityException(String message) { + super(message); + } + + public ProfanityException() { + super("비속어가 포함되어 있습니다."); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java b/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java new file mode 100644 index 000000000..28f9447b3 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/infra/DefaultPurgomalumClient.java @@ -0,0 +1,26 @@ +package kitchenpos.products.tobe.infra; + +import java.net.URI; +import kitchenpos.products.tobe.domain.PurgomalumClient; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Component +public class DefaultPurgomalumClient implements PurgomalumClient { + private final RestTemplate restTemplate; + + public DefaultPurgomalumClient(final RestTemplateBuilder restTemplateBuilder) { + this.restTemplate = restTemplateBuilder.build(); + } + + @Override + public boolean containsProfanity(final String text) { + final URI url = UriComponentsBuilder.fromUriString("https://www.purgomalum.com/service/containsprofanity") + .queryParam("text", text) + .build() + .toUri(); + return Boolean.parseBoolean(restTemplate.getForObject(url, String.class)); + } +} diff --git a/src/main/java/kitchenpos/products/tobe/infra/JpaProductRepository.java b/src/main/java/kitchenpos/products/tobe/infra/JpaProductRepository.java new file mode 100644 index 000000000..a462cd8e2 --- /dev/null +++ b/src/main/java/kitchenpos/products/tobe/infra/JpaProductRepository.java @@ -0,0 +1,10 @@ +package kitchenpos.products.tobe.infra; + +import java.util.UUID; +import kitchenpos.products.tobe.domain.Product; +import kitchenpos.products.tobe.domain.ProductRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface JpaProductRepository extends JpaRepository, ProductRepository { + +} diff --git a/src/main/java/kitchenpos/products/ui/ProductRestController.java b/src/main/java/kitchenpos/products/ui/ProductRestController.java index c71c795a4..6d00a8a1f 100644 --- a/src/main/java/kitchenpos/products/ui/ProductRestController.java +++ b/src/main/java/kitchenpos/products/ui/ProductRestController.java @@ -1,7 +1,10 @@ package kitchenpos.products.ui; -import kitchenpos.products.application.ProductService; -import kitchenpos.products.domain.Product; +import kitchenpos.products.tobe.application.ProductService; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.tobe.application.dto.ProductCreateRequest; +import kitchenpos.products.tobe.application.dto.ProductPriceUpdateRequest; +import kitchenpos.products.tobe.application.dto.ProductResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -25,19 +28,20 @@ public ProductRestController(final ProductService productService) { } @PostMapping - public ResponseEntity create(@RequestBody final Product request) { - final Product response = productService.create(request); + public ResponseEntity create(@RequestBody final ProductCreateRequest request) { + final ProductResponse response = productService.create(request); return ResponseEntity.created(URI.create("/api/products/" + response.getId())) .body(response); } @PutMapping("/{productId}/price") - public ResponseEntity changePrice(@PathVariable final UUID productId, @RequestBody final Product request) { + public ResponseEntity changePrice(@PathVariable final UUID productId, + @RequestBody final ProductPriceUpdateRequest request) { return ResponseEntity.ok(productService.changePrice(productId, request)); } @GetMapping - public ResponseEntity> findAll() { + public ResponseEntity> findAll() { return ResponseEntity.ok(productService.findAll()); } } diff --git a/src/test/java/kitchenpos/Fixtures.java b/src/test/java/kitchenpos/Fixtures.java index 434768a52..beb469ae7 100644 --- a/src/test/java/kitchenpos/Fixtures.java +++ b/src/test/java/kitchenpos/Fixtures.java @@ -8,7 +8,7 @@ import kitchenpos.menus.domain.Menu; import kitchenpos.menus.domain.MenuGroup; import kitchenpos.menus.domain.MenuProduct; -import kitchenpos.products.domain.Product; +import kitchenpos.products.asis.domain.Product; import java.math.BigDecimal; import java.time.LocalDateTime; diff --git a/src/test/java/kitchenpos/menus/application/MenuServiceTest.java b/src/test/java/kitchenpos/menus/application/MenuServiceTest.java index 277679118..a09686b20 100644 --- a/src/test/java/kitchenpos/menus/application/MenuServiceTest.java +++ b/src/test/java/kitchenpos/menus/application/MenuServiceTest.java @@ -6,9 +6,9 @@ import kitchenpos.menus.domain.MenuRepository; import kitchenpos.products.application.FakePurgomalumClient; import kitchenpos.products.application.InMemoryProductRepository; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.asis.domain.ProductRepository; +import kitchenpos.products.asis.infra.PurgomalumClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java b/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java index 3c4114798..dbb181cb1 100644 --- a/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java +++ b/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java @@ -1,6 +1,6 @@ package kitchenpos.products.application; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.asis.infra.PurgomalumClient; import java.util.Arrays; import java.util.List; diff --git a/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java b/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java index b55c5ec5e..8286eee9b 100644 --- a/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java +++ b/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java @@ -1,7 +1,7 @@ package kitchenpos.products.application; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.asis.domain.ProductRepository; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/test/java/kitchenpos/products/application/ProductServiceTest.java b/src/test/java/kitchenpos/products/application/ProductServiceTest.java index 74a31073e..3d170d646 100644 --- a/src/test/java/kitchenpos/products/application/ProductServiceTest.java +++ b/src/test/java/kitchenpos/products/application/ProductServiceTest.java @@ -3,9 +3,10 @@ import kitchenpos.menus.application.InMemoryMenuRepository; import kitchenpos.menus.domain.Menu; import kitchenpos.menus.domain.MenuRepository; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.products.asis.application.ProductService; +import kitchenpos.products.asis.domain.Product; +import kitchenpos.products.asis.domain.ProductRepository; +import kitchenpos.products.asis.infra.PurgomalumClient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/src/test/java/kitchenpos/products/tobe/FakePurgomalumClient.java b/src/test/java/kitchenpos/products/tobe/FakePurgomalumClient.java new file mode 100644 index 000000000..0cdda10d6 --- /dev/null +++ b/src/test/java/kitchenpos/products/tobe/FakePurgomalumClient.java @@ -0,0 +1,19 @@ +package kitchenpos.products.tobe; + +import java.util.Arrays; +import java.util.List; +import kitchenpos.products.tobe.domain.PurgomalumClient; + +public class FakePurgomalumClient implements PurgomalumClient { + private static final List profanities; + + static { + profanities = Arrays.asList("비속어", "욕설"); + } + + @Override + public boolean containsProfanity(final String text) { + return profanities.stream() + .anyMatch(profanity -> text.contains(profanity)); + } +} diff --git a/src/test/java/kitchenpos/products/tobe/InMemoryProductRepository.java b/src/test/java/kitchenpos/products/tobe/InMemoryProductRepository.java new file mode 100644 index 000000000..795164d27 --- /dev/null +++ b/src/test/java/kitchenpos/products/tobe/InMemoryProductRepository.java @@ -0,0 +1,38 @@ +package kitchenpos.products.tobe; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import kitchenpos.products.tobe.domain.Product; +import kitchenpos.products.tobe.domain.ProductRepository; + +public class InMemoryProductRepository implements ProductRepository { + private final Map products = new HashMap<>(); + + @Override + public Product save(final Product product) { + products.put(product.getId(), product); + return product; + } + + @Override + public Optional findById(final UUID id) { + return Optional.ofNullable(products.get(id)); + } + + @Override + public List findAll() { + return new ArrayList<>(products.values()); + } + + @Override + public List findAllByIdIn(final List ids) { + return products.values() + .stream() + .filter(product -> ids.contains(product.getId())) + .toList(); + } +} diff --git a/src/test/java/kitchenpos/products/tobe/ProductServiceTest.java b/src/test/java/kitchenpos/products/tobe/ProductServiceTest.java new file mode 100644 index 000000000..e66283a68 --- /dev/null +++ b/src/test/java/kitchenpos/products/tobe/ProductServiceTest.java @@ -0,0 +1,118 @@ +package kitchenpos.products.tobe; + +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.math.BigDecimal; +import java.util.List; +import java.util.UUID; +import kitchenpos.menus.application.InMemoryMenuRepository; +import kitchenpos.menus.domain.MenuRepository; +import kitchenpos.products.tobe.application.ProductService; +import kitchenpos.products.tobe.application.dto.ProductCreateRequest; +import kitchenpos.products.tobe.application.dto.ProductPriceUpdateRequest; +import kitchenpos.products.tobe.application.dto.ProductResponse; +import kitchenpos.products.tobe.domain.PurgomalumClient; +import kitchenpos.products.tobe.domain.ProductRepository; +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +class ProductServiceTest { + + private ProductRepository productRepository; + private MenuRepository menuRepository; + private PurgomalumClient purgomalumClient; + private ProductService productService; + + @BeforeEach + void setUp() { + productRepository = new InMemoryProductRepository(); + menuRepository = new InMemoryMenuRepository(); + purgomalumClient = new FakePurgomalumClient(); + productService = new ProductService(productRepository, menuRepository, purgomalumClient); + } + + @DisplayName("상품을 등록할 수 있다.") + @Test + void create() { + final ProductCreateRequest expected = createProductRequest("후라이드", 16_000L); + final ProductResponse actual = productService.create(expected); + assertThat(actual).isNotNull(); + assertAll( + () -> assertThat(actual.getId()).isNotNull(), + () -> assertThat(actual.getName()).isEqualTo(expected.getName()), + () -> assertThat(actual.getPrice()).isEqualTo(expected.getPrice()) + ); + } + + @DisplayName("상품의 가격이 올바르지 않으면 등록할 수 없다.") + @ValueSource(strings = "-1000") + @NullSource + @ParameterizedTest + void create(final BigDecimal price) { + final ProductCreateRequest expected = createProductRequest("후라이드", price); + assertThatThrownBy(() -> productService.create(expected)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("상품의 이름이 올바르지 않으면 등록할 수 없다.") + @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) + @NullSource + @ParameterizedTest + void create(final String name) { + final ProductCreateRequest expected = createProductRequest(name, 16_000L); + assertThatThrownBy(() -> productService.create(expected)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("상품의 가격을 변경할 수 있다.") + @Test + void changePrice() { + final UUID productId = productService.create(createProductRequest("후라이드", 16_000L)).getId(); + final ProductPriceUpdateRequest expected = changePriceRequest(15_000L); + final ProductResponse actual = productService.changePrice(productId, expected); + assertThat(actual.getPrice()).isEqualTo(expected.getPrice()); + } + + @DisplayName("상품의 가격이 올바르지 않으면 변경할 수 없다.") + @ValueSource(strings = "-1000") + @NullSource + @ParameterizedTest + void changePrice(final BigDecimal price) { + final UUID productId = productService.create(createProductRequest("후라이드", 16_000L)).getId(); + final ProductPriceUpdateRequest expected = changePriceRequest(price); + assertThatThrownBy(() -> productService.changePrice(productId, expected)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("상품의 목록을 조회할 수 있다.") + @Test + void findAll() { + productService.create(createProductRequest("후라이드", 16_000L)); + productService.create(createProductRequest("양념치킨", 16_000L)); + + final List actual = productService.findAll(); + assertThat(actual).hasSize(2); + } + + private ProductCreateRequest createProductRequest(final String name, final long price) { + return createProductRequest(name, BigDecimal.valueOf(price)); + } + + private ProductCreateRequest createProductRequest(final String name, final BigDecimal price) { + return new ProductCreateRequest(name, price); + } + + private ProductPriceUpdateRequest changePriceRequest(final long price) { + return changePriceRequest(BigDecimal.valueOf(price)); + } + + private ProductPriceUpdateRequest changePriceRequest(final BigDecimal price) { + return new ProductPriceUpdateRequest(price); + } +} diff --git a/src/test/java/kitchenpos/products/tobe/domain/ProductNameTest.java b/src/test/java/kitchenpos/products/tobe/domain/ProductNameTest.java new file mode 100644 index 000000000..e37cdc614 --- /dev/null +++ b/src/test/java/kitchenpos/products/tobe/domain/ProductNameTest.java @@ -0,0 +1,74 @@ +package kitchenpos.products.tobe.domain; + +import kitchenpos.products.tobe.exception.ProductNameRequiredException; +import kitchenpos.products.tobe.exception.ProfanityException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ProductNameTest { + + private PurgomalumClient purgomalumClient; + + @BeforeEach + void setUp() { + purgomalumClient = Mockito.mock(PurgomalumClient.class); + } + + @Test + @DisplayName("상품명이 null일 때 ProductNameRequiredException이 발생해야 한다.") + void testFrom_NullName_ThrowsProductNameRequiredException() { + // Arrange + String name = null; + + // Act & Assert + assertThrows(ProductNameRequiredException.class, () -> { + ProductName.from(name, purgomalumClient); + }); + } + + @Test + @DisplayName("상품명이 비어 있을 때 ProductNameRequiredException이 발생해야 한다.") + void testFrom_EmptyName_ThrowsProductNameRequiredException() { + // Arrange + String name = ""; + + // Act & Assert + assertThrows(ProductNameRequiredException.class, () -> { + ProductName.from(name, purgomalumClient); + }); + } + + @Test + @DisplayName("상품명에 비속어가 포함되어 있을 때 ProfanityException이 발생해야 한다.") + void testFrom_NameWithProfanity_ThrowsProfanityException() { + // Arrange + String name = "badword"; + Mockito.when(purgomalumClient.containsProfanity(name)).thenReturn(true); + + // Act & Assert + assertThrows(ProfanityException.class, () -> { + ProductName.from(name, purgomalumClient); + }); + } + + @Test + @DisplayName("유효한 상품명이 주어졌을 때 ProductName 객체가 반환되어야 한다.") + void testFrom_ValidName_ReturnsProductName() { + // Arrange + String name = "Valid Product"; + Mockito.when(purgomalumClient.containsProfanity(name)).thenReturn(false); + + // Act + ProductName productName = ProductName.from(name, purgomalumClient); + + // Assert + assertNotNull(productName); + assertEquals(name, productName.getName()); + } +} diff --git a/src/test/java/kitchenpos/products/tobe/domain/ProductPriceTest.java b/src/test/java/kitchenpos/products/tobe/domain/ProductPriceTest.java new file mode 100644 index 000000000..31ee51545 --- /dev/null +++ b/src/test/java/kitchenpos/products/tobe/domain/ProductPriceTest.java @@ -0,0 +1,77 @@ +package kitchenpos.products.tobe.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import kitchenpos.products.tobe.exception.InvalidPriceException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +class ProductPriceTest { + + @Test + @DisplayName("가격이 null일 때 InvalidPriceException이 발생해야 한다.") + void testFrom_NullPrice_ThrowsInvalidPriceException() { + // Arrange + BigDecimal price = null; + + // Act & Assert + assertThrows(InvalidPriceException.class, () -> { + ProductPrice.from(price); + }); + } + + @Test + @DisplayName("가격이 0보다 작을 때 InvalidPriceException이 발생해야 한다.") + void testFrom_NegativePrice_ThrowsInvalidPriceException() { + // Arrange + BigDecimal price = new BigDecimal("-10.00"); + + // Act & Assert + assertThrows(InvalidPriceException.class, () -> { + ProductPrice.from(price); + }); + } + + @Test + @DisplayName("가격이 0일 때 ProductPrice 객체가 반환되어야 한다.") + void testFrom_ZeroPrice_ReturnsProductPrice() { + // Arrange + BigDecimal price = BigDecimal.ZERO; + + // Act + ProductPrice productPrice = ProductPrice.from(price); + + // Assert + assertNotNull(productPrice); + assertEquals(price, productPrice.getPrice()); + } + + @Test + @DisplayName("가격이 0보다 클 때 ProductPrice 객체가 반환되어야 한다.") + void testFrom_PositivePrice_ReturnsProductPrice() { + // Arrange + BigDecimal price = new BigDecimal("10.00"); + + // Act + ProductPrice productPrice = ProductPrice.from(price); + + // Assert + assertNotNull(productPrice); + assertEquals(price, productPrice.getPrice()); + } + + @Test + @DisplayName("동일한 가격을 가진 두 ProductPrice 객체는 같아야 한다.") + void testEquals_SamePrice_ReturnsTrue() { + // Arrange + BigDecimal price = new BigDecimal("10.00"); + ProductPrice productPrice1 = ProductPrice.from(price); + ProductPrice productPrice2 = ProductPrice.from(price); + + // Act & Assert + assertEquals(productPrice1, productPrice2); + assertEquals(productPrice1.hashCode(), productPrice2.hashCode()); + } +}