diff --git a/src/main/java/kitchenpos/eatinorders/application/OrderService.java b/src/main/java/kitchenpos/core/eatinorders/application/OrderService.java similarity index 88% rename from src/main/java/kitchenpos/eatinorders/application/OrderService.java rename to src/main/java/kitchenpos/core/eatinorders/application/OrderService.java index 1159a0ba0..729736fbf 100644 --- a/src/main/java/kitchenpos/eatinorders/application/OrderService.java +++ b/src/main/java/kitchenpos/core/eatinorders/application/OrderService.java @@ -1,15 +1,16 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.deliveryorders.infra.KitchenridersClient; -import kitchenpos.eatinorders.domain.Order; -import kitchenpos.eatinorders.domain.OrderLineItem; -import kitchenpos.eatinorders.domain.OrderRepository; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderTableRepository; -import kitchenpos.eatinorders.domain.OrderType; -import kitchenpos.menus.domain.Menu; -import kitchenpos.menus.domain.MenuRepository; +import kitchenpos.core.shared.value.Money; +import kitchenpos.data.KitchenridersClient; +import kitchenpos.core.eatinorders.domain.Order; +import kitchenpos.core.eatinorders.domain.OrderLineItem; +import kitchenpos.core.eatinorders.domain.OrderRepository; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderTableRepository; +import kitchenpos.core.eatinorders.domain.OrderType; +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -71,7 +72,7 @@ public Order create(final Order request) { if (!menu.isDisplayed()) { throw new IllegalStateException(); } - if (menu.getPrice().compareTo(orderLineItemRequest.getPrice()) != 0) { + if (!menu.getPrice().isEqual(Money.wons(orderLineItemRequest.getPrice()))) { throw new IllegalArgumentException(); } final OrderLineItem orderLineItem = new OrderLineItem(); @@ -111,13 +112,13 @@ public Order accept(final UUID orderId) { throw new IllegalStateException(); } if (order.getType() == OrderType.DELIVERY) { - BigDecimal sum = BigDecimal.ZERO; + Money sum = Money.ZERO; for (final OrderLineItem orderLineItem : order.getOrderLineItems()) { sum = orderLineItem.getMenu() .getPrice() - .multiply(BigDecimal.valueOf(orderLineItem.getQuantity())); + .multiply(orderLineItem.getQuantity()); } - kitchenridersClient.requestDelivery(orderId, sum, order.getDeliveryAddress()); + kitchenridersClient.requestDelivery(orderId, sum.getAmount(), order.getDeliveryAddress()); } order.setStatus(OrderStatus.ACCEPTED); return order; diff --git a/src/main/java/kitchenpos/eatinorders/application/OrderTableService.java b/src/main/java/kitchenpos/core/eatinorders/application/OrderTableService.java similarity index 90% rename from src/main/java/kitchenpos/eatinorders/application/OrderTableService.java rename to src/main/java/kitchenpos/core/eatinorders/application/OrderTableService.java index 1df7e345f..a59666ac0 100644 --- a/src/main/java/kitchenpos/eatinorders/application/OrderTableService.java +++ b/src/main/java/kitchenpos/core/eatinorders/application/OrderTableService.java @@ -1,9 +1,9 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.eatinorders.domain.OrderRepository; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderTableRepository; +import kitchenpos.core.eatinorders.domain.OrderRepository; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderTableRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/kitchenpos/eatinorders/domain/JpaOrderRepository.java b/src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderRepository.java similarity index 80% rename from src/main/java/kitchenpos/eatinorders/domain/JpaOrderRepository.java rename to src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderRepository.java index 01c825c45..993242cc8 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/JpaOrderRepository.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/kitchenpos/eatinorders/domain/JpaOrderTableRepository.java b/src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderTableRepository.java similarity index 81% rename from src/main/java/kitchenpos/eatinorders/domain/JpaOrderTableRepository.java rename to src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderTableRepository.java index 84c0d3c6f..abd03ed25 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/JpaOrderTableRepository.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/JpaOrderTableRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/kitchenpos/eatinorders/domain/Order.java b/src/main/java/kitchenpos/core/eatinorders/domain/Order.java similarity index 98% rename from src/main/java/kitchenpos/eatinorders/domain/Order.java rename to src/main/java/kitchenpos/core/eatinorders/domain/Order.java index 4a5991301..e6034ffc5 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/Order.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/Order.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderLineItem.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderLineItem.java similarity index 95% rename from src/main/java/kitchenpos/eatinorders/domain/OrderLineItem.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderLineItem.java index a5fe38278..2d1f083bc 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderLineItem.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderLineItem.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,7 +10,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; -import kitchenpos.menus.domain.Menu; +import kitchenpos.core.menus.domain.Menu; import java.math.BigDecimal; import java.util.UUID; diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderRepository.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderRepository.java similarity index 87% rename from src/main/java/kitchenpos/eatinorders/domain/OrderRepository.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderRepository.java index f98d45c15..fd5466748 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderRepository.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderStatus.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderStatus.java similarity index 67% rename from src/main/java/kitchenpos/eatinorders/domain/OrderStatus.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderStatus.java index fe0f76c7d..6869758da 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderStatus.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderStatus.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; public enum OrderStatus { WAITING, ACCEPTED, SERVED, DELIVERING, DELIVERED, COMPLETED diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderTable.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderTable.java similarity index 96% rename from src/main/java/kitchenpos/eatinorders/domain/OrderTable.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderTable.java index bdd3dd23c..eedcdb165 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderTable.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderTable.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderTableRepository.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderTableRepository.java similarity index 84% rename from src/main/java/kitchenpos/eatinorders/domain/OrderTableRepository.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderTableRepository.java index 1e9047d43..256113753 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderTableRepository.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderTableRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/kitchenpos/eatinorders/domain/OrderType.java b/src/main/java/kitchenpos/core/eatinorders/domain/OrderType.java similarity index 56% rename from src/main/java/kitchenpos/eatinorders/domain/OrderType.java rename to src/main/java/kitchenpos/core/eatinorders/domain/OrderType.java index 0e3133e69..655a5efae 100644 --- a/src/main/java/kitchenpos/eatinorders/domain/OrderType.java +++ b/src/main/java/kitchenpos/core/eatinorders/domain/OrderType.java @@ -1,4 +1,4 @@ -package kitchenpos.eatinorders.domain; +package kitchenpos.core.eatinorders.domain; public enum OrderType { DELIVERY, TAKEOUT, EAT_IN diff --git a/src/main/java/kitchenpos/core/foundation/exception/SystemException.java b/src/main/java/kitchenpos/core/foundation/exception/SystemException.java new file mode 100644 index 000000000..938f353cb --- /dev/null +++ b/src/main/java/kitchenpos/core/foundation/exception/SystemException.java @@ -0,0 +1,34 @@ +package kitchenpos.core.foundation.exception; + +import org.springframework.context.MessageSourceResolvable; + +public class SystemException extends RuntimeException implements MessageSourceResolvable { + + public SystemException(String format, Object... args) { + super(String.format(format, args)); + } + + public SystemException(Throwable cause) { + super(cause); + } + + public SystemException(String message, Throwable cause) { + super(message, cause); + } + + @Override + public String[] getCodes() { + return new String[]{"Exception." + getClass().getSimpleName()}; + } + + @Override + public Object[] getArguments() { + return new Object[0]; + } + + @Override + public String getDefaultMessage() { + return getMessage(); + } + +} diff --git a/src/main/java/kitchenpos/core/menus/application/MenuEventListener.java b/src/main/java/kitchenpos/core/menus/application/MenuEventListener.java new file mode 100644 index 000000000..f60d5cabb --- /dev/null +++ b/src/main/java/kitchenpos/core/menus/application/MenuEventListener.java @@ -0,0 +1,24 @@ +package kitchenpos.core.menus.application; + +import jakarta.transaction.Transactional; +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuRepository; +import kitchenpos.core.shared.event.ProductPriceChangedEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +public class MenuEventListener { + private final MenuRepository menuRepository; + + public MenuEventListener(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) + public void onProductPriceChanged(final ProductPriceChangedEvent event) { + menuRepository.findAllByProductId(event.id()) + .forEach(Menu::recalculateDisplayStatus); + } +} diff --git a/src/main/java/kitchenpos/menus/application/MenuGroupService.java b/src/main/java/kitchenpos/core/menus/application/MenuGroupService.java similarity index 86% rename from src/main/java/kitchenpos/menus/application/MenuGroupService.java rename to src/main/java/kitchenpos/core/menus/application/MenuGroupService.java index c468893a2..f501946f8 100644 --- a/src/main/java/kitchenpos/menus/application/MenuGroupService.java +++ b/src/main/java/kitchenpos/core/menus/application/MenuGroupService.java @@ -1,7 +1,7 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.MenuGroup; -import kitchenpos.menus.domain.MenuGroupRepository; +import kitchenpos.core.menus.domain.MenuGroup; +import kitchenpos.core.menus.domain.MenuGroupRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/java/kitchenpos/menus/application/MenuService.java b/src/main/java/kitchenpos/core/menus/application/MenuService.java similarity index 73% rename from src/main/java/kitchenpos/menus/application/MenuService.java rename to src/main/java/kitchenpos/core/menus/application/MenuService.java index abefa5bcf..3fb7a5558 100644 --- a/src/main/java/kitchenpos/menus/application/MenuService.java +++ b/src/main/java/kitchenpos/core/menus/application/MenuService.java @@ -1,13 +1,16 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.Menu; -import kitchenpos.menus.domain.MenuGroup; -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.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuGroup; +import kitchenpos.core.menus.domain.MenuGroupRepository; +import kitchenpos.core.menus.domain.MenuProduct; +import kitchenpos.core.menus.domain.MenuRepository; +import kitchenpos.core.products.domain.ProductRepository; +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import kitchenpos.core.shared.domain.ProfanityChecker; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,24 +25,24 @@ public class MenuService { private final MenuRepository menuRepository; private final MenuGroupRepository menuGroupRepository; - private final ProductRepository productRepository; - private final PurgomalumClient purgomalumClient; + private final TobeProductRepository productRepository; + private final ProfanityChecker profanityChecker; public MenuService( - final MenuRepository menuRepository, - final MenuGroupRepository menuGroupRepository, - final ProductRepository productRepository, - final PurgomalumClient purgomalumClient + final MenuRepository menuRepository, + final MenuGroupRepository menuGroupRepository, + final TobeProductRepository productRepository, + final ProfanityChecker profanityChecker ) { this.menuRepository = menuRepository; this.menuGroupRepository = menuGroupRepository; this.productRepository = productRepository; - this.purgomalumClient = purgomalumClient; + this.profanityChecker = profanityChecker; } @Transactional public Menu create(final Menu request) { - final BigDecimal price = request.getPrice(); + final BigDecimal price = request.getPrice().getAmount(); if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException(); } @@ -60,32 +63,32 @@ public Menu create(final Menu request) { final List menuProducts = new ArrayList<>(); BigDecimal sum = BigDecimal.ZERO; for (final MenuProduct menuProductRequest : menuProductRequests) { - final long quantity = menuProductRequest.getQuantity(); + final long quantity = menuProductRequest.getQuantity().getValue(); if (quantity < 0) { throw new IllegalArgumentException(); } final Product product = productRepository.findById(menuProductRequest.getProductId()) .orElseThrow(NoSuchElementException::new); sum = sum.add( - product.getPrice() + product.getPrice().getPrice().getAmount() .multiply(BigDecimal.valueOf(quantity)) ); final MenuProduct menuProduct = new MenuProduct(); menuProduct.setProduct(product); - menuProduct.setQuantity(quantity); + menuProduct.setQuantity(Quantity.of(quantity)); menuProducts.add(menuProduct); } if (price.compareTo(sum) > 0) { throw new IllegalArgumentException(); } final String name = request.getName(); - if (Objects.isNull(name) || purgomalumClient.containsProfanity(name)) { + if (Objects.isNull(name) || profanityChecker.containsProfanity(name)) { throw new IllegalArgumentException(); } final Menu menu = new Menu(); menu.setId(UUID.randomUUID()); menu.setName(name); - menu.setPrice(price); + menu.setPrice(Money.wons(price)); menu.setMenuGroup(menuGroup); menu.setDisplayed(request.isDisplayed()); menu.setMenuProducts(menuProducts); @@ -94,7 +97,7 @@ public Menu create(final Menu request) { @Transactional public Menu changePrice(final UUID menuId, final Menu request) { - final BigDecimal price = request.getPrice(); + final BigDecimal price = request.getPrice().getAmount(); if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException(); } @@ -104,14 +107,14 @@ public Menu changePrice(final UUID menuId, final Menu request) { for (final MenuProduct menuProduct : menu.getMenuProducts()) { sum = sum.add( menuProduct.getProduct() - .getPrice() - .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) + .getPrice().getPrice().getAmount() + .multiply(BigDecimal.valueOf(menuProduct.getQuantity().getValue())) ); } if (price.compareTo(sum) > 0) { throw new IllegalArgumentException(); } - menu.setPrice(price); + menu.setPrice(Money.wons(price)); return menu; } @@ -123,11 +126,11 @@ public Menu display(final UUID menuId) { for (final MenuProduct menuProduct : menu.getMenuProducts()) { sum = sum.add( menuProduct.getProduct() - .getPrice() - .multiply(BigDecimal.valueOf(menuProduct.getQuantity())) + .getPrice().getPrice().getAmount() + .multiply(BigDecimal.valueOf(menuProduct.getQuantity().getValue())) ); } - if (menu.getPrice().compareTo(sum) > 0) { + if (menu.getPrice().getAmount().compareTo(sum) > 0) { throw new IllegalStateException(); } menu.setDisplayed(true); diff --git a/src/main/java/kitchenpos/menus/domain/JpaMenuGroupRepository.java b/src/main/java/kitchenpos/core/menus/domain/JpaMenuGroupRepository.java similarity index 83% rename from src/main/java/kitchenpos/menus/domain/JpaMenuGroupRepository.java rename to src/main/java/kitchenpos/core/menus/domain/JpaMenuGroupRepository.java index 233488198..70dd3f37e 100644 --- a/src/main/java/kitchenpos/menus/domain/JpaMenuGroupRepository.java +++ b/src/main/java/kitchenpos/core/menus/domain/JpaMenuGroupRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/kitchenpos/menus/domain/JpaMenuRepository.java b/src/main/java/kitchenpos/core/menus/domain/JpaMenuRepository.java similarity index 71% rename from src/main/java/kitchenpos/menus/domain/JpaMenuRepository.java rename to src/main/java/kitchenpos/core/menus/domain/JpaMenuRepository.java index 796499c30..2cbedbead 100644 --- a/src/main/java/kitchenpos/menus/domain/JpaMenuRepository.java +++ b/src/main/java/kitchenpos/core/menus/domain/JpaMenuRepository.java @@ -1,5 +1,6 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; +import kitchenpos.core.shared.identifier.ProductId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -10,5 +11,5 @@ 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); + List findAllByProductId(@Param("productId") ProductId productId); } diff --git a/src/main/java/kitchenpos/menus/domain/Menu.java b/src/main/java/kitchenpos/core/menus/domain/Menu.java similarity index 72% rename from src/main/java/kitchenpos/menus/domain/Menu.java rename to src/main/java/kitchenpos/core/menus/domain/Menu.java index 8d1cc1139..ce7d47b28 100644 --- a/src/main/java/kitchenpos/menus/domain/Menu.java +++ b/src/main/java/kitchenpos/core/menus/domain/Menu.java @@ -1,15 +1,8 @@ -package kitchenpos.menus.domain; - -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.ForeignKey; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import jakarta.persistence.Table; -import jakarta.persistence.Transient; +package kitchenpos.core.menus.domain; + +import jakarta.persistence.*; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.shared.value.Money; import java.math.BigDecimal; import java.util.List; @@ -25,8 +18,8 @@ public class Menu { @Column(name = "name", nullable = false) private String name; - @Column(name = "price", nullable = false) - private BigDecimal price; + @AttributeOverride(name = "amount", column = @Column(name = "price", nullable = false, columnDefinition = "decimal(19,2)")) + private Money price; @ManyToOne(optional = false) @JoinColumn( @@ -54,6 +47,16 @@ public class Menu { public Menu() { } + public void recalculateDisplayStatus() { + Money sum = menuProducts.stream() + .map(MenuProduct::calculatePrice) + .reduce(Money.ZERO, Money::add); + // 메뉴 가격이 구성 상품의 총합보다 높으면 표시 안 함 + if (this.price.isBiggerThan(sum)) { + this.displayed = false; + } + } + public UUID getId() { return id; } @@ -70,11 +73,11 @@ public void setName(final String name) { this.name = name; } - public BigDecimal getPrice() { + public Money getPrice() { return price; } - public void setPrice(final BigDecimal price) { + public void setPrice(final Money price) { this.price = price; } diff --git a/src/main/java/kitchenpos/menus/domain/MenuGroup.java b/src/main/java/kitchenpos/core/menus/domain/MenuGroup.java similarity index 94% rename from src/main/java/kitchenpos/menus/domain/MenuGroup.java rename to src/main/java/kitchenpos/core/menus/domain/MenuGroup.java index 70e0b8e23..36af130b4 100644 --- a/src/main/java/kitchenpos/menus/domain/MenuGroup.java +++ b/src/main/java/kitchenpos/core/menus/domain/MenuGroup.java @@ -1,4 +1,4 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/kitchenpos/menus/domain/MenuGroupRepository.java b/src/main/java/kitchenpos/core/menus/domain/MenuGroupRepository.java similarity index 86% rename from src/main/java/kitchenpos/menus/domain/MenuGroupRepository.java rename to src/main/java/kitchenpos/core/menus/domain/MenuGroupRepository.java index b25e6acbc..85a1d4028 100644 --- a/src/main/java/kitchenpos/menus/domain/MenuGroupRepository.java +++ b/src/main/java/kitchenpos/core/menus/domain/MenuGroupRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/kitchenpos/menus/domain/MenuProduct.java b/src/main/java/kitchenpos/core/menus/domain/MenuProduct.java similarity index 64% rename from src/main/java/kitchenpos/menus/domain/MenuProduct.java rename to src/main/java/kitchenpos/core/menus/domain/MenuProduct.java index b47ca26cb..7ce3001f1 100644 --- a/src/main/java/kitchenpos/menus/domain/MenuProduct.java +++ b/src/main/java/kitchenpos/core/menus/domain/MenuProduct.java @@ -1,4 +1,4 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -10,8 +10,13 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.Transient; -import kitchenpos.products.domain.Product; +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; +import java.math.BigDecimal; import java.util.UUID; @Table(name = "menu_product") @@ -28,13 +33,13 @@ public class MenuProduct { columnDefinition = "binary(16)", foreignKey = @ForeignKey(name = "fk_menu_product_to_product") ) - private Product product; + private kitchenpos.core.products.tobe.domain.Product product; @Column(name = "quantity", nullable = false) - private long quantity; + private Quantity quantity; @Transient - private UUID productId; + private ProductId productId; public MenuProduct() { } @@ -55,19 +60,24 @@ public void setProduct(final Product product) { this.product = product; } - public long getQuantity() { + public Quantity getQuantity() { return quantity; } - public void setQuantity(final long quantity) { + public void setQuantity(final Quantity quantity) { this.quantity = quantity; } - public UUID getProductId() { + public ProductId getProductId() { return productId; } - public void setProductId(final UUID productId) { + public void setProductId(final ProductId productId) { this.productId = productId; } + + public Money calculatePrice() { + return product.getPrice().multiply(quantity); + } + } diff --git a/src/main/java/kitchenpos/menus/domain/MenuRepository.java b/src/main/java/kitchenpos/core/menus/domain/MenuRepository.java similarity index 63% rename from src/main/java/kitchenpos/menus/domain/MenuRepository.java rename to src/main/java/kitchenpos/core/menus/domain/MenuRepository.java index 5fbaab864..7ebb077c9 100644 --- a/src/main/java/kitchenpos/menus/domain/MenuRepository.java +++ b/src/main/java/kitchenpos/core/menus/domain/MenuRepository.java @@ -1,4 +1,6 @@ -package kitchenpos.menus.domain; +package kitchenpos.core.menus.domain; + +import kitchenpos.core.shared.identifier.ProductId; import java.util.List; import java.util.Optional; @@ -13,6 +15,6 @@ public interface MenuRepository { List findAllByIdIn(List ids); - List findAllByProductId(UUID productId); + List findAllByProductId(ProductId productId); } diff --git a/src/main/java/kitchenpos/core/products/application/AddProduct.java b/src/main/java/kitchenpos/core/products/application/AddProduct.java new file mode 100644 index 000000000..4d28e176b --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/AddProduct.java @@ -0,0 +1,8 @@ +package kitchenpos.core.products.application; + +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.products.tobe.domain.Product; + +public interface AddProduct { + Product addProduct(CreateProductRequest request); +} diff --git a/src/main/java/kitchenpos/core/products/application/ChangeProductPrice.java b/src/main/java/kitchenpos/core/products/application/ChangeProductPrice.java new file mode 100644 index 000000000..efd4f8825 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/ChangeProductPrice.java @@ -0,0 +1,10 @@ +package kitchenpos.core.products.application; + +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.products.tobe.domain.exception.ProductNotFoundException; +import kitchenpos.core.shared.identifier.ProductId; + +public interface ChangeProductPrice { + Product changePrice(final ProductId productId, final ProductPrice request) throws ProductNotFoundException; +} diff --git a/src/main/java/kitchenpos/core/products/application/CommandProductService.java b/src/main/java/kitchenpos/core/products/application/CommandProductService.java new file mode 100644 index 000000000..2cbfa2751 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/CommandProductService.java @@ -0,0 +1,47 @@ +package kitchenpos.core.products.application; + +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuRepository; +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import kitchenpos.core.products.tobe.domain.exception.ProductNotFoundException; +import kitchenpos.core.shared.identifier.ProductId; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CommandProductService implements AddProduct, ChangeProductPrice { + private final TobeProductRepository tobeProductRepository; + private final MenuRepository menuRepository; + + public CommandProductService( + final TobeProductRepository tobeProductRepository, + final MenuRepository menuRepository + ) { + this.tobeProductRepository = tobeProductRepository; + this.menuRepository = menuRepository; + } + + @Transactional + public kitchenpos.core.products.tobe.domain.Product addProduct(final CreateProductRequest request) { + return tobeProductRepository.save(kitchenpos.core.products.tobe.domain.Product.create( + request.id(), + request.name(), + request.price() + )); + } + + @Transactional + public Product changePrice(final ProductId productId, final ProductPrice request) { + + Product product = tobeProductRepository.findById(productId) + .map(p -> p.changePrice(request)) + .orElseThrow(() -> new ProductNotFoundException(productId)); + tobeProductRepository.save(product); + return product; + } + + +} diff --git a/src/main/java/kitchenpos/core/products/application/FindProducts.java b/src/main/java/kitchenpos/core/products/application/FindProducts.java new file mode 100644 index 000000000..61ad32f88 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/FindProducts.java @@ -0,0 +1,9 @@ +package kitchenpos.core.products.application; + +import kitchenpos.core.products.tobe.domain.Product; + +import java.util.List; + +public interface FindProducts { + List findProducts(); +} diff --git a/src/main/java/kitchenpos/core/products/application/QueryProductService.java b/src/main/java/kitchenpos/core/products/application/QueryProductService.java new file mode 100644 index 000000000..b3c4b35cc --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/QueryProductService.java @@ -0,0 +1,24 @@ +package kitchenpos.core.products.application; + +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class QueryProductService implements FindProducts { + + private final TobeProductRepository tobeProductRepository; + + public QueryProductService(final TobeProductRepository tobeProductRepository) { + this.tobeProductRepository = tobeProductRepository; + } + + @Override + @Transactional(readOnly = true) + public List findProducts() { + return tobeProductRepository.findAll(); + } +} diff --git a/src/main/java/kitchenpos/core/products/application/dto/CreateProductRequest.java b/src/main/java/kitchenpos/core/products/application/dto/CreateProductRequest.java new file mode 100644 index 000000000..64d07c503 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/application/dto/CreateProductRequest.java @@ -0,0 +1,12 @@ +package kitchenpos.core.products.application.dto; + +import kitchenpos.core.products.tobe.domain.ProductName; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.shared.identifier.ProductId; + +public record CreateProductRequest( + ProductId id, + ProductName name, + ProductPrice price +) { +} diff --git a/src/main/java/kitchenpos/products/domain/JpaProductRepository.java b/src/main/java/kitchenpos/core/products/domain/JpaProductRepository.java similarity index 81% rename from src/main/java/kitchenpos/products/domain/JpaProductRepository.java rename to src/main/java/kitchenpos/core/products/domain/JpaProductRepository.java index 90b069779..990737430 100644 --- a/src/main/java/kitchenpos/products/domain/JpaProductRepository.java +++ b/src/main/java/kitchenpos/core/products/domain/JpaProductRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.core.products.domain; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/kitchenpos/products/domain/Product.java b/src/main/java/kitchenpos/core/products/domain/Product.java similarity index 95% rename from src/main/java/kitchenpos/products/domain/Product.java rename to src/main/java/kitchenpos/core/products/domain/Product.java index ee2a7dfa9..91a1700c7 100644 --- a/src/main/java/kitchenpos/products/domain/Product.java +++ b/src/main/java/kitchenpos/core/products/domain/Product.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.core.products.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/core/products/domain/ProductRepository.java similarity index 86% rename from src/main/java/kitchenpos/products/domain/ProductRepository.java rename to src/main/java/kitchenpos/core/products/domain/ProductRepository.java index 3637e4232..23c999d94 100644 --- a/src/main/java/kitchenpos/products/domain/ProductRepository.java +++ b/src/main/java/kitchenpos/core/products/domain/ProductRepository.java @@ -1,4 +1,4 @@ -package kitchenpos.products.domain; +package kitchenpos.core.products.domain; import java.util.List; import java.util.Optional; diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/Product.java b/src/main/java/kitchenpos/core/products/tobe/domain/Product.java new file mode 100644 index 000000000..cb5353045 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/Product.java @@ -0,0 +1,65 @@ +package kitchenpos.core.products.tobe.domain; + +import jakarta.persistence.*; +import kitchenpos.core.shared.domain.AggregateRoot; +import kitchenpos.core.shared.event.ProductPriceChangedEvent; +import kitchenpos.core.shared.identifier.ProductId; + +import java.util.Collection; +import java.util.Objects; + +@Table(name = "product_tobe") +@Entity(name = "ProductTobe") +public class Product extends AggregateRoot { + + @EmbeddedId + @AttributeOverride(name = "value", column = @Column(name = "id", nullable = false, columnDefinition = "binary(16)")) + private ProductId id; + + @Embedded + @AttributeOverride(name = "name", column = @Column(name = "name", nullable = false)) + private ProductName name; + + @Embedded + @AttributeOverride(name = "price", column = @Column(name = "price", nullable = false, columnDefinition = "decimal(19,2)")) + private ProductPrice price; + + @SuppressWarnings("unused") + protected Product() {} + + private Product(ProductId id, ProductName name, ProductPrice price) { + this.id = Objects.requireNonNull(id, "id는 null이 될 수 없습니다."); + this.name = Objects.requireNonNull(name, "name은 null이 될 수 없습니다."); + this.price = Objects.requireNonNull(price, "price은 null이 될 수 없습니다."); + } + + public static Product create(final ProductId id, final ProductName name, final ProductPrice price) { + return new Product(id, name, price); + } + + /** + * getter + */ + @Override + public ProductId getId() { + return id; + } + + public ProductName getName() { return name; } + + public ProductPrice getPrice() { return price; } + + public Product changePrice(final ProductPrice newPrice) { + Objects.requireNonNull(newPrice, "변경할 금액은 null이 될 수 없습니다."); + ProductPriceChangedEvent productPriceChangedEvent = new ProductPriceChangedEvent(this.id, this.price, newPrice); + this.price = newPrice; + + registerEvent(productPriceChangedEvent); + return this; + } + + @Override + public Collection domainEvents() { + return super.domainEvents(); + } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/ProductIdGenerator.java b/src/main/java/kitchenpos/core/products/tobe/domain/ProductIdGenerator.java new file mode 100644 index 000000000..ed0865a5f --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/ProductIdGenerator.java @@ -0,0 +1,7 @@ +package kitchenpos.core.products.tobe.domain; + +import kitchenpos.core.shared.identifier.ProductId; + +public interface ProductIdGenerator { + ProductId generateId(); +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/ProductName.java b/src/main/java/kitchenpos/core/products/tobe/domain/ProductName.java new file mode 100644 index 000000000..0eb28bbfb --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/ProductName.java @@ -0,0 +1,40 @@ +package kitchenpos.core.products.tobe.domain; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Transient; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductNameException; +import kitchenpos.core.products.tobe.domain.support.ProductNameValidationResult; +import kitchenpos.core.shared.domain.ProfanityChecker; +import kitchenpos.core.shared.domain.ValueObject; +import org.jetbrains.annotations.NotNull; + +@Embeddable +public class ProductName extends ValueObject { + + private String name; + + @SuppressWarnings("unused") + protected ProductName() {} + + private ProductName(String name) { + this.name = name; + } + + public static ProductName create(@NotNull ProductNamePolicy policy, String name) { + ProductNameValidationResult result = policy.validate(name); + if (!result.valid()) { + throw new InvalidProductNameException((String.join("; ", result.errorMessages()))); + } + return new ProductName(name.strip()); + } + + public String getName() { + return name; + } + + @Override + @Transient + protected Object[] getEqualityFields() { + return new Object[] { name }; + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/ProductNamePolicy.java b/src/main/java/kitchenpos/core/products/tobe/domain/ProductNamePolicy.java new file mode 100644 index 000000000..2cceacd34 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/ProductNamePolicy.java @@ -0,0 +1,7 @@ +package kitchenpos.core.products.tobe.domain; + +import kitchenpos.core.products.tobe.domain.support.ProductNameValidationResult; + +public interface ProductNamePolicy { + ProductNameValidationResult validate(String name); +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/ProductPrice.java b/src/main/java/kitchenpos/core/products/tobe/domain/ProductPrice.java new file mode 100644 index 000000000..034802a8c --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/ProductPrice.java @@ -0,0 +1,47 @@ +package kitchenpos.core.products.tobe.domain; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Transient; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductPriceException; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.domain.ValueObject; +import kitchenpos.core.shared.value.Quantity; + +import java.math.BigDecimal; + +@Embeddable +public class ProductPrice extends ValueObject { + private Money price; + + @SuppressWarnings("unused") + protected ProductPrice() {} + + private ProductPrice(Money price) { + if (price == null || price.isLessThan(Money.ZERO)) { + throw new InvalidProductPriceException("상품 가격은 null 이거나 0보다 작을 수 없습니다."); + } + + this.price = price; + } + + public static ProductPrice of(Money price) { + return new ProductPrice(price); + } + + public Money getPrice() { + return price; + } + + //quantity 곱하는 로직 + + + @Override + @Transient + protected Object[] getEqualityFields() { + return new Object[] { price }; + } + + public Money multiply(Quantity quantity) { + return price.multiply(quantity.getValue()); + } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/TobeProductRepository.java b/src/main/java/kitchenpos/core/products/tobe/domain/TobeProductRepository.java new file mode 100644 index 000000000..752037690 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/TobeProductRepository.java @@ -0,0 +1,20 @@ +package kitchenpos.core.products.tobe.domain; + +import kitchenpos.core.shared.identifier.ProductId; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Transactional(readOnly = true) +public interface TobeProductRepository { + Product save(Product product); + + Optional findById(ProductId id); + + List findAll(); + + List findAllByIdIn(List ids); +} + diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductIdException.java b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductIdException.java new file mode 100644 index 000000000..21fc4e487 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductIdException.java @@ -0,0 +1,5 @@ +package kitchenpos.core.products.tobe.domain.exception; + +public class InvalidProductIdException extends ProductException { + public InvalidProductIdException(String s) { super(s); } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductNameException.java b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductNameException.java new file mode 100644 index 000000000..04113090d --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductNameException.java @@ -0,0 +1,7 @@ +package kitchenpos.core.products.tobe.domain.exception; + +public class InvalidProductNameException extends ProductException { + public InvalidProductNameException(String s) { + super(s); + } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductPriceException.java b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductPriceException.java new file mode 100644 index 000000000..a784344d5 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/exception/InvalidProductPriceException.java @@ -0,0 +1,7 @@ +package kitchenpos.core.products.tobe.domain.exception; + +public class InvalidProductPriceException extends ProductException { + public InvalidProductPriceException(String s) { + super(s); + } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductException.java b/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductException.java new file mode 100644 index 000000000..33f889882 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductException.java @@ -0,0 +1,14 @@ +package kitchenpos.core.products.tobe.domain.exception; + +import kitchenpos.core.foundation.exception.SystemException; + +public class ProductException extends SystemException { + public ProductException(String format, Object... args) { + super(format, args); + } + + public ProductException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductNotFoundException.java b/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductNotFoundException.java new file mode 100644 index 000000000..98a731c80 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/exception/ProductNotFoundException.java @@ -0,0 +1,17 @@ +package kitchenpos.core.products.tobe.domain.exception; + +import kitchenpos.core.shared.identifier.ProductId; + +public class ProductNotFoundException extends ProductException { + + private final ProductId id; + + public ProductNotFoundException(ProductId id) { + super("해당 id를 가진 Product를 찾을 수 없습니다. id: " + id.getValue()); + this.id = id; + } + + public ProductId getId() { + return id; + } +} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/support/DefaultProductNamePolicy.java b/src/main/java/kitchenpos/core/products/tobe/domain/support/DefaultProductNamePolicy.java new file mode 100644 index 000000000..2c56cfa7a --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/support/DefaultProductNamePolicy.java @@ -0,0 +1,30 @@ +package kitchenpos.core.products.tobe.domain.support; + +import kitchenpos.core.products.tobe.domain.ProductNamePolicy; +import kitchenpos.core.shared.domain.ProfanityChecker; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class DefaultProductNamePolicy implements ProductNamePolicy { + + private final ProfanityChecker profanityChecker; + + public DefaultProductNamePolicy(ProfanityChecker profanityChecker) { + this.profanityChecker = profanityChecker; + } + + @Override + public ProductNameValidationResult validate(String name) { + List errors = new ArrayList<>(); + if (name == null || name.isBlank()) { + errors.add("Product name은 null 이거나 빈 값이 될 수 없습니다."); + } + if (name != null && profanityChecker.containsProfanity(name)) { + errors.add("Product name에 비속어가 포함될 수 없습니다."); + } + return new ProductNameValidationResult(errors.isEmpty(), errors); + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/support/ProductNameValidationResult.java b/src/main/java/kitchenpos/core/products/tobe/domain/support/ProductNameValidationResult.java new file mode 100644 index 000000000..a2b20be4d --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/support/ProductNameValidationResult.java @@ -0,0 +1,5 @@ +package kitchenpos.core.products.tobe.domain.support; + +import java.util.List; + +public record ProductNameValidationResult(boolean valid, List errorMessages) {} diff --git a/src/main/java/kitchenpos/core/products/tobe/domain/support/UUIDBasedProductIdGenerator.java b/src/main/java/kitchenpos/core/products/tobe/domain/support/UUIDBasedProductIdGenerator.java new file mode 100644 index 000000000..8b8e33412 --- /dev/null +++ b/src/main/java/kitchenpos/core/products/tobe/domain/support/UUIDBasedProductIdGenerator.java @@ -0,0 +1,15 @@ +package kitchenpos.core.products.tobe.domain.support; + +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.products.tobe.domain.ProductIdGenerator; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class UUIDBasedProductIdGenerator implements ProductIdGenerator { + @Override + public ProductId generateId() { + return ProductId.of(UUID.randomUUID()); + } +} diff --git a/src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java b/src/main/java/kitchenpos/core/shared/data/DefaultProfanityChecker.java similarity index 77% rename from src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java rename to src/main/java/kitchenpos/core/shared/data/DefaultProfanityChecker.java index 87dba885c..e1df5dd07 100644 --- a/src/main/java/kitchenpos/products/infra/DefaultPurgomalumClient.java +++ b/src/main/java/kitchenpos/core/shared/data/DefaultProfanityChecker.java @@ -1,5 +1,6 @@ -package kitchenpos.products.infra; +package kitchenpos.core.shared.data; +import kitchenpos.core.shared.domain.ProfanityChecker; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -8,10 +9,10 @@ import java.net.URI; @Component -public class DefaultPurgomalumClient implements PurgomalumClient { +public class DefaultProfanityChecker implements ProfanityChecker { private final RestTemplate restTemplate; - public DefaultPurgomalumClient(final RestTemplateBuilder restTemplateBuilder) { + public DefaultProfanityChecker(final RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder.build(); } diff --git a/src/main/java/kitchenpos/core/shared/data/jpa/MoneyConverter.java b/src/main/java/kitchenpos/core/shared/data/jpa/MoneyConverter.java new file mode 100644 index 000000000..7c5b1e0ee --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/data/jpa/MoneyConverter.java @@ -0,0 +1,20 @@ +package kitchenpos.core.shared.data.jpa; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import kitchenpos.core.shared.value.Money; + +import java.math.BigDecimal; + +@Converter(autoApply = true) +public class MoneyConverter implements AttributeConverter { + @Override + public BigDecimal convertToDatabaseColumn(Money money) { + return money.getAmount(); + } + + @Override + public Money convertToEntityAttribute(BigDecimal amount) { + return Money.wons(amount); + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/shared/data/jpa/QuantityConverter.java b/src/main/java/kitchenpos/core/shared/data/jpa/QuantityConverter.java new file mode 100644 index 000000000..85a04e685 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/data/jpa/QuantityConverter.java @@ -0,0 +1,24 @@ +package kitchenpos.core.shared.data.jpa; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; + +import java.math.BigDecimal; + +@Converter(autoApply = true) +public class QuantityConverter implements AttributeConverter { + @Override + public Long convertToDatabaseColumn(Quantity quantity) { + return quantity.getValue(); + } + + @Override + public Quantity convertToEntityAttribute(Long quantity) { + if(quantity == null) { + return Quantity.ZERO; + } + return Quantity.of(quantity); + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/shared/domain/AggregateRoot.java b/src/main/java/kitchenpos/core/shared/domain/AggregateRoot.java new file mode 100644 index 000000000..56a697de7 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/domain/AggregateRoot.java @@ -0,0 +1,34 @@ +package kitchenpos.core.shared.domain; + +import jakarta.persistence.Transient; +import org.springframework.data.domain.AfterDomainEventPublication; +import org.springframework.data.domain.DomainEvents; +import org.springframework.util.Assert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public abstract class AggregateRoot, TID> extends DomainEntity { + @Transient + private final transient List domainEvents = new ArrayList(); + + public AggregateRoot() { + } + + protected void registerEvent(T event) { + Assert.notNull(event, "Domain event must not be null"); + this.domainEvents.add(event); + } + + @AfterDomainEventPublication + protected void clearDomainEvents() { + this.domainEvents.clear(); + } + + @DomainEvents + protected Collection domainEvents() { + return Collections.unmodifiableList(this.domainEvents); + } +} diff --git a/src/main/java/kitchenpos/core/shared/domain/DomainEntity.java b/src/main/java/kitchenpos/core/shared/domain/DomainEntity.java new file mode 100644 index 000000000..a52614e62 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/domain/DomainEntity.java @@ -0,0 +1,37 @@ +package kitchenpos.core.shared.domain; + +import java.io.Serializable; + +public abstract class DomainEntity, TID> implements Serializable { + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + return equals((T)other); + } + + public boolean equals(T other) { + if (other == null) { + return false; + } + + if (getId() == null) { + return false; + } + + if (other.getClass().equals(getClass())) { + return getId().equals(other.getId()); + } + + return super.equals(other); + } + + @Override + public int hashCode() { + return getId() == null ? 0 : getId().hashCode(); + } + + abstract public TID getId(); +} diff --git a/src/main/java/kitchenpos/core/shared/domain/ProfanityChecker.java b/src/main/java/kitchenpos/core/shared/domain/ProfanityChecker.java new file mode 100644 index 000000000..485154724 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/domain/ProfanityChecker.java @@ -0,0 +1,5 @@ +package kitchenpos.core.shared.domain; + +public interface ProfanityChecker { + boolean containsProfanity(String text); +} diff --git a/src/main/java/kitchenpos/core/shared/domain/ValueObject.java b/src/main/java/kitchenpos/core/shared/domain/ValueObject.java new file mode 100644 index 000000000..ab7961d6d --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/domain/ValueObject.java @@ -0,0 +1,49 @@ +package kitchenpos.core.shared.domain; + +import java.io.Serializable; +import java.util.Arrays; + +public abstract class ValueObject> implements Serializable { + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (!(other.getClass().equals(getClass()))) { + return false; + } + + return equals((T)other); + } + + public boolean equals(T other) { + if (other == null) { + return false; + } + + return Arrays.equals(getEqualityFields(), other.getEqualityFields()); + } + + @Override + public int hashCode() { + int hash = 17; + for(Object each : getEqualityFields()) { + hash = hash * 31 + (each == null ? 0 : each.hashCode()); + } + return hash; + } + + protected Object[] getEqualityFields() { + return Arrays.stream(getClass().getDeclaredFields()) + .map(field -> { + try { + field.setAccessible(true); + return field.get(this); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .toArray(); + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/shared/event/ProductPriceChangedEvent.java b/src/main/java/kitchenpos/core/shared/event/ProductPriceChangedEvent.java new file mode 100644 index 000000000..bad9333e4 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/event/ProductPriceChangedEvent.java @@ -0,0 +1,7 @@ +package kitchenpos.core.shared.event; + +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.shared.identifier.ProductId; + +public record ProductPriceChangedEvent(ProductId id, ProductPrice oldPrice, ProductPrice newPrice) { +} diff --git a/src/main/java/kitchenpos/core/shared/identifier/ProductId.java b/src/main/java/kitchenpos/core/shared/identifier/ProductId.java new file mode 100644 index 000000000..d36a082a9 --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/identifier/ProductId.java @@ -0,0 +1,39 @@ +package kitchenpos.core.shared.identifier; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Transient; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductIdException; +import kitchenpos.core.shared.domain.ValueObject; + +import java.io.Serializable; +import java.util.UUID; + +@Embeddable +public class ProductId extends ValueObject { + + private UUID value; + + @SuppressWarnings("unused") + protected ProductId() {} + + private ProductId(UUID value) { + if (value == null) { + throw new InvalidProductIdException("Product ID 는 null 이거나 빈 값이 될 수 없습니다."); + } + this.value = value; + } + + public static ProductId of(UUID value) { + return new ProductId(value); + } + + public UUID getValue() { + return value; + } + + @Override + @Transient + protected Object[] getEqualityFields() { + return new Object[] { value }; + } +} \ No newline at end of file diff --git a/src/main/java/kitchenpos/core/shared/value/Money.java b/src/main/java/kitchenpos/core/shared/value/Money.java new file mode 100644 index 000000000..47c28113a --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/value/Money.java @@ -0,0 +1,64 @@ +package kitchenpos.core.shared.value; + +import kitchenpos.core.shared.domain.ValueObject; + +import java.math.BigDecimal; + +public class Money extends ValueObject { + public static final Money ZERO = Money.wons(0L); + + private final BigDecimal amount; + + public static Money wons(long amount) { + return new Money(BigDecimal.valueOf(amount)); + } + + public static Money wons(BigDecimal amount) { + return new Money(amount); + } + + private Money(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("금액은 null이 될 수 없습니다."); + } + + if (amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("금액은 0보다 작을 수 없습니다. 입력된 금액: " + amount); + } + this.amount = amount; + } + + public Money add(Money other) { + return new Money(this.amount.add(other.amount)); + } + + public boolean isLessThan(Money other) { + return this.amount.compareTo(other.amount) < 0; + } + + public BigDecimal getAmount() { + return amount; + } + + @Override + protected Object[] getEqualityFields() { + return new Object[] { amount.doubleValue() }; + } + + @Override + public String toString() { + return amount.toString() + "원"; + } + + public Money multiply(long value) { + return new Money(amount.multiply(BigDecimal.valueOf(value))); + } + + public boolean isBiggerThan(Money other) { + return this.amount.compareTo(other.amount) > 0; + } + + public boolean isEqual(Money other) { + return this.amount.compareTo(other.amount) == 0; + } +} diff --git a/src/main/java/kitchenpos/core/shared/value/Quantity.java b/src/main/java/kitchenpos/core/shared/value/Quantity.java new file mode 100644 index 000000000..cb8dd2b4b --- /dev/null +++ b/src/main/java/kitchenpos/core/shared/value/Quantity.java @@ -0,0 +1,33 @@ +package kitchenpos.core.shared.value; + +import kitchenpos.core.shared.domain.ValueObject; + +public class Quantity extends ValueObject { + public static final Quantity ZERO = Quantity.of(0); + + private final Long value; + + private Quantity(long value) { + if (value < 0) { + throw new IllegalArgumentException("수량은 음수가 될 수 없습니다."); + } + this.value = value; + } + + public static Quantity of(long value) { + return new Quantity(value); + } + + public long getValue() { + return value; + } + + @Override + protected Object[] getEqualityFields() { + return new Object[]{value}; + } + @Override + public String toString() { + return value + " 개"; + } +} diff --git a/src/main/java/kitchenpos/takeoutorders/empty.txt b/src/main/java/kitchenpos/core/takeoutorders/empty.txt similarity index 100% rename from src/main/java/kitchenpos/takeoutorders/empty.txt rename to src/main/java/kitchenpos/core/takeoutorders/empty.txt diff --git a/src/main/java/kitchenpos/deliveryorders/infra/DefaultKitchenridersClient.java b/src/main/java/kitchenpos/data/DefaultKitchenridersClient.java similarity index 88% rename from src/main/java/kitchenpos/deliveryorders/infra/DefaultKitchenridersClient.java rename to src/main/java/kitchenpos/data/DefaultKitchenridersClient.java index 31d6d8cee..bd7a74bd4 100644 --- a/src/main/java/kitchenpos/deliveryorders/infra/DefaultKitchenridersClient.java +++ b/src/main/java/kitchenpos/data/DefaultKitchenridersClient.java @@ -1,4 +1,4 @@ -package kitchenpos.deliveryorders.infra; +package kitchenpos.data; import org.springframework.stereotype.Component; diff --git a/src/main/java/kitchenpos/deliveryorders/infra/KitchenridersClient.java b/src/main/java/kitchenpos/data/KitchenridersClient.java similarity index 81% rename from src/main/java/kitchenpos/deliveryorders/infra/KitchenridersClient.java rename to src/main/java/kitchenpos/data/KitchenridersClient.java index 0c8278791..cd61969f9 100644 --- a/src/main/java/kitchenpos/deliveryorders/infra/KitchenridersClient.java +++ b/src/main/java/kitchenpos/data/KitchenridersClient.java @@ -1,4 +1,4 @@ -package kitchenpos.deliveryorders.infra; +package kitchenpos.data; import java.math.BigDecimal; import java.util.UUID; diff --git a/src/main/java/kitchenpos/data/flyway/FlywayConfig.java b/src/main/java/kitchenpos/data/flyway/FlywayConfig.java new file mode 100644 index 000000000..ffe727623 --- /dev/null +++ b/src/main/java/kitchenpos/data/flyway/FlywayConfig.java @@ -0,0 +1,16 @@ +package kitchenpos.data.flyway; + +import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FlywayConfig { + @Bean + public FlywayMigrationStrategy cleanMigrateStrategy() { + return flyway -> { + flyway.repair(); + flyway.migrate(); + }; + } +} diff --git a/src/main/java/kitchenpos/data/jpa/TobeJpaProductRepository.java b/src/main/java/kitchenpos/data/jpa/TobeJpaProductRepository.java new file mode 100644 index 000000000..e15cc125e --- /dev/null +++ b/src/main/java/kitchenpos/data/jpa/TobeJpaProductRepository.java @@ -0,0 +1,9 @@ +package kitchenpos.data.jpa; + +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +interface TobeJpaProductRepository extends TobeProductRepository, JpaRepository { +} diff --git a/src/main/java/kitchenpos/products/application/ProductService.java b/src/main/java/kitchenpos/products/application/ProductService.java deleted file mode 100644 index 20cf63996..000000000 --- a/src/main/java/kitchenpos/products/application/ProductService.java +++ /dev/null @@ -1,81 +0,0 @@ -package kitchenpos.products.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 org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.math.BigDecimal; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.UUID; - -@Service -public class ProductService { - private final ProductRepository productRepository; - private final MenuRepository menuRepository; - private final PurgomalumClient purgomalumClient; - - public ProductService( - final ProductRepository productRepository, - final MenuRepository menuRepository, - final PurgomalumClient purgomalumClient - ) { - this.productRepository = productRepository; - this.menuRepository = menuRepository; - this.purgomalumClient = purgomalumClient; - } - - @Transactional - public Product create(final Product request) { - final BigDecimal price = request.getPrice(); - if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException(); - } - final String name = request.getName(); - if (Objects.isNull(name) || purgomalumClient.containsProfanity(name)) { - throw new IllegalArgumentException(); - } - final Product product = new Product(); - product.setId(UUID.randomUUID()); - product.setName(name); - product.setPrice(price); - return productRepository.save(product); - } - - @Transactional - public Product changePrice(final UUID productId, final Product request) { - final BigDecimal price = request.getPrice(); - if (Objects.isNull(price) || price.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException(); - } - final Product product = productRepository.findById(productId) - .orElseThrow(NoSuchElementException::new); - product.setPrice(price); - 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 product; - } - - @Transactional(readOnly = true) - public List findAll() { - return productRepository.findAll(); - } -} diff --git a/src/main/java/kitchenpos/products/infra/PurgomalumClient.java b/src/main/java/kitchenpos/products/infra/PurgomalumClient.java deleted file mode 100644 index 4002a2bb8..000000000 --- a/src/main/java/kitchenpos/products/infra/PurgomalumClient.java +++ /dev/null @@ -1,5 +0,0 @@ -package kitchenpos.products.infra; - -public interface PurgomalumClient { - boolean containsProfanity(String text); -} diff --git a/src/main/java/kitchenpos/products/ui/ProductRestController.java b/src/main/java/kitchenpos/products/ui/ProductRestController.java deleted file mode 100644 index c71c795a4..000000000 --- a/src/main/java/kitchenpos/products/ui/ProductRestController.java +++ /dev/null @@ -1,43 +0,0 @@ -package kitchenpos.products.ui; - -import kitchenpos.products.application.ProductService; -import kitchenpos.products.domain.Product; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import java.net.URI; -import java.util.List; -import java.util.UUID; - -@RequestMapping("/api/products") -@RestController -public class ProductRestController { - private final ProductService productService; - - public ProductRestController(final ProductService productService) { - this.productService = productService; - } - - @PostMapping - public ResponseEntity create(@RequestBody final Product request) { - final Product 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) { - return ResponseEntity.ok(productService.changePrice(productId, request)); - } - - @GetMapping - public ResponseEntity> findAll() { - return ResponseEntity.ok(productService.findAll()); - } -} diff --git a/src/main/java/kitchenpos/menus/ui/MenuGroupRestController.java b/src/main/java/kitchenpos/web/MenuGroupRestController.java similarity index 89% rename from src/main/java/kitchenpos/menus/ui/MenuGroupRestController.java rename to src/main/java/kitchenpos/web/MenuGroupRestController.java index 30c38b4a1..6b4387a0b 100644 --- a/src/main/java/kitchenpos/menus/ui/MenuGroupRestController.java +++ b/src/main/java/kitchenpos/web/MenuGroupRestController.java @@ -1,7 +1,7 @@ -package kitchenpos.menus.ui; +package kitchenpos.web; -import kitchenpos.menus.application.MenuGroupService; -import kitchenpos.menus.domain.MenuGroup; +import kitchenpos.core.menus.application.MenuGroupService; +import kitchenpos.core.menus.domain.MenuGroup; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; diff --git a/src/main/java/kitchenpos/menus/ui/MenuRestController.java b/src/main/java/kitchenpos/web/MenuRestController.java similarity index 93% rename from src/main/java/kitchenpos/menus/ui/MenuRestController.java rename to src/main/java/kitchenpos/web/MenuRestController.java index 626b214fc..0fab1ddf0 100644 --- a/src/main/java/kitchenpos/menus/ui/MenuRestController.java +++ b/src/main/java/kitchenpos/web/MenuRestController.java @@ -1,7 +1,7 @@ -package kitchenpos.menus.ui; +package kitchenpos.web; -import kitchenpos.menus.application.MenuService; -import kitchenpos.menus.domain.Menu; +import kitchenpos.core.menus.application.MenuService; +import kitchenpos.core.menus.domain.Menu; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/src/main/java/kitchenpos/eatinorders/ui/OrderRestController.java b/src/main/java/kitchenpos/web/OrderRestController.java similarity index 94% rename from src/main/java/kitchenpos/eatinorders/ui/OrderRestController.java rename to src/main/java/kitchenpos/web/OrderRestController.java index dbb5568ad..5dce78a22 100644 --- a/src/main/java/kitchenpos/eatinorders/ui/OrderRestController.java +++ b/src/main/java/kitchenpos/web/OrderRestController.java @@ -1,7 +1,7 @@ -package kitchenpos.eatinorders.ui; +package kitchenpos.web; -import kitchenpos.eatinorders.application.OrderService; -import kitchenpos.eatinorders.domain.Order; +import kitchenpos.core.eatinorders.application.OrderService; +import kitchenpos.core.eatinorders.domain.Order; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/src/main/java/kitchenpos/eatinorders/ui/OrderTableRestController.java b/src/main/java/kitchenpos/web/OrderTableRestController.java similarity index 93% rename from src/main/java/kitchenpos/eatinorders/ui/OrderTableRestController.java rename to src/main/java/kitchenpos/web/OrderTableRestController.java index 1ceabe86f..af9f72089 100644 --- a/src/main/java/kitchenpos/eatinorders/ui/OrderTableRestController.java +++ b/src/main/java/kitchenpos/web/OrderTableRestController.java @@ -1,7 +1,7 @@ -package kitchenpos.eatinorders.ui; +package kitchenpos.web; -import kitchenpos.eatinorders.application.OrderTableService; -import kitchenpos.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.application.OrderTableService; +import kitchenpos.core.eatinorders.domain.OrderTable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/src/main/java/kitchenpos/web/ProductRestController.java b/src/main/java/kitchenpos/web/ProductRestController.java new file mode 100644 index 000000000..b6e395f1c --- /dev/null +++ b/src/main/java/kitchenpos/web/ProductRestController.java @@ -0,0 +1,51 @@ +package kitchenpos.web; + +import kitchenpos.core.products.application.*; +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.ProductPrice; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.shared.value.Money; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.net.URI; +import java.util.List; +import java.util.UUID; + +@RequestMapping("/v1/api/products") +@RestController +public class ProductRestController { + private final AddProduct addProduct; + private final ChangeProductPrice changeProductPrice; + private final FindProducts findProducts; + + public ProductRestController(AddProduct addProduct, ChangeProductPrice changeProductPrice, FindProducts findProducts) { + this.addProduct = addProduct; + this.changeProductPrice = changeProductPrice; + this.findProducts = findProducts; + } + + @PostMapping + public ResponseEntity create(@RequestBody final CreateProductRequest request) { + kitchenpos.core.products.tobe.domain.Product response = addProduct.addProduct(request); + return ResponseEntity.created(URI.create("/api/products/" + response.getId())) + .body(response); + } + + @PutMapping("/{productId}/price") + public ResponseEntity changePrice(@PathVariable final UUID productId, @RequestBody final Long amount) { + return ResponseEntity.ok(changeProductPrice.changePrice(ProductId.of(productId), ProductPrice.of(Money.wons(amount)))); + } + + @GetMapping + public ResponseEntity> findAll() { + return ResponseEntity.ok(findProducts.findProducts()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da61c4529..4f58b8945 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,3 +6,8 @@ spring.jpa.hibernate.ddl-auto=validate spring.jpa.properties.hibernate.format_sql=true spring.jpa.show-sql=true logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + + +# ?? db connection ?? +spring.datasource.hikari.auto-commit=false +spring.jpa.properties.hibernate.connection.provider_disables_autocommit=true \ No newline at end of file diff --git a/src/main/resources/db/migration/V3__temp_product_tobe.sql b/src/main/resources/db/migration/V3__temp_product_tobe.sql new file mode 100644 index 000000000..0c20cc74b --- /dev/null +++ b/src/main/resources/db/migration/V3__temp_product_tobe.sql @@ -0,0 +1,9 @@ +create table product_tobe +( + id binary(16) not null + primary key, + name varchar(255) not null, + price decimal(19, 2) not null +); + + diff --git a/src/main/resources/db/migration/V4__Insert_product_tobe.sql b/src/main/resources/db/migration/V4__Insert_product_tobe.sql new file mode 100644 index 000000000..0725d14ef --- /dev/null +++ b/src/main/resources/db/migration/V4__Insert_product_tobe.sql @@ -0,0 +1,12 @@ +insert into product_tobe (id, name, price) +values (x'3b52824434f7406bbb7e690912f66b10', '후라이드', 16000); +insert into product_tobe (id, name, price) +values (x'c5ee925c3dbb4941b825021446f24446', '양념치킨', 16000); +insert into product_tobe (id, name, price) +values (x'625c6fc4145d408f8dd533c16ba26064', '반반치킨', 16000); +insert into product_tobe (id, name, price) +values (x'4721ee722ff3417fade3acd0a804605b', '통구이', 16000); +insert into product_tobe (id, name, price) +values (x'0ac16db71b024a87b9c1e7d8f226c48d', '간장치킨', 17000); +insert into product_tobe (id, name, price) +values (x'7de4b8affa0f4391aaa9c61ea9b40f83', '순살치킨', 17000); diff --git a/src/test/java/kitchenpos/config/UnitTest.java b/src/test/java/kitchenpos/config/UnitTest.java new file mode 100644 index 000000000..c915598c5 --- /dev/null +++ b/src/test/java/kitchenpos/config/UnitTest.java @@ -0,0 +1,15 @@ +package kitchenpos.config; + +import org.junit.jupiter.api.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Tag("unitTest") +@Target({ElementType.TYPE}) +@Retention(RUNTIME) +public @interface UnitTest { +} diff --git a/src/test/java/kitchenpos/eatinorders/application/FakeKitchenridersClient.java b/src/test/java/kitchenpos/core/eatinorders/application/FakeKitchenridersClient.java similarity index 86% rename from src/test/java/kitchenpos/eatinorders/application/FakeKitchenridersClient.java rename to src/test/java/kitchenpos/core/eatinorders/application/FakeKitchenridersClient.java index 301e1377b..3441cc5b8 100644 --- a/src/test/java/kitchenpos/eatinorders/application/FakeKitchenridersClient.java +++ b/src/test/java/kitchenpos/core/eatinorders/application/FakeKitchenridersClient.java @@ -1,6 +1,6 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.deliveryorders.infra.KitchenridersClient; +import kitchenpos.data.KitchenridersClient; import java.math.BigDecimal; import java.util.UUID; diff --git a/src/test/java/kitchenpos/eatinorders/application/InMemoryOrderRepository.java b/src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderRepository.java similarity index 77% rename from src/test/java/kitchenpos/eatinorders/application/InMemoryOrderRepository.java rename to src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderRepository.java index 85f27e38d..6d4413786 100644 --- a/src/test/java/kitchenpos/eatinorders/application/InMemoryOrderRepository.java +++ b/src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderRepository.java @@ -1,9 +1,9 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.eatinorders.domain.Order; -import kitchenpos.eatinorders.domain.OrderRepository; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.Order; +import kitchenpos.core.eatinorders.domain.OrderRepository; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/test/java/kitchenpos/eatinorders/application/InMemoryOrderTableRepository.java b/src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderTableRepository.java similarity index 81% rename from src/test/java/kitchenpos/eatinorders/application/InMemoryOrderTableRepository.java rename to src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderTableRepository.java index 663de6289..ad0bec799 100644 --- a/src/test/java/kitchenpos/eatinorders/application/InMemoryOrderTableRepository.java +++ b/src/test/java/kitchenpos/core/eatinorders/application/InMemoryOrderTableRepository.java @@ -1,7 +1,7 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderTableRepository; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderTableRepository; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/test/java/kitchenpos/eatinorders/application/OrderServiceTest.java b/src/test/java/kitchenpos/core/eatinorders/application/OrderServiceTest.java similarity index 95% rename from src/test/java/kitchenpos/eatinorders/application/OrderServiceTest.java rename to src/test/java/kitchenpos/core/eatinorders/application/OrderServiceTest.java index 8701fcaef..4313528b0 100644 --- a/src/test/java/kitchenpos/eatinorders/application/OrderServiceTest.java +++ b/src/test/java/kitchenpos/core/eatinorders/application/OrderServiceTest.java @@ -1,14 +1,14 @@ -package kitchenpos.eatinorders.application; - -import kitchenpos.eatinorders.domain.Order; -import kitchenpos.eatinorders.domain.OrderLineItem; -import kitchenpos.eatinorders.domain.OrderRepository; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderTableRepository; -import kitchenpos.eatinorders.domain.OrderType; -import kitchenpos.menus.application.InMemoryMenuRepository; -import kitchenpos.menus.domain.MenuRepository; +package kitchenpos.core.eatinorders.application; + +import kitchenpos.core.eatinorders.domain.Order; +import kitchenpos.core.eatinorders.domain.OrderLineItem; +import kitchenpos.core.eatinorders.domain.OrderRepository; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderTableRepository; +import kitchenpos.core.eatinorders.domain.OrderType; +import kitchenpos.core.menus.application.InMemoryMenuRepository; +import kitchenpos.core.menus.domain.MenuRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -27,11 +27,11 @@ import java.util.Random; import java.util.UUID; -import static kitchenpos.Fixtures.INVALID_ID; -import static kitchenpos.Fixtures.menu; -import static kitchenpos.Fixtures.menuProduct; -import static kitchenpos.Fixtures.order; -import static kitchenpos.Fixtures.orderTable; +import static kitchenpos.fixture.Fixtures.INVALID_ID; +import static kitchenpos.fixture.Fixtures.menu; +import static kitchenpos.fixture.Fixtures.menuProduct; +import static kitchenpos.fixture.Fixtures.order; +import static kitchenpos.fixture.Fixtures.orderTable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; diff --git a/src/test/java/kitchenpos/eatinorders/application/OrderTableServiceTest.java b/src/test/java/kitchenpos/core/eatinorders/application/OrderTableServiceTest.java similarity index 93% rename from src/test/java/kitchenpos/eatinorders/application/OrderTableServiceTest.java rename to src/test/java/kitchenpos/core/eatinorders/application/OrderTableServiceTest.java index 01551a8d4..eb171308d 100644 --- a/src/test/java/kitchenpos/eatinorders/application/OrderTableServiceTest.java +++ b/src/test/java/kitchenpos/core/eatinorders/application/OrderTableServiceTest.java @@ -1,9 +1,9 @@ -package kitchenpos.eatinorders.application; +package kitchenpos.core.eatinorders.application; -import kitchenpos.eatinorders.domain.OrderRepository; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderTableRepository; +import kitchenpos.core.eatinorders.domain.OrderRepository; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderTableRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -14,8 +14,8 @@ import java.util.List; import java.util.UUID; -import static kitchenpos.Fixtures.order; -import static kitchenpos.Fixtures.orderTable; +import static kitchenpos.fixture.Fixtures.order; +import static kitchenpos.fixture.Fixtures.orderTable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; diff --git a/src/test/java/kitchenpos/menus/application/InMemoryMenuGroupRepository.java b/src/test/java/kitchenpos/core/menus/application/InMemoryMenuGroupRepository.java similarity index 82% rename from src/test/java/kitchenpos/menus/application/InMemoryMenuGroupRepository.java rename to src/test/java/kitchenpos/core/menus/application/InMemoryMenuGroupRepository.java index 85962e6dc..0f068c2cd 100644 --- a/src/test/java/kitchenpos/menus/application/InMemoryMenuGroupRepository.java +++ b/src/test/java/kitchenpos/core/menus/application/InMemoryMenuGroupRepository.java @@ -1,7 +1,7 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.MenuGroup; -import kitchenpos.menus.domain.MenuGroupRepository; +import kitchenpos.core.menus.domain.MenuGroup; +import kitchenpos.core.menus.domain.MenuGroupRepository; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/test/java/kitchenpos/menus/application/InMemoryMenuRepository.java b/src/test/java/kitchenpos/core/menus/application/InMemoryMenuRepository.java similarity index 80% rename from src/test/java/kitchenpos/menus/application/InMemoryMenuRepository.java rename to src/test/java/kitchenpos/core/menus/application/InMemoryMenuRepository.java index ca30b679c..d211631e8 100644 --- a/src/test/java/kitchenpos/menus/application/InMemoryMenuRepository.java +++ b/src/test/java/kitchenpos/core/menus/application/InMemoryMenuRepository.java @@ -1,7 +1,8 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.Menu; -import kitchenpos.menus.domain.MenuRepository; +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuRepository; +import kitchenpos.core.shared.identifier.ProductId; import java.util.ArrayList; import java.util.HashMap; @@ -38,7 +39,7 @@ public List findAllByIdIn(final List ids) { } @Override - public List findAllByProductId(final UUID productId) { + public List findAllByProductId(final ProductId productId) { return menus.values() .stream() .filter(menu -> menu.getMenuProducts().stream().anyMatch(menuProduct -> menuProduct.getProduct().getId().equals(productId))) diff --git a/src/test/java/kitchenpos/menus/application/MenuGroupServiceTest.java b/src/test/java/kitchenpos/core/menus/application/MenuGroupServiceTest.java similarity index 91% rename from src/test/java/kitchenpos/menus/application/MenuGroupServiceTest.java rename to src/test/java/kitchenpos/core/menus/application/MenuGroupServiceTest.java index a5fbc71d1..c2730d172 100644 --- a/src/test/java/kitchenpos/menus/application/MenuGroupServiceTest.java +++ b/src/test/java/kitchenpos/core/menus/application/MenuGroupServiceTest.java @@ -1,7 +1,7 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.MenuGroup; -import kitchenpos.menus.domain.MenuGroupRepository; +import kitchenpos.core.menus.domain.MenuGroup; +import kitchenpos.core.menus.domain.MenuGroupRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,7 +10,7 @@ import java.util.List; -import static kitchenpos.Fixtures.menuGroup; +import static kitchenpos.fixture.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; diff --git a/src/test/java/kitchenpos/menus/application/MenuServiceTest.java b/src/test/java/kitchenpos/core/menus/application/MenuServiceTest.java similarity index 82% rename from src/test/java/kitchenpos/menus/application/MenuServiceTest.java rename to src/test/java/kitchenpos/core/menus/application/MenuServiceTest.java index 277679118..f9fccbd05 100644 --- a/src/test/java/kitchenpos/menus/application/MenuServiceTest.java +++ b/src/test/java/kitchenpos/core/menus/application/MenuServiceTest.java @@ -1,14 +1,17 @@ -package kitchenpos.menus.application; +package kitchenpos.core.menus.application; -import kitchenpos.menus.domain.Menu; -import kitchenpos.menus.domain.MenuGroupRepository; -import kitchenpos.menus.domain.MenuProduct; -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.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuGroupRepository; +import kitchenpos.core.menus.domain.MenuProduct; +import kitchenpos.core.menus.domain.MenuRepository; +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; +import kitchenpos.core.products.application.FakeProfanityChecker; +import kitchenpos.core.products.application.InMemoryProductRepository; +import kitchenpos.core.shared.domain.ProfanityChecker; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,11 +28,11 @@ import java.util.NoSuchElementException; import java.util.UUID; -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.fixture.Fixtures.INVALID_ID; +import static kitchenpos.fixture.Fixtures.menu; +import static kitchenpos.fixture.Fixtures.menuGroup; +import static kitchenpos.fixture.Fixtures.menuProduct; +import static kitchenpos.fixture.ProductFixtures.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; @@ -37,8 +40,8 @@ class MenuServiceTest { private MenuRepository menuRepository; private MenuGroupRepository menuGroupRepository; - private ProductRepository productRepository; - private PurgomalumClient purgomalumClient; + private TobeProductRepository productRepository; + private ProfanityChecker profanityChecker; private MenuService menuService; private UUID menuGroupId; private Product product; @@ -48,8 +51,8 @@ void setUp() { menuRepository = new InMemoryMenuRepository(); menuGroupRepository = new InMemoryMenuGroupRepository(); productRepository = new InMemoryProductRepository(); - purgomalumClient = new FakePurgomalumClient(); - menuService = new MenuService(menuRepository, menuGroupRepository, productRepository, purgomalumClient); + profanityChecker = new FakeProfanityChecker(); + menuService = new MenuService(menuRepository, menuGroupRepository, productRepository, profanityChecker); menuGroupId = menuGroupRepository.save(menuGroup()).getId(); product = productRepository.save(product("후라이드", 16_000L)); } @@ -85,17 +88,16 @@ private static List menuProducts() { return Arrays.asList( null, Arguments.of(Collections.emptyList()), - Arguments.of(Arrays.asList(createMenuProductRequest(INVALID_ID, 2L))) + Arguments.of(Arrays.asList(createMenuProductRequest(ProductId.of(INVALID_ID), 2L))) ); } @DisplayName("메뉴에 속한 상품의 수량은 0개 이상이어야 한다.") @Test void createNegativeQuantity() { - final Menu expected = createMenuRequest( - "후라이드+후라이드", 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), -1L) - ); - assertThatThrownBy(() -> menuService.create(expected)) + assertThatThrownBy(() -> menuService.create(createMenuRequest( + "후라이드+후라이드", 19_000L, menuGroupId, true, createMenuProductRequest(product.getId(), -1L) + ))) .isInstanceOf(IllegalArgumentException.class); } @@ -104,10 +106,10 @@ void createNegativeQuantity() { @NullSource @ParameterizedTest void create(final BigDecimal price) { - final Menu expected = createMenuRequest( - "후라이드+후라이드", price, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) - ); - assertThatThrownBy(() -> menuService.create(expected)) + assertThatThrownBy(() -> + menuService.create(createMenuRequest( + "후라이드+후라이드", price, menuGroupId, true, createMenuProductRequest(product.getId(), 2L) + ))) .isInstanceOf(IllegalArgumentException.class); } @@ -159,8 +161,7 @@ void changePrice() { @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)) + assertThatThrownBy(() -> menuService.changePrice(menuId, changePriceRequest(price))) .isInstanceOf(IllegalArgumentException.class); } @@ -244,17 +245,17 @@ private Menu createMenuRequest( ) { final Menu menu = new Menu(); menu.setName(name); - menu.setPrice(price); + menu.setPrice(Money.wons(price)); menu.setMenuGroupId(menuGroupId); menu.setDisplayed(displayed); menu.setMenuProducts(menuProducts); return menu; } - private static MenuProduct createMenuProductRequest(final UUID productId, final long quantity) { + private static MenuProduct createMenuProductRequest(final ProductId productId, final long quantity) { final MenuProduct menuProduct = new MenuProduct(); menuProduct.setProductId(productId); - menuProduct.setQuantity(quantity); + menuProduct.setQuantity(Quantity.of(quantity)); return menuProduct; } @@ -264,7 +265,7 @@ private Menu changePriceRequest(final long price) { private Menu changePriceRequest(final BigDecimal price) { final Menu menu = new Menu(); - menu.setPrice(price); + menu.setPrice(Money.wons(price)); return menu; } } diff --git a/src/test/java/kitchenpos/core/products/application/CommandProductServiceTest.java b/src/test/java/kitchenpos/core/products/application/CommandProductServiceTest.java new file mode 100644 index 000000000..8da20c8e4 --- /dev/null +++ b/src/test/java/kitchenpos/core/products/application/CommandProductServiceTest.java @@ -0,0 +1,118 @@ +package kitchenpos.core.products.application; + +import kitchenpos.config.UnitTest; +import kitchenpos.core.shared.event.ProductPriceChangedEvent; +import kitchenpos.fixture.ProductFixtures; +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.products.tobe.domain.*; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductNameException; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.menus.application.InMemoryMenuRepository; +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuRepository; +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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collection; + +import static kitchenpos.fixture.Fixtures.menu; +import static kitchenpos.fixture.Fixtures.menuProduct; +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 static org.junit.jupiter.api.Assertions.assertThrows; + +@UnitTest +@DisplayName("[Product] CommandProductService 테스트") +class CommandProductServiceTest { + private TobeProductRepository productRepository; + private MenuRepository menuRepository; + private CommandProductService sut; + + + @BeforeEach + void setUp() { + productRepository = new InMemoryProductRepository(); + menuRepository = new InMemoryMenuRepository(); + sut = new CommandProductService(productRepository, menuRepository); + } + + @DisplayName("성공: 상품을 등록할 수 있다.") + @Test + void create() { + CreateProductRequest expected = ProductFixtures.createProductRequest("후라이드", 16_000L); + final Product actual = sut.addProduct(expected); + assertThat(actual).isNotNull(); + assertAll( + () -> assertThat(actual.getId()).isNotNull(), + () -> assertThat(actual.getName()).isEqualTo(expected.name()), + () -> assertThat(actual.getPrice()).isEqualTo(expected.price()) + ); + } + + @Disabled("ProductPrice 생성 시에 이미 검증") + @DisplayName("실패: 상품의 가격이 올바르지 않으면 등록할 수 없다.") + @ValueSource(longs = -1000) + @ParameterizedTest + void create(final Long price) { + assertThrows(IllegalArgumentException.class, () -> sut.addProduct(ProductFixtures.createProductRequest("후라이드", price))); + } + + @Disabled("ProductName 생성 시에 이미 검증") + @DisplayName("실패: 상품의 이름이 올바르지 않으면 등록할 수 없다.") + @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) + @NullSource + @ParameterizedTest + void create(final String name) { + assertThatThrownBy(() -> sut.addProduct(ProductFixtures.createProductRequest(name, 16_000L))) + .isInstanceOf(InvalidProductNameException.class); + } + + @DisplayName("상품의 가격을 변경할 수 있다.") + @Test + void changePrice() { + final ProductId productId = productRepository.save(ProductFixtures.product("후라이드", 16_000L)).getId(); + final ProductPrice expected = ProductPrice.of(Money.wons(15_000L)); + final Product actual = sut.changePrice(productId, expected); + assertThat(actual.getPrice()).isEqualTo(expected); + } + + @Disabled("ProductPrice 생성 시에 이미 검증") + @DisplayName("상품의 가격이 올바르지 않으면 변경할 수 없다.") + @ValueSource(longs = -1000) + @ParameterizedTest + void changePrice(final long price) { + final ProductId productId = productRepository.save(ProductFixtures.product("후라이드", 16_000L)).getId(); + assertThatThrownBy(() -> sut.changePrice(productId, ProductPrice.of(Money.wons(price)))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Disabled("Product Event 발행으로 변경") + @DisplayName("상품의 가격이 변경될 때 메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 크면 메뉴가 숨겨진다.") + @Test + void changePriceInMenu() { + final Product product = productRepository.save(ProductFixtures.product("후라이드", 16_000L)); + final Menu menu = menuRepository.save(menu(19_000L, true, menuProduct(product, 2L))); + sut.changePrice(product.getId(), ProductPrice.of(Money.wons(8_000L))); + assertThat(menuRepository.findById(menu.getId()).get().isDisplayed()).isFalse(); + } + + @DisplayName("성공: 상품의 가격이 변경될 때 ProductPriceChangedEvent가 발생한다.") + @Test + void changePrice2() { + final Product product = productRepository.save(ProductFixtures.product("후라이드", 16_000L)); + sut.changePrice(product.getId(), ProductPrice.of(Money.wons(8_000L))); + // then: domainEvents를 확인 + Collection events = product.domainEvents(); + + assertThat(events).hasSize(1); + Object event = events.iterator().next(); + assertThat(event).isInstanceOf(ProductPriceChangedEvent.class); + } +} diff --git a/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java b/src/test/java/kitchenpos/core/products/application/FakeProfanityChecker.java similarity index 56% rename from src/test/java/kitchenpos/products/application/FakePurgomalumClient.java rename to src/test/java/kitchenpos/core/products/application/FakeProfanityChecker.java index 3c4114798..d78b1766c 100644 --- a/src/test/java/kitchenpos/products/application/FakePurgomalumClient.java +++ b/src/test/java/kitchenpos/core/products/application/FakeProfanityChecker.java @@ -1,11 +1,12 @@ -package kitchenpos.products.application; +package kitchenpos.core.products.application; -import kitchenpos.products.infra.PurgomalumClient; +import kitchenpos.core.shared.domain.ProfanityChecker; +import org.thymeleaf.util.StringUtils; import java.util.Arrays; import java.util.List; -public class FakePurgomalumClient implements PurgomalumClient { +public class FakeProfanityChecker implements ProfanityChecker { private static final List profanities; static { @@ -14,6 +15,9 @@ public class FakePurgomalumClient implements PurgomalumClient { @Override public boolean containsProfanity(final String text) { + if(StringUtils.isEmpty(text)) { + return false; + } return profanities.stream() .anyMatch(profanity -> text.contains(profanity)); } diff --git a/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java b/src/test/java/kitchenpos/core/products/application/InMemoryProductRepository.java similarity index 55% rename from src/test/java/kitchenpos/products/application/InMemoryProductRepository.java rename to src/test/java/kitchenpos/core/products/application/InMemoryProductRepository.java index b55c5ec5e..0a5791208 100644 --- a/src/test/java/kitchenpos/products/application/InMemoryProductRepository.java +++ b/src/test/java/kitchenpos/core/products/application/InMemoryProductRepository.java @@ -1,17 +1,18 @@ -package kitchenpos.products.application; +package kitchenpos.core.products.application; -import kitchenpos.products.domain.Product; -import kitchenpos.products.domain.ProductRepository; + +import kitchenpos.core.products.tobe.domain.Product; +import kitchenpos.core.products.tobe.domain.TobeProductRepository; +import kitchenpos.core.shared.identifier.ProductId; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.UUID; -public class InMemoryProductRepository implements ProductRepository { - private final Map products = new HashMap<>(); +public class InMemoryProductRepository implements TobeProductRepository { + private final Map products = new HashMap<>(); @Override public Product save(final Product product) { @@ -20,7 +21,7 @@ public Product save(final Product product) { } @Override - public Optional findById(final UUID id) { + public Optional findById(final ProductId id) { return Optional.ofNullable(products.get(id)); } @@ -30,7 +31,7 @@ public List findAll() { } @Override - public List findAllByIdIn(final List ids) { + public List findAllByIdIn(final List ids) { return products.values() .stream() .filter(product -> ids.contains(product.getId())) diff --git a/src/test/java/kitchenpos/core/products/application/QueryProductServiceTest.java b/src/test/java/kitchenpos/core/products/application/QueryProductServiceTest.java new file mode 100644 index 000000000..22d54ffcb --- /dev/null +++ b/src/test/java/kitchenpos/core/products/application/QueryProductServiceTest.java @@ -0,0 +1,39 @@ +package kitchenpos.core.products.application; + +import kitchenpos.config.UnitTest; +import kitchenpos.fixture.ProductFixtures; +import kitchenpos.core.products.tobe.domain.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static kitchenpos.fixture.Fixtures.menu; +import static kitchenpos.fixture.Fixtures.menuProduct; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@UnitTest +@DisplayName("[Product] QueryProductService 테스트") +class QueryProductServiceTest { + private TobeProductRepository productRepository; + private QueryProductService sut; + + @BeforeEach + void setUp() { + productRepository = new InMemoryProductRepository(); + sut = new QueryProductService(productRepository); + } + + @DisplayName("성공: 저장된 전체 상품을 조회할 수 있다.") + @Test + void findAll() { + productRepository.save(ProductFixtures.product("후라이드", 16_000L)); + productRepository.save(ProductFixtures.product("양념치킨", 16_000L)); + final List actual = sut.findProducts(); + assertThat(actual).hasSize(2); + } + +} diff --git a/src/test/java/kitchenpos/core/products/tobe/domain/ProductNameTest.java b/src/test/java/kitchenpos/core/products/tobe/domain/ProductNameTest.java new file mode 100644 index 000000000..6b4f2d827 --- /dev/null +++ b/src/test/java/kitchenpos/core/products/tobe/domain/ProductNameTest.java @@ -0,0 +1,53 @@ +package kitchenpos.core.products.tobe.domain; + +import kitchenpos.config.UnitTest; +import kitchenpos.core.products.application.FakeProfanityChecker; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductNameException; +import kitchenpos.core.products.tobe.domain.support.DefaultProductNamePolicy; +import kitchenpos.core.shared.domain.ProfanityChecker; +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; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +@UnitTest +@DisplayName("[Product] ProductName 테스트") +class ProductNameTest { + + private final ProfanityChecker profanityChecker = new FakeProfanityChecker(); + private final ProductNamePolicy productNamePolicy = new DefaultProductNamePolicy(profanityChecker); + + @Test + @DisplayName("성공: 유효한 이름으로 ProductName 생성") + void createProductNameSuccess() { + // given + String rawName = " Test Product "; + + // when + ProductName productName = ProductName.create(productNamePolicy, rawName); + + // then + assertThat(productName.getName()).isEqualTo("Test Product"); + } + + @ParameterizedTest + @DisplayName("실패: null이나 빈 문자열로 ProductName 생성시 InvalidProductNameException 발생") + @NullAndEmptySource + void createProductNameFail(String name) { + + // when & then + assertThrows(InvalidProductNameException.class, () -> ProductName.create(productNamePolicy, name)); + } + + @ParameterizedTest + @DisplayName("실패: 비속어가 포함된 ProductName 생성시 InvalidProductNameException 발생") + @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) + void createProductNameFailWhenContainsProfanity(String name) { + // when & then + assertThrows(InvalidProductNameException.class, () -> ProductName.create(productNamePolicy, name)); + } +} \ No newline at end of file diff --git a/src/test/java/kitchenpos/core/products/tobe/domain/ProductPriceTest.java b/src/test/java/kitchenpos/core/products/tobe/domain/ProductPriceTest.java new file mode 100644 index 000000000..939ac2601 --- /dev/null +++ b/src/test/java/kitchenpos/core/products/tobe/domain/ProductPriceTest.java @@ -0,0 +1,75 @@ +package kitchenpos.core.products.tobe.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import kitchenpos.config.UnitTest; +import kitchenpos.core.products.tobe.domain.exception.InvalidProductPriceException; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; +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.ValueSource; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + +@UnitTest +@DisplayName("[Product] ProductPrice 테스트") +class ProductPriceTest { + + @Test + @DisplayName("성공: 유효한 금액으로 ProductPrice 생성") + void createProductPriceSuccess() { + // given + Money money = Money.wons(new BigDecimal("10000")); + + // when + ProductPrice productPrice = ProductPrice.of(money); + + // then + assertNotNull(productPrice); + assertEquals(money, productPrice.getPrice()); + } + + @Disabled("Money 객체의 생성자에서 null 체크를 하므로 해당 테스트는 필요 없음") + @Test + @DisplayName("실패: null 금액으로 ProductPrice 생성 시 예외 발생") + void createProductPriceFailWhenNull() { + // given & when & then + assertThrows(InvalidProductPriceException.class, () -> { + ProductPrice.of(null); + }); + } + + @Disabled("Money 객체의 생성자에서 0보다 작은 금액 체크를 하므로 해당 테스트는 필요 없음") + @ParameterizedTest + @DisplayName("실패: 0보다 작은 금액으로 ProductPrice 생성 시 예외 발생") + @ValueSource(strings = {"-1", "-10000"}) + void createProductPriceFailWhenNegative(BigDecimal price) { + // given + Money negativeMoney = Money.wons(price); + + // when & then + assertThrows(InvalidProductPriceException.class, () -> { + ProductPrice.of(negativeMoney); + }); + } + @Test + @DisplayName("성공: 곱하기 연산 테스트") + void multiplyTest() { + // given + Money money = Money.wons(new BigDecimal("5000")); + ProductPrice productPrice = ProductPrice.of(money); + Quantity quantity = Quantity.of(3); // Quantity의 내부 값은 3으로 가정 + + // when + Money result = productPrice.multiply(quantity); + + // then + Money expected = money.multiply(3); + assertEquals(expected, result); + } +} \ No newline at end of file diff --git a/src/test/java/kitchenpos/core/products/tobe/domain/ProductTest.java b/src/test/java/kitchenpos/core/products/tobe/domain/ProductTest.java new file mode 100644 index 000000000..602fdabc7 --- /dev/null +++ b/src/test/java/kitchenpos/core/products/tobe/domain/ProductTest.java @@ -0,0 +1,85 @@ +package kitchenpos.core.products.tobe.domain; + +import kitchenpos.config.UnitTest; +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.shared.value.Money; +import kitchenpos.fixture.ProductFixtures; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@UnitTest +@DisplayName("[Product] Product 테스트") +class ProductTest { + @Test + @DisplayName("성공: 유효한 값으로 Product 생성") + void testCreateProductSuccess() { + // given + CreateProductRequest request = ProductFixtures.createProductRequest("후라이드", 16_000L); + + // when + Product product = Product.create(request.id(), request.name(), request.price()); + + // then + assertNotNull(product); + assertEquals(request.id(), product.getId()); + assertEquals(request.name(), product.getName()); + assertEquals(request.price(), product.getPrice()); + } + + @Test + @DisplayName("실패: null id로 Product 생성 시 예외 발생") + void testCreateProductFailWhenIdIsNull() { + // given + CreateProductRequest request = ProductFixtures.createProductRequest("후라이드", 16_000L); + + // when & then + Exception exception = assertThrows(NullPointerException.class, () -> + Product.create(null, request.name(), request.price()) + ); + assertTrue(exception.getMessage().contains("id는 null이 될 수 없습니다.")); + } + + @Test + @DisplayName("실패: null name으로 Product 생성 시 예외 발생") + void testCreateProductFailWhenNameIsNull() { + // given + CreateProductRequest request = ProductFixtures.createProductRequest("후라이드", 16_000L); + + // when & then + Exception exception = assertThrows(NullPointerException.class, () -> + Product.create(request.id(), null, request.price()) + ); + assertTrue(exception.getMessage().contains("name은 null이 될 수 없습니다.")); + } + + @Test + @DisplayName("실패: null price로 Product 생성 시 예외 발생") + void testCreateProductFailWhenPriceIsNull() { + // given + CreateProductRequest request = ProductFixtures.createProductRequest("후라이드", 16_000L); + + // when & then + Exception exception = assertThrows(NullPointerException.class, () -> + Product.create(request.id(), request.name(), null) + ); + assertTrue(exception.getMessage().contains("price은 null이 될 수 없습니다.")); + } + + @Test + @DisplayName("성공: changePrice를 통해 Product 가격 변경") + void testChangePrice() { + // given + CreateProductRequest request = ProductFixtures.createProductRequest("후라이드", 16_000L); + Product product = Product.create(request.id(), request.name(), request.price()); + + ProductPrice newPrice = ProductPrice.of(Money.wons(15_000)); + + // when + product.changePrice(newPrice); + + // then + assertEquals(newPrice, product.getPrice()); + } +} \ No newline at end of file diff --git a/src/test/java/kitchenpos/core/shared/value/MoneyTest.java b/src/test/java/kitchenpos/core/shared/value/MoneyTest.java new file mode 100644 index 000000000..82ec93ac9 --- /dev/null +++ b/src/test/java/kitchenpos/core/shared/value/MoneyTest.java @@ -0,0 +1,111 @@ +package kitchenpos.core.shared.value; + +import kitchenpos.config.UnitTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; + + +@UnitTest +@DisplayName("[Shared] Money 테스트") +class MoneyTest { + + @Test + @DisplayName("성공: wons(long)으로 Money 생성") + void createMoneyFromLong() { + Money money = Money.wons(1000); + assertNotNull(money); + assertEquals(BigDecimal.valueOf(1000), money.getAmount()); + } + + @Test + @DisplayName("성공: wons(BigDecimal)으로 Money 생성") + void createMoneyFromBigDecimal() { + Money money = Money.wons(BigDecimal.valueOf(5000)); + assertNotNull(money); + assertEquals(BigDecimal.valueOf(5000), money.getAmount()); + } + + @Test + @DisplayName("실패: null 금액으로 Money 생성 시 IllegalArgumentException 예외 발생") + void createMoneyFailWhenNull() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Money.wons((BigDecimal) null) + ); + assertTrue(exception.getMessage().contains("금액은 null이 될 수 없습니다.")); + } + + @Test + @DisplayName("실패: 음수 금액으로 Money 생성 시 IllegalArgumentException 예외 발생") + void createMoneyFailWhenNegative() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Money.wons(-100L) + ); + assertTrue(exception.getMessage().contains("금액은 0보다 작을 수 없습니다.")); + } + + @Test + @DisplayName("성공: Money.ZERO는 0원이어야 함") + void moneyZeroTest() { + Money zero = Money.ZERO; + assertNotNull(zero); + assertEquals(BigDecimal.ZERO, zero.getAmount()); + } + + @Test + @DisplayName("성공: add 연산 테스트") + void testAdd() { + Money money1 = Money.wons(1000); + Money money2 = Money.wons(2000); + Money result = money1.add(money2); + Money expected = Money.wons(3000); + assertEquals(expected.getAmount(), result.getAmount()); + } + + @Test + @DisplayName("성공: multiply 연산 테스트") + void testMultiply() { + Money money = Money.wons(500); + Money result = money.multiply(4); + Money expected = Money.wons(2000); + assertEquals(expected.getAmount(), result.getAmount()); + } + + @Test + @DisplayName("성공: isLessThan 연산 테스트") + void testIsLessThan() { + Money money1 = Money.wons(1000); + Money money2 = Money.wons(2000); + assertTrue(money1.isLessThan(money2)); + assertFalse(money2.isLessThan(money1)); + } + + @Test + @DisplayName("성공: isBiggerThan 연산 테스트") + void testIsBiggerThan() { + Money money1 = Money.wons(3000); + Money money2 = Money.wons(2000); + assertTrue(money1.isBiggerThan(money2)); + assertFalse(money2.isBiggerThan(money1)); + } + + @Test + @DisplayName("성공: isEqual 연산 테스트") + void testIsEqual() { + Money money1 = Money.wons(1500); + Money money2 = Money.wons(1500); + Money money3 = Money.wons(2000); + assertTrue(money1.isEqual(money2)); + assertFalse(money1.isEqual(money3)); + } + + @Test + @DisplayName("성공: toString 테스트") + void testToString() { + Money money = Money.wons(2500); + assertEquals("2500원", money.toString()); + } +} \ No newline at end of file diff --git a/src/test/java/kitchenpos/core/shared/value/QuantityTest.java b/src/test/java/kitchenpos/core/shared/value/QuantityTest.java new file mode 100644 index 000000000..bda51b927 --- /dev/null +++ b/src/test/java/kitchenpos/core/shared/value/QuantityTest.java @@ -0,0 +1,59 @@ +package kitchenpos.core.shared.value; + +import kitchenpos.config.UnitTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@UnitTest +@DisplayName("[Shared] Quantity 테스트") +class QuantityTest { + + @Test + @DisplayName("성공: 유효한 수량으로 Quantity 생성") + void createQuantitySuccess() { + // given + long validValue = 5; + // when + Quantity quantity = Quantity.of(validValue); + // then + assertNotNull(quantity); + assertEquals(validValue, quantity.getValue()); + } + + @Test + @DisplayName("실패: 음수 수량으로 Quantity 생성 시 IllegalArgumentException 예외 발생") + void createQuantityFailWhenNegative() { + // given + long negativeValue = -1; + // when & then + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + Quantity.of(negativeValue) + ); + assertTrue(exception.getMessage().contains("수량은 음수가 될 수 없습니다.")); + } + + @Test + @DisplayName("성공: toString 테스트") + void testToString() { + // given + Quantity quantity = Quantity.of(10); + // when + String result = quantity.toString(); + // then + assertEquals("10 개", result); + } + + @Test + @DisplayName("성공: isEqual 테스트") + void testEquality() { + // given + Quantity q1 = Quantity.of(5); + Quantity q2 = Quantity.of(5); + Quantity q3 = Quantity.of(6); + // then + assertEquals(q1, q2); + assertNotEquals(q1, q3); + } +} \ No newline at end of file diff --git a/src/test/java/kitchenpos/Fixtures.java b/src/test/java/kitchenpos/fixture/Fixtures.java similarity index 78% rename from src/test/java/kitchenpos/Fixtures.java rename to src/test/java/kitchenpos/fixture/Fixtures.java index 434768a52..a65c0684d 100644 --- a/src/test/java/kitchenpos/Fixtures.java +++ b/src/test/java/kitchenpos/fixture/Fixtures.java @@ -1,22 +1,25 @@ -package kitchenpos; - -import kitchenpos.eatinorders.domain.Order; -import kitchenpos.eatinorders.domain.OrderLineItem; -import kitchenpos.eatinorders.domain.OrderStatus; -import kitchenpos.eatinorders.domain.OrderTable; -import kitchenpos.eatinorders.domain.OrderType; -import kitchenpos.menus.domain.Menu; -import kitchenpos.menus.domain.MenuGroup; -import kitchenpos.menus.domain.MenuProduct; -import kitchenpos.products.domain.Product; - -import java.math.BigDecimal; +package kitchenpos.fixture; + +import kitchenpos.core.eatinorders.domain.Order; +import kitchenpos.core.eatinorders.domain.OrderLineItem; +import kitchenpos.core.eatinorders.domain.OrderStatus; +import kitchenpos.core.eatinorders.domain.OrderTable; +import kitchenpos.core.eatinorders.domain.OrderType; +import kitchenpos.core.menus.domain.Menu; +import kitchenpos.core.menus.domain.MenuGroup; +import kitchenpos.core.menus.domain.MenuProduct; + +import kitchenpos.core.products.tobe.domain.*; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.shared.value.Quantity; + import java.time.LocalDateTime; import java.util.Arrays; import java.util.Random; import java.util.UUID; public class Fixtures { + public static final UUID INVALID_ID = new UUID(0L, 0L); public static Menu menu() { @@ -31,7 +34,7 @@ public static Menu menu(final long price, final boolean displayed, final MenuPro final Menu menu = new Menu(); menu.setId(UUID.randomUUID()); menu.setName("후라이드+후라이드"); - menu.setPrice(BigDecimal.valueOf(price)); + menu.setPrice(Money.wons(price)); menu.setMenuGroup(menuGroup()); menu.setDisplayed(displayed); menu.setMenuProducts(Arrays.asList(menuProducts)); @@ -52,8 +55,8 @@ public static MenuGroup menuGroup(final String name) { public static MenuProduct menuProduct() { final MenuProduct menuProduct = new MenuProduct(); menuProduct.setSeq(new Random().nextLong()); - menuProduct.setProduct(product()); - menuProduct.setQuantity(2L); + menuProduct.setProduct(ProductFixtures.product()); + menuProduct.setQuantity(Quantity.of(2L)); return menuProduct; } @@ -61,7 +64,7 @@ public static MenuProduct menuProduct(final Product product, final long quantity final MenuProduct menuProduct = new MenuProduct(); menuProduct.setSeq(new Random().nextLong()); menuProduct.setProduct(product); - menuProduct.setQuantity(quantity); + menuProduct.setQuantity(Quantity.of(quantity)); return menuProduct; } @@ -117,15 +120,4 @@ public static OrderTable orderTable(final boolean occupied, final int numberOfGu return orderTable; } - public static Product product() { - return product("후라이드", 16_000L); - } - - public static Product product(final String name, final long price) { - final Product product = new Product(); - product.setId(UUID.randomUUID()); - product.setName(name); - product.setPrice(BigDecimal.valueOf(price)); - return product; - } } diff --git a/src/test/java/kitchenpos/fixture/ProductFixtures.java b/src/test/java/kitchenpos/fixture/ProductFixtures.java new file mode 100644 index 000000000..ddc2437bb --- /dev/null +++ b/src/test/java/kitchenpos/fixture/ProductFixtures.java @@ -0,0 +1,39 @@ +package kitchenpos.fixture; + +import kitchenpos.core.products.application.dto.CreateProductRequest; +import kitchenpos.core.products.tobe.domain.*; +import kitchenpos.core.products.tobe.domain.support.DefaultProductNamePolicy; +import kitchenpos.core.shared.domain.ProfanityChecker; +import kitchenpos.core.shared.identifier.ProductId; +import kitchenpos.core.shared.value.Money; +import kitchenpos.core.products.application.FakeProfanityChecker; + +import java.util.UUID; + +public class ProductFixtures { + + private static ProductIdGenerator productIdGenerator = () -> ProductId.of(UUID.randomUUID()); + private static ProfanityChecker profanityChecker = new FakeProfanityChecker(); + private static ProductNamePolicy productNamePolicy = new DefaultProductNamePolicy(profanityChecker); + + public static Product product() { + return product("후라이드", 16_000L); + } + + public static Product product(final String name, final long price) { + final Product product = Product.create( + productIdGenerator.generateId(), + ProductName.create(productNamePolicy, name), + ProductPrice.of(Money.wons(price)) + ); + return product; + } + + public static CreateProductRequest createProductRequest(final String name, final long price) { + return new CreateProductRequest( + productIdGenerator.generateId(), + ProductName.create(productNamePolicy, name), + ProductPrice.of(Money.wons(price))); + } + +} diff --git a/src/test/java/kitchenpos/products/application/ProductServiceTest.java b/src/test/java/kitchenpos/products/application/ProductServiceTest.java deleted file mode 100644 index 74a31073e..000000000 --- a/src/test/java/kitchenpos/products/application/ProductServiceTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package kitchenpos.products.application; - -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 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; - -import java.math.BigDecimal; -import java.util.List; -import java.util.UUID; - -import static kitchenpos.Fixtures.menu; -import static kitchenpos.Fixtures.menuProduct; -import static kitchenpos.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; - -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 Product expected = createProductRequest("후라이드", 16_000L); - final Product 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 Product expected = createProductRequest("후라이드", price); - assertThatThrownBy(() -> productService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("상품의 이름이 올바르지 않으면 등록할 수 없다.") - @ValueSource(strings = {"비속어", "욕설이 포함된 이름"}) - @NullSource - @ParameterizedTest - void create(final String name) { - final Product expected = createProductRequest(name, 16_000L); - assertThatThrownBy(() -> productService.create(expected)) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("상품의 가격을 변경할 수 있다.") - @Test - void changePrice() { - final UUID productId = productRepository.save(product("후라이드", 16_000L)).getId(); - final Product expected = changePriceRequest(15_000L); - final Product 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 = productRepository.save(product("후라이드", 16_000L)).getId(); - final Product expected = changePriceRequest(price); - assertThatThrownBy(() -> productService.changePrice(productId, expected)) - .isInstanceOf(IllegalArgumentException.class); - } - - @DisplayName("상품의 가격이 변경될 때 메뉴의 가격이 메뉴에 속한 상품 금액의 합보다 크면 메뉴가 숨겨진다.") - @Test - void changePriceInMenu() { - final Product product = productRepository.save(product("후라이드", 16_000L)); - final Menu menu = menuRepository.save(menu(19_000L, true, menuProduct(product, 2L))); - productService.changePrice(product.getId(), changePriceRequest(8_000L)); - assertThat(menuRepository.findById(menu.getId()).get().isDisplayed()).isFalse(); - } - - @DisplayName("상품의 목록을 조회할 수 있다.") - @Test - void findAll() { - productRepository.save(product("후라이드", 16_000L)); - productRepository.save(product("양념치킨", 16_000L)); - final List actual = productService.findAll(); - assertThat(actual).hasSize(2); - } - - private Product createProductRequest(final String name, final long price) { - return createProductRequest(name, BigDecimal.valueOf(price)); - } - - private Product createProductRequest(final String name, final BigDecimal price) { - final Product product = new Product(); - product.setName(name); - product.setPrice(price); - return product; - } - - private Product changePriceRequest(final long price) { - return changePriceRequest(BigDecimal.valueOf(price)); - } - - private Product changePriceRequest(final BigDecimal price) { - final Product product = new Product(); - product.setPrice(price); - return product; - } -}