From 49af9bd9c37dde84b01f82f4db97ecf4a565656b Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:30:11 +0900 Subject: [PATCH 01/33] =?UTF-8?q?[feat]=20Order=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문하기 전에 OrderValidator를 통해 검증한다. --- .../camp/woowak/lab/order/domain/Order.java | 24 +++++++++++++++++++ .../lab/order/domain/OrderValidator.java | 9 +++++++ 2 files changed, 33 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/domain/OrderValidator.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index 206c0c4e..dfc6fff6 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -1,17 +1,28 @@ package camp.woowak.lab.order.domain; +import java.util.ArrayList; +import java.util.List; + import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.order.domain.vo.OrderItem; import camp.woowak.lab.store.domain.Store; +import jakarta.persistence.CollectionTable; +import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; @Entity @Table(name = "ORDERS") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -20,4 +31,17 @@ public class Order { private Customer requester; @ManyToOne(fetch = FetchType.LAZY) private Store store; + @CollectionTable(name = "ORDER_ITEMS", joinColumns = @JoinColumn(name = "order_id")) + @ElementCollection(fetch = FetchType.EAGER) + private List orderItems = new ArrayList<>(); + + public Order(Customer requester, List orderedMenus, OrderValidator orderValidator) { + orderValidator.check(orderedMenus); + this.requester = requester; + this.store = orderedMenus.get(0).getStore(); + } + + public Long getId() { + return id; + } } diff --git a/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java new file mode 100644 index 00000000..73109633 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import camp.woowak.lab.menu.domain.Menu; + +public interface OrderValidator { + void check(List orderedMenus); +} From a590a9fa553e25092b2c659670c20c602a763bb4 Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:31:31 +0900 Subject: [PATCH 02/33] [feat] SingleStoreOrderValidator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 동일한 가게에 대한 주문인지 검증 --- .../domain/SingleStoreOrderValidator.java | 25 +++++++++++++ .../order/exception/EmptyCartException.java | 9 +++++ .../exception/MultiStoreOrderException.java | 9 +++++ .../lab/order/exception/OrderErrorCode.java | 35 +++++++++++++++++++ 4 files changed, 78 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java create mode 100644 src/main/java/camp/woowak/lab/order/exception/EmptyCartException.java create mode 100644 src/main/java/camp/woowak/lab/order/exception/MultiStoreOrderException.java create mode 100644 src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java diff --git a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java new file mode 100644 index 00000000..c592af5f --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java @@ -0,0 +1,25 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.exception.MultiStoreOrderException; + +@Component +public class SingleStoreOrderValidator implements OrderValidator { + @Override + public void check(List orderedMenus) { + if (orderedMenus == null || orderedMenus.isEmpty()) { + throw new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다."); + } + Long storeId = orderedMenus.get(0).getStore().getId(); + for (Menu orderedMenu : orderedMenus) { + if (!orderedMenu.getStore().getId().equals(storeId)) { + throw new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다."); + } + } + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/EmptyCartException.java b/src/main/java/camp/woowak/lab/order/exception/EmptyCartException.java new file mode 100644 index 00000000..a6f093a2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/EmptyCartException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class EmptyCartException extends BadRequestException { + public EmptyCartException(String message) { + super(OrderErrorCode.EMPTY_CART, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/MultiStoreOrderException.java b/src/main/java/camp/woowak/lab/order/exception/MultiStoreOrderException.java new file mode 100644 index 00000000..5368eb1d --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/MultiStoreOrderException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class MultiStoreOrderException extends BadRequestException { + public MultiStoreOrderException(String message) { + super(OrderErrorCode.MULTI_STORE_ORDER, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java new file mode 100644 index 00000000..538f24cf --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java @@ -0,0 +1,35 @@ +package camp.woowak.lab.order.exception; + +import org.springframework.http.HttpStatus; + +import camp.woowak.lab.common.exception.ErrorCode; + +public enum OrderErrorCode implements ErrorCode { + EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1", "장바구니가 비어있습니다."), + MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_2", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."); + + private final int status; + private final String errorCode; + private final String message; + + OrderErrorCode(HttpStatus httpStatus, String errorCode, String message) { + this.status = httpStatus.value(); + this.errorCode = errorCode; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getErrorCode() { + return errorCode; + } + + @Override + public String getMessage() { + return message; + } +} From edb05f36112c5e188ef4358253a2c1f9ce48e191 Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:38:56 +0900 Subject: [PATCH 03/33] [feat] PayAmountValidator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 잔고가 부족하면 주문할 수 없습니다. --- .../camp/woowak/lab/order/domain/Order.java | 2 +- .../lab/order/domain/OrderValidator.java | 3 ++- .../lab/order/domain/PayAmountValidator.java | 20 +++++++++++++++++++ .../domain/SingleStoreOrderValidator.java | 3 ++- .../exception/NotEnoughBalanceException.java | 9 +++++++++ .../lab/order/exception/OrderErrorCode.java | 5 +++-- 6 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java create mode 100644 src/main/java/camp/woowak/lab/order/exception/NotEnoughBalanceException.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index dfc6fff6..f02b70a0 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -36,7 +36,7 @@ public class Order { private List orderItems = new ArrayList<>(); public Order(Customer requester, List orderedMenus, OrderValidator orderValidator) { - orderValidator.check(orderedMenus); + orderValidator.check(requester, orderedMenus); this.requester = requester; this.store = orderedMenus.get(0).getStore(); } diff --git a/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java index 73109633..ac12c17f 100644 --- a/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java +++ b/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java @@ -2,8 +2,9 @@ import java.util.List; +import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.menu.domain.Menu; public interface OrderValidator { - void check(List orderedMenus); + void check(Customer requester, List orderedMenus); } diff --git a/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java b/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java new file mode 100644 index 00000000..d90cf34b --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java @@ -0,0 +1,20 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.order.exception.NotEnoughBalanceException; + +public class PayAmountValidator implements OrderValidator { + @Override + public void check(Customer requester, List orderedMenus) { + Integer totalPrice = 0; + for (Menu orderedMenu : orderedMenus) { + totalPrice += orderedMenu.getPrice(); + } + if (requester.getPayAccount().getBalance() < totalPrice) { + throw new NotEnoughBalanceException("구매자 " + requester.getId() + "가 잔고가 부족하여 주문에 실패했습니다."); + } + } +} diff --git a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java index c592af5f..372809f7 100644 --- a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java +++ b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Component; +import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.menu.domain.Menu; import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.exception.MultiStoreOrderException; @@ -11,7 +12,7 @@ @Component public class SingleStoreOrderValidator implements OrderValidator { @Override - public void check(List orderedMenus) { + public void check(Customer requester, List orderedMenus) { if (orderedMenus == null || orderedMenus.isEmpty()) { throw new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다."); } diff --git a/src/main/java/camp/woowak/lab/order/exception/NotEnoughBalanceException.java b/src/main/java/camp/woowak/lab/order/exception/NotEnoughBalanceException.java new file mode 100644 index 00000000..02aa3795 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/NotEnoughBalanceException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class NotEnoughBalanceException extends BadRequestException { + public NotEnoughBalanceException(String message) { + super(OrderErrorCode.NOT_ENOUGH_BALANCE, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java index 538f24cf..344b9488 100644 --- a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java +++ b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java @@ -5,8 +5,9 @@ import camp.woowak.lab.common.exception.ErrorCode; public enum OrderErrorCode implements ErrorCode { - EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1", "장바구니가 비어있습니다."), - MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_2", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."); + EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1_1", "장바구니가 비어있습니다."), + MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_2", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."), + NOT_ENOUGH_BALANCE(HttpStatus.BAD_REQUEST, "o_1_2", "잔고가 부족합니다."); private final int status; private final String errorCode; From 7a7a8f666fba953aceb89f9abb5c0b26cbbce5ae Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:42:54 +0900 Subject: [PATCH 04/33] =?UTF-8?q?[fix]=20CustomerRepository=EC=9D=98=20Id?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long -> UUID --- .../woowak/lab/customer/exception/CustomerErrorCode.java | 3 ++- .../customer/exception/NotFoundCustomerException.java | 9 +++++++++ .../lab/customer/repository/CustomerRepository.java | 5 +++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 src/main/java/camp/woowak/lab/customer/exception/NotFoundCustomerException.java diff --git a/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java index 8b575767..846377a0 100644 --- a/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java +++ b/src/main/java/camp/woowak/lab/customer/exception/CustomerErrorCode.java @@ -7,7 +7,8 @@ public enum CustomerErrorCode implements ErrorCode { INVALID_CREATION(HttpStatus.BAD_REQUEST, "C1", "잘못된 요청입니다."), DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "C2", "이미 존재하는 이메일입니다."), - AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "C3", "이메일 또는 비밀번호가 올바르지 않습니다."); + AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED, "C3", "이메일 또는 비밀번호가 올바르지 않습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "C4", "존재하지 않는 사용자입니다."); private final HttpStatus status; private final String errorCode; diff --git a/src/main/java/camp/woowak/lab/customer/exception/NotFoundCustomerException.java b/src/main/java/camp/woowak/lab/customer/exception/NotFoundCustomerException.java new file mode 100644 index 00000000..a1bda32b --- /dev/null +++ b/src/main/java/camp/woowak/lab/customer/exception/NotFoundCustomerException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.customer.exception; + +import camp.woowak.lab.common.exception.NotFoundException; + +public class NotFoundCustomerException extends NotFoundException { + public NotFoundCustomerException(String message) { + super(CustomerErrorCode.NOT_FOUND, message); + } +} diff --git a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java index 28be0c62..22cf2374 100644 --- a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java +++ b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java @@ -6,7 +6,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.exception.NotFoundCustomerException; public interface CustomerRepository extends JpaRepository { Optional findByEmail(String email); + + default Customer findByIdOrThrow(UUID id) { + return findById(id).orElseThrow(() -> new NotFoundCustomerException("존재하지 않는 사용자를 조회했습니다.")); + } } From 5a022e64db9c7e393e81bf1b266566fcd17c6779 Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:43:07 +0900 Subject: [PATCH 05/33] =?UTF-8?q?[chore]=20.gitkeep=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/camp/woowak/lab/order/repository/.gitkeep | 0 src/main/java/camp/woowak/lab/order/service/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/main/java/camp/woowak/lab/order/repository/.gitkeep delete mode 100644 src/main/java/camp/woowak/lab/order/service/.gitkeep diff --git a/src/main/java/camp/woowak/lab/order/repository/.gitkeep b/src/main/java/camp/woowak/lab/order/repository/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/camp/woowak/lab/order/service/.gitkeep b/src/main/java/camp/woowak/lab/order/service/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 04fa03343984515d26579e589f346e3e9806d8c2 Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:46:09 +0900 Subject: [PATCH 06/33] =?UTF-8?q?[feat]=20OrderCreationService=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composite 패턴으로 Validator를 Order 생성자에 넘긴다. --- .../order/domain/CompositeOrderValidator.java | 21 ++++++++ .../lab/order/domain/PayAmountValidator.java | 3 ++ .../lab/order/domain/StockValidator.java | 16 +++++++ .../order/service/OrderCreationService.java | 48 +++++++++++++++++++ .../service/command/OrderCreationCommand.java | 8 ++++ 5 files changed, 96 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java create mode 100644 src/main/java/camp/woowak/lab/order/domain/StockValidator.java create mode 100644 src/main/java/camp/woowak/lab/order/service/OrderCreationService.java create mode 100644 src/main/java/camp/woowak/lab/order/service/command/OrderCreationCommand.java diff --git a/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java new file mode 100644 index 00000000..1b179071 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java @@ -0,0 +1,21 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.menu.domain.Menu; + +public class CompositeOrderValidator implements OrderValidator { + private final List orderValidators; + + public CompositeOrderValidator(List orderValidators) { + this.orderValidators = orderValidators; + } + + @Override + public void check(Customer requester, List orderedMenus) { + for (OrderValidator orderValidator : orderValidators) { + orderValidator.check(requester, orderedMenus); + } + } +} diff --git a/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java b/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java index d90cf34b..3b792055 100644 --- a/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java +++ b/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java @@ -2,10 +2,13 @@ import java.util.List; +import org.springframework.stereotype.Component; + import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.menu.domain.Menu; import camp.woowak.lab.order.exception.NotEnoughBalanceException; +@Component public class PayAmountValidator implements OrderValidator { @Override public void check(Customer requester, List orderedMenus) { diff --git a/src/main/java/camp/woowak/lab/order/domain/StockValidator.java b/src/main/java/camp/woowak/lab/order/domain/StockValidator.java new file mode 100644 index 00000000..3a4a235f --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/StockValidator.java @@ -0,0 +1,16 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.menu.domain.Menu; + +@Component +public class StockValidator implements OrderValidator { + @Override + public void check(Customer requester, List orderedMenus) { + + } +} diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java new file mode 100644 index 00000000..a5ce0965 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -0,0 +1,48 @@ +package camp.woowak.lab.order.service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.repository.CartRepository; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.order.domain.CompositeOrderValidator; +import camp.woowak.lab.order.domain.Order; +import camp.woowak.lab.order.domain.OrderValidator; +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.repository.OrderRepository; +import camp.woowak.lab.order.service.command.OrderCreationCommand; + +@Service +@Transactional +public class OrderCreationService { + private final OrderRepository orderRepository; + private final CartRepository cartRepository; + private final CustomerRepository customerRepository; + private final CompositeOrderValidator orderValidators; + + public OrderCreationService(OrderRepository orderRepository, CartRepository cartRepository, + CustomerRepository customerRepository, List orderValidators) { + this.orderRepository = orderRepository; + this.cartRepository = cartRepository; + this.customerRepository = customerRepository; + this.orderValidators = new CompositeOrderValidator(orderValidators); + } + + public Long create(OrderCreationCommand cmd) { + UUID requesterId = cmd.requesterId(); + Customer requester = customerRepository.findByIdOrThrow(requesterId); + Optional findCart = cartRepository.findByCustomerId(requesterId.toString()); + Cart cart; + if (findCart.isEmpty() || (cart = findCart.get()).getMenuList().isEmpty()) { + throw new EmptyCartException("구매자 " + cmd.requesterId() + "가 비어있는 카트로 주문을 시도했습니다."); + } + Order savedOrder = orderRepository.save(new Order(requester, cart.getMenuList(), orderValidators)); + return savedOrder.getId(); + } +} diff --git a/src/main/java/camp/woowak/lab/order/service/command/OrderCreationCommand.java b/src/main/java/camp/woowak/lab/order/service/command/OrderCreationCommand.java new file mode 100644 index 00000000..e75ade67 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/service/command/OrderCreationCommand.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.order.service.command; + +import java.util.UUID; + +public record OrderCreationCommand( + UUID requesterId +) { +} From 517be588d49b8e075aac7636df43622f917098cb Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:46:26 +0900 Subject: [PATCH 07/33] [feat] OrderRepository --- .../camp/woowak/lab/order/repository/OrderRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/repository/OrderRepository.java diff --git a/src/main/java/camp/woowak/lab/order/repository/OrderRepository.java b/src/main/java/camp/woowak/lab/order/repository/OrderRepository.java new file mode 100644 index 00000000..afcd1f7b --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/repository/OrderRepository.java @@ -0,0 +1,8 @@ +package camp.woowak.lab.order.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import camp.woowak.lab.order.domain.Order; + +public interface OrderRepository extends JpaRepository { +} From cb53e35fe1291381e3a75e8a3518e92cdb26c160 Mon Sep 17 00:00:00 2001 From: donghar Date: Thu, 15 Aug 2024 23:48:50 +0900 Subject: [PATCH 08/33] [feat] OrderItem --- .../java/camp/woowak/lab/order/domain/vo/OrderItem.java | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java diff --git a/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java new file mode 100644 index 00000000..d1a36afc --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.domain.vo; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class OrderItem { + private Long menuId; + private int quantity; +} From 046e02b3a862e354f782a4a7b5b2feda0cc64c49 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 16:18:15 +0900 Subject: [PATCH 09/33] =?UTF-8?q?[fix]=20Cart=EB=8A=94=20Menu=EA=B0=80=20?= =?UTF-8?q?=EC=95=84=EB=8B=88=EB=9D=BC=20CartItem=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EA=B0=80=EC=A7=84=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 수량을 반영하기 위해 MenuId를 가지는 CartItem으로 수정 --- .../camp/woowak/lab/cart/domain/Cart.java | 38 +++++++++------ .../woowak/lab/cart/domain/vo/CartItem.java | 46 +++++++++++++++++++ 2 files changed, 69 insertions(+), 15 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java diff --git a/src/main/java/camp/woowak/lab/cart/domain/Cart.java b/src/main/java/camp/woowak/lab/cart/domain/Cart.java index 9d434931..8bda0433 100644 --- a/src/main/java/camp/woowak/lab/cart/domain/Cart.java +++ b/src/main/java/camp/woowak/lab/cart/domain/Cart.java @@ -6,6 +6,7 @@ import java.util.Set; import java.util.stream.Collectors; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.cart.exception.OtherStoreMenuException; import camp.woowak.lab.cart.exception.StoreNotOpenException; import camp.woowak.lab.menu.domain.Menu; @@ -16,7 +17,7 @@ @Getter public class Cart { private final String customerId; - private final List menuList; + private final List cartItems; /** * 생성될 때 무조건 cart가 비어있도록 구현 @@ -31,27 +32,36 @@ public Cart(String customerId) { * 해당 Domain을 사용하는 같은 패키지내의 클래스, 혹은 자식 클래스는 List를 커스텀할 수 있습니다. * * @param customerId 장바구니 소유주의 ID값입니다. - * @param menuList 장바구니에 사용될 List입니다. + * @param cartItems 장바구니에 사용될 List입니다. */ - protected Cart(String customerId, List menuList) { + protected Cart(String customerId, List cartItems) { this.customerId = customerId; - this.menuList = menuList; + this.cartItems = cartItems; } public void addMenu(Menu menu) { + addMenu(menu, 1); + } + + public void addMenu(Menu menu, int amount) { Store store = menu.getStore(); validateOtherStore(store.getId()); validateStoreOpenTime(store); - this.menuList.add(menu); + CartItem existingCartItem = getExistingCartItem(menu, store); + if (existingCartItem != null) { + CartItem updatedCartItem = existingCartItem.add(amount); + cartItems.set(cartItems.indexOf(existingCartItem), updatedCartItem); + } else { + this.cartItems.add(new CartItem(menu.getId(), store.getId(), amount)); + } } - public long getTotalPrice() { - return this.menuList.stream() - .map(Menu::getPrice) - .mapToLong(Long::valueOf) - .boxed() - .reduce(0L, Long::sum); + private CartItem getExistingCartItem(Menu menu, Store store) { + return cartItems.stream() + .filter(item -> item.getMenuId().equals(menu.getId()) && item.getStoreId().equals(store.getId())) + .findFirst() + .orElse(null); } private void validateStoreOpenTime(Store store) { @@ -72,10 +82,8 @@ private void validateOtherStore(Long menuStoreId) { } private Set getStoreIds() { - return this.menuList.stream() - .map(Menu::getStore) - .mapToLong(Store::getId) - .boxed() + return this.cartItems.stream() + .map(CartItem::getStoreId) .collect(Collectors.toSet()); } } diff --git a/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java new file mode 100644 index 00000000..b88d3dc6 --- /dev/null +++ b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java @@ -0,0 +1,46 @@ +package camp.woowak.lab.cart.domain.vo; + +import java.util.Objects; + +public class CartItem { + private final Long menuId; + private final Long storeId; + private final Integer amount; + + public CartItem(Long menuId, Long storeId, Integer amount) { + this.menuId = menuId; + this.storeId = storeId; + this.amount = amount; + } + + public Long getMenuId() { + return menuId; + } + + public Long getStoreId() { + return storeId; + } + + public Integer getAmount() { + return amount; + } + + public CartItem add(Integer increment) { + return new CartItem(menuId, storeId, amount + increment); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof CartItem item)) + return false; + return Objects.equals(menuId, item.menuId) && Objects.equals(storeId, item.storeId) + && Objects.equals(amount, item.amount); + } + + @Override + public int hashCode() { + return Objects.hash(menuId, storeId, amount); + } +} From 9f8996ed26c8febdd5963bf9bdf1b72650308a03 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 16:19:59 +0900 Subject: [PATCH 10/33] =?UTF-8?q?[fix]=20=EC=B9=B4=ED=8A=B8=EC=97=90=20?= =?UTF-8?q?=EB=8B=B4=EA=B8=B4=20=EC=83=81=ED=92=88=EC=9D=98=20=EC=B4=9D?= =?UTF-8?q?=ED=95=A9=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=98=EA=B8=B0=20?= =?UTF-8?q?=EC=9C=84=ED=95=B4=EC=84=9C=EB=8A=94=20Repository=EB=A5=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상품의 가격은 변동될 수 있습니다. 카트에 담긴 상품의 총합은 (카트에 담긴 시점의 가격이 아니라) 변동된 가격을 기준으로 계산되어야 합니다. --- .../woowak/lab/cart/service/CartService.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/main/java/camp/woowak/lab/cart/service/CartService.java b/src/main/java/camp/woowak/lab/cart/service/CartService.java index 4841ac81..037f4473 100644 --- a/src/main/java/camp/woowak/lab/cart/service/CartService.java +++ b/src/main/java/camp/woowak/lab/cart/service/CartService.java @@ -1,8 +1,11 @@ package camp.woowak.lab.cart.service; +import java.util.List; + import org.springframework.stereotype.Service; import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.cart.exception.MenuNotFoundException; import camp.woowak.lab.cart.repository.CartRepository; import camp.woowak.lab.cart.service.command.AddCartCommand; @@ -36,8 +39,18 @@ public void addMenu(AddCartCommand command) { } public long getTotalPrice(CartTotalPriceCommand command) { - return getCart(command.customerId()) - .getTotalPrice(); + Cart cart = getCart(command.customerId()); + List findMenus = menuRepository.findAllById( + cart.getCartItems().stream().map(CartItem::getMenuId).toList()); + long totalPrice = 0L; + for (Menu findMenu : findMenus) { + for (CartItem item : cart.getCartItems()) { + if (item.getMenuId().equals(findMenu.getId())) { + totalPrice += (long)findMenu.getPrice() * item.getAmount(); + } + } + } + return totalPrice; } private Cart getCart(String customerId) { From 61a98e1c14a5f7d6f34189e00869700620025d29 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 16:22:29 +0900 Subject: [PATCH 11/33] =?UTF-8?q?[test]=20Cart=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cart.getTotalPrice 삭제로 인한 테스트 코드 수정 Menu -> CartItem 으로 인한 테스트 코드 수정 --- .../camp/woowak/lab/cart/domain/CartTest.java | 63 +++++-------------- .../lab/cart/service/CartServiceTest.java | 3 +- 2 files changed, 17 insertions(+), 49 deletions(-) diff --git a/src/test/java/camp/woowak/lab/cart/domain/CartTest.java b/src/test/java/camp/woowak/lab/cart/domain/CartTest.java index a69a4586..db4189db 100644 --- a/src/test/java/camp/woowak/lab/cart/domain/CartTest.java +++ b/src/test/java/camp/woowak/lab/cart/domain/CartTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.cart.exception.OtherStoreMenuException; import camp.woowak.lab.cart.exception.StoreNotOpenException; import camp.woowak.lab.fixture.CartFixture; @@ -26,7 +27,7 @@ class CartTest implements CartFixture { private final String customerId = UUID.randomUUID().toString(); - private List cartList; + private List cartItemList; private Cart cart; private Menu menu; private int minPrice = 8000; @@ -35,8 +36,8 @@ class CartTest implements CartFixture { @BeforeEach void setUp() { - cartList = new LinkedList<>(); - cart = new Cart(customerId, cartList); + cartItemList = new LinkedList<>(); + cart = new Cart(customerId, cartItemList); vendor = createSavedVendor(UUID.randomUUID(), new PayAccount(), new NoOpPasswordEncoder()); store = createSavedStore(1L, vendor, "중화반점", minPrice, @@ -46,7 +47,7 @@ void setUp() { } @Nested - @DisplayName("addMenu 메서드") + @DisplayName("addMenu 메서드는") class AddMenuTest { @Test @DisplayName("Menu를 받으면 cart에 저장된다.") @@ -57,7 +58,9 @@ void addMenuTest() { cart.addMenu(menu); //then - assertThat(cartList.get(0)).isEqualTo(menu); + assertThat(cartItemList).hasSize(1); + assertThat(cartItemList.get(0).getMenuId()).isEqualTo(menu.getId()); + assertThat(cartItemList.get(0).getAmount()).isEqualTo(1); } @Test @@ -72,7 +75,7 @@ void storeNotOpenExceptionTest() { //when & then assertThatThrownBy(() -> cart.addMenu(closedMenu)) .isExactlyInstanceOf(StoreNotOpenException.class); - assertThat(cartList).isEmpty(); + assertThat(cartItemList).isEmpty(); } @Test @@ -81,7 +84,7 @@ void otherStoreMenuExceptionTest() { //given cart.addMenu(menu); - Store otherStore = createSavedStore(2L, vendor, "closed", minPrice, + Store otherStore = createSavedStore(2L, vendor, "otherStore", minPrice, LocalDateTime.now().minusMinutes(30).withSecond(0).withNano(0), LocalDateTime.now().plusMinutes(30).withSecond(0).withNano(0)); Menu otherStoreMenu = createSavedMenu(2L, otherStore, new MenuCategory(otherStore, "중식"), "짬뽕", 9000); @@ -89,48 +92,12 @@ void otherStoreMenuExceptionTest() { //when & then assertThatThrownBy(() -> cart.addMenu(otherStoreMenu)) .isExactlyInstanceOf(OtherStoreMenuException.class); - assertThat(cartList).doesNotContain(otherStoreMenu); - assertThat(cartList).contains(menu); - } - } - - @Nested - @DisplayName("totalPrice 메서드는") - class GetTotalPriceTest { - @Test - @DisplayName("장바구니가 비어있으면 0원을 return한다.") - void getTotalPriceWithEmptyList() { - //given - - //when - long totalPrice = cart.getTotalPrice(); - - //then - assertThat(totalPrice).isEqualTo(0); - } - - @Test - @DisplayName("현재 장바구니에 담긴 모든 메뉴의 총 금액을 return 받는다.") - void getTotalPriceTest() { - //given - MenuCategory menuCategory = new MenuCategory(store, "중식"); - int price1 = 1000; - Menu menu1 = createSavedMenu(2L, store, menuCategory, "짜장면1", price1); - cart.addMenu(menu1); - - int price2 = 2000; - Menu menu2 = createSavedMenu(3L, store, menuCategory, "짬뽕1", price2); - cart.addMenu(menu2); - - int price3 = Integer.MAX_VALUE; - Menu menu3 = createSavedMenu(4L, store, menuCategory, "황제정식", price3); - cart.addMenu(menu3); - - //when - long totalPrice = cart.getTotalPrice(); //then - assertThat(totalPrice).isEqualTo((long)price1 + (long)price2 + (long)price3); + assertThat(cartItemList).hasSize(1); + assertThat(cartItemList) + .containsExactly(new CartItem(menu.getId(), store.getId(), 1)) + .doesNotContain(new CartItem(otherStoreMenu.getId(), otherStore.getId(), 1)); } } -} \ No newline at end of file +} diff --git a/src/test/java/camp/woowak/lab/cart/service/CartServiceTest.java b/src/test/java/camp/woowak/lab/cart/service/CartServiceTest.java index 51919fa5..f6e29f62 100644 --- a/src/test/java/camp/woowak/lab/cart/service/CartServiceTest.java +++ b/src/test/java/camp/woowak/lab/cart/service/CartServiceTest.java @@ -13,6 +13,7 @@ import org.springframework.boot.test.context.SpringBootTest; import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.cart.exception.MenuNotFoundException; import camp.woowak.lab.cart.exception.OtherStoreMenuException; import camp.woowak.lab.cart.exception.StoreNotOpenException; @@ -100,7 +101,7 @@ void addMenuInCartWhenMenuExists() { Optional cart = cartRepository.findByCustomerId(customer.getId().toString()); assertThat(cart).isPresent(); Cart cartList = cart.get(); - assertThat(cartList.getMenuList()).contains(menu); + assertThat(cartList.getCartItems()).contains(new CartItem(menu.getId(), store.getId(), 1)); } @Test From 20bfb3b77322dd75b688c279db496d0e28693445 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 17:30:51 +0900 Subject: [PATCH 12/33] =?UTF-8?q?[feat]=20Order=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문 생성시 1)동일한 가게의 메뉴에 대한 주문인지 2)재고가 있는지 검증 후 생성한다. --- .../woowak/lab/cart/domain/vo/CartItem.java | 4 ++-- .../order/domain/CompositeOrderValidator.java | 21 ----------------- .../camp/woowak/lab/order/domain/Order.java | 13 +++++++---- .../lab/order/domain/OrderValidator.java | 10 -------- .../lab/order/domain/PayAmountValidator.java | 23 ------------------- .../lab/order/domain/StockValidator.java | 16 ------------- .../woowak/lab/order/domain/vo/OrderItem.java | 8 +++++++ 7 files changed, 19 insertions(+), 76 deletions(-) delete mode 100644 src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java delete mode 100644 src/main/java/camp/woowak/lab/order/domain/OrderValidator.java delete mode 100644 src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java delete mode 100644 src/main/java/camp/woowak/lab/order/domain/StockValidator.java diff --git a/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java index b88d3dc6..2592a33d 100644 --- a/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java +++ b/src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java @@ -5,7 +5,7 @@ public class CartItem { private final Long menuId; private final Long storeId; - private final Integer amount; + private final int amount; public CartItem(Long menuId, Long storeId, Integer amount) { this.menuId = menuId; @@ -21,7 +21,7 @@ public Long getStoreId() { return storeId; } - public Integer getAmount() { + public int getAmount() { return amount; } diff --git a/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java deleted file mode 100644 index 1b179071..00000000 --- a/src/main/java/camp/woowak/lab/order/domain/CompositeOrderValidator.java +++ /dev/null @@ -1,21 +0,0 @@ -package camp.woowak.lab.order.domain; - -import java.util.List; - -import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; - -public class CompositeOrderValidator implements OrderValidator { - private final List orderValidators; - - public CompositeOrderValidator(List orderValidators) { - this.orderValidators = orderValidators; - } - - @Override - public void check(Customer requester, List orderedMenus) { - for (OrderValidator orderValidator : orderValidators) { - orderValidator.check(requester, orderedMenus); - } - } -} diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index f02b70a0..a0eae2d4 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -3,8 +3,8 @@ import java.util.ArrayList; import java.util.List; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; import camp.woowak.lab.order.domain.vo.OrderItem; import camp.woowak.lab.store.domain.Store; import jakarta.persistence.CollectionTable; @@ -35,10 +35,15 @@ public class Order { @ElementCollection(fetch = FetchType.EAGER) private List orderItems = new ArrayList<>(); - public Order(Customer requester, List orderedMenus, OrderValidator orderValidator) { - orderValidator.check(requester, orderedMenus); + public Order(Customer requester, Store store, List cartItems, SingleStoreOrderValidator validator, + StockRequester stockRequester) { + validator.check(store, cartItems); + stockRequester.request(cartItems); this.requester = requester; - this.store = orderedMenus.get(0).getStore(); + this.store = store; + for (CartItem cartItem : cartItems) { + orderItems.add(new OrderItem(cartItem.getMenuId(), cartItem.getAmount())); + } } public Long getId() { diff --git a/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java deleted file mode 100644 index ac12c17f..00000000 --- a/src/main/java/camp/woowak/lab/order/domain/OrderValidator.java +++ /dev/null @@ -1,10 +0,0 @@ -package camp.woowak.lab.order.domain; - -import java.util.List; - -import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; - -public interface OrderValidator { - void check(Customer requester, List orderedMenus); -} diff --git a/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java b/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java deleted file mode 100644 index 3b792055..00000000 --- a/src/main/java/camp/woowak/lab/order/domain/PayAmountValidator.java +++ /dev/null @@ -1,23 +0,0 @@ -package camp.woowak.lab.order.domain; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; -import camp.woowak.lab.order.exception.NotEnoughBalanceException; - -@Component -public class PayAmountValidator implements OrderValidator { - @Override - public void check(Customer requester, List orderedMenus) { - Integer totalPrice = 0; - for (Menu orderedMenu : orderedMenus) { - totalPrice += orderedMenu.getPrice(); - } - if (requester.getPayAccount().getBalance() < totalPrice) { - throw new NotEnoughBalanceException("구매자 " + requester.getId() + "가 잔고가 부족하여 주문에 실패했습니다."); - } - } -} diff --git a/src/main/java/camp/woowak/lab/order/domain/StockValidator.java b/src/main/java/camp/woowak/lab/order/domain/StockValidator.java deleted file mode 100644 index 3a4a235f..00000000 --- a/src/main/java/camp/woowak/lab/order/domain/StockValidator.java +++ /dev/null @@ -1,16 +0,0 @@ -package camp.woowak.lab.order.domain; - -import java.util.List; - -import org.springframework.stereotype.Component; - -import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; - -@Component -public class StockValidator implements OrderValidator { - @Override - public void check(Customer requester, List orderedMenus) { - - } -} diff --git a/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java index d1a36afc..e910ffe4 100644 --- a/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java +++ b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java @@ -6,4 +6,12 @@ public class OrderItem { private Long menuId; private int quantity; + + protected OrderItem() { + } + + public OrderItem(Long menuId, int quantity) { + this.menuId = menuId; + this.quantity = quantity; + } } From 681e0f0e55a0fe9ffb8e1be2dfaf95677e1a4e68 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 21:37:51 +0900 Subject: [PATCH 13/33] =?UTF-8?q?[feat]=20Menu=20=EC=9E=AC=EA=B3=A0=20?= =?UTF-8?q?=EC=82=AD=EA=B0=90=20=EC=84=9C=EB=B9=84=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문 생성시 해당 메뉴의 재고를 삭감하는 서비스 --- .../camp/woowak/lab/menu/domain/Menu.java | 3 ++ .../lab/menu/repository/MenuRepository.java | 7 +++++ .../lab/order/domain/StockRequester.java | 29 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/order/domain/StockRequester.java diff --git a/src/main/java/camp/woowak/lab/menu/domain/Menu.java b/src/main/java/camp/woowak/lab/menu/domain/Menu.java index 9219b30b..499a0b04 100644 --- a/src/main/java/camp/woowak/lab/menu/domain/Menu.java +++ b/src/main/java/camp/woowak/lab/menu/domain/Menu.java @@ -58,4 +58,7 @@ public Long getId() { return id; } + public void decrementStockCount(int amount) { + stockCount -= amount; + } } diff --git a/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java index e4d7e107..a1de78e7 100644 --- a/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java +++ b/src/main/java/camp/woowak/lab/menu/repository/MenuRepository.java @@ -1,14 +1,21 @@ package camp.woowak.lab.menu.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import camp.woowak.lab.menu.domain.Menu; +import jakarta.persistence.LockModeType; public interface MenuRepository extends JpaRepository { @Query("SELECT m FROM Menu m JOIN FETCH m.store WHERE m.id = :id") Optional findByIdWithStore(@Param("id") Long id); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT m FROM Menu m where m.id in :ids") + List findAllByIdForUpdate(List ids); } diff --git a/src/main/java/camp/woowak/lab/order/domain/StockRequester.java b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java new file mode 100644 index 00000000..a86ffdeb --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java @@ -0,0 +1,29 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.repository.MenuRepository; + +@Component +public class StockRequester { + private final MenuRepository menuRepository; + + public StockRequester(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + public void request(List cartItems) { + List allById = menuRepository.findAllByIdForUpdate(cartItems.stream().map(CartItem::getMenuId).toList()); + for (Menu menu : allById) { + for (CartItem cartItem : cartItems) { + if (cartItem.getMenuId().equals(menu.getId())) { + menu.decrementStockCount(cartItem.getAmount()); + } + } + } + } +} From 2b99e662d570f17a31d1a50e5ac3bd9587995115 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 21:38:24 +0900 Subject: [PATCH 14/33] =?UTF-8?q?[feat]=20Menu=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문은 같은 가게의 메뉴만 주문할 수 있다. --- .../order/domain/SingleStoreOrderValidator.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java index 372809f7..16404cb7 100644 --- a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java +++ b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java @@ -4,21 +4,19 @@ import org.springframework.stereotype.Component; -import camp.woowak.lab.customer.domain.Customer; -import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.store.domain.Store; @Component -public class SingleStoreOrderValidator implements OrderValidator { - @Override - public void check(Customer requester, List orderedMenus) { - if (orderedMenus == null || orderedMenus.isEmpty()) { +public class SingleStoreOrderValidator { + public void check(Store store, List cartItems) { + if (cartItems == null || cartItems.isEmpty()) { throw new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다."); } - Long storeId = orderedMenus.get(0).getStore().getId(); - for (Menu orderedMenu : orderedMenus) { - if (!orderedMenu.getStore().getId().equals(storeId)) { + for (CartItem cartItem : cartItems) { + if (!cartItem.getStoreId().equals(store.getId())) { throw new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다."); } } From cb113bd7efc6613b78e6314b9b69856f07f70047 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 21:43:28 +0900 Subject: [PATCH 15/33] =?UTF-8?q?[feat]=20=EC=A3=BC=EB=AC=B8=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=84=9C=EB=B9=84=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문은 1)같은 가게의 메뉴에 대해 2)재고가 있고 3)결제할 포인트가 충분할 때 진행할 수 있다. --- .../order/service/OrderCreationService.java | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index a5ce0965..593891df 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -8,41 +8,55 @@ import org.springframework.transaction.annotation.Transactional; import camp.woowak.lab.cart.domain.Cart; +import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.cart.repository.CartRepository; import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; -import camp.woowak.lab.order.domain.CompositeOrderValidator; import camp.woowak.lab.order.domain.Order; -import camp.woowak.lab.order.domain.OrderValidator; +import camp.woowak.lab.order.domain.SingleStoreOrderValidator; +import camp.woowak.lab.order.domain.StockRequester; import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.repository.OrderRepository; import camp.woowak.lab.order.service.command.OrderCreationCommand; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.exception.NotFoundStoreException; +import camp.woowak.lab.store.repository.StoreRepository; @Service @Transactional public class OrderCreationService { private final OrderRepository orderRepository; private final CartRepository cartRepository; + private final StoreRepository storeRepository; private final CustomerRepository customerRepository; - private final CompositeOrderValidator orderValidators; + private final SingleStoreOrderValidator singleStoreOrderValidator; + private final StockRequester stockRequester; public OrderCreationService(OrderRepository orderRepository, CartRepository cartRepository, - CustomerRepository customerRepository, List orderValidators) { + StoreRepository storeRepository, CustomerRepository customerRepository, + SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester) { this.orderRepository = orderRepository; this.cartRepository = cartRepository; + this.storeRepository = storeRepository; this.customerRepository = customerRepository; - this.orderValidators = new CompositeOrderValidator(orderValidators); + this.singleStoreOrderValidator = singleStoreOrderValidator; + this.stockRequester = stockRequester; } public Long create(OrderCreationCommand cmd) { UUID requesterId = cmd.requesterId(); Customer requester = customerRepository.findByIdOrThrow(requesterId); Optional findCart = cartRepository.findByCustomerId(requesterId.toString()); - Cart cart; - if (findCart.isEmpty() || (cart = findCart.get()).getMenuList().isEmpty()) { + List cartItems; + if (findCart.isEmpty() || (cartItems = findCart.get().getCartItems()) == null || cartItems.isEmpty()) { throw new EmptyCartException("구매자 " + cmd.requesterId() + "가 비어있는 카트로 주문을 시도했습니다."); } - Order savedOrder = orderRepository.save(new Order(requester, cart.getMenuList(), orderValidators)); + Optional findStore = storeRepository.findById(cartItems.get(0).getStoreId()); + if (findStore.isEmpty()) { + throw new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다."); + } + Order savedOrder = orderRepository.save( + new Order(requester, findStore.get(), cartItems, singleStoreOrderValidator, stockRequester)); return savedOrder.getId(); } } From 001c0b3952454031f8b454b0e606df234ac85c64 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 22:06:19 +0900 Subject: [PATCH 16/33] =?UTF-8?q?[feat]=20PriceChecker=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Menu의 현재 가격으로 OrderItem을 만들어주는 검증 서비스 --- .../camp/woowak/lab/order/domain/Order.java | 7 ++-- .../woowak/lab/order/domain/PriceChecker.java | 38 +++++++++++++++++++ .../woowak/lab/order/domain/vo/OrderItem.java | 10 ++++- .../exception/NotFoundMenuException.java | 9 +++++ .../lab/order/exception/OrderErrorCode.java | 7 ++-- 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/order/domain/PriceChecker.java create mode 100644 src/main/java/camp/woowak/lab/order/exception/NotFoundMenuException.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index a0eae2d4..bf1337d8 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -36,14 +36,13 @@ public class Order { private List orderItems = new ArrayList<>(); public Order(Customer requester, Store store, List cartItems, SingleStoreOrderValidator validator, - StockRequester stockRequester) { + StockRequester stockRequester, PriceChecker priceChecker) { validator.check(store, cartItems); stockRequester.request(cartItems); + List orderItem = priceChecker.check(cartItems); this.requester = requester; this.store = store; - for (CartItem cartItem : cartItems) { - orderItems.add(new OrderItem(cartItem.getMenuId(), cartItem.getAmount())); - } + this.orderItems = orderItem; } public Long getId() { diff --git a/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java new file mode 100644 index 00000000..259dba1b --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java @@ -0,0 +1,38 @@ +package camp.woowak.lab.order.domain; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.menu.domain.Menu; +import camp.woowak.lab.menu.repository.MenuRepository; +import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.order.exception.NotFoundMenuException; + +@Component +public class PriceChecker { + private final MenuRepository menuRepository; + + public PriceChecker(MenuRepository menuRepository) { + this.menuRepository = menuRepository; + } + + public List check(List cartItems) { + List menus = menuRepository.findAllById(cartItems.stream().map(CartItem::getMenuId).toList()); + if (menus.size() != cartItems.size()) { + throw new NotFoundMenuException("등록되지 않은 메뉴를 주문했습니다."); + } + List orderItems = new ArrayList<>(); + for (Menu menu : menus) { + for (CartItem cartItem : cartItems) { + if (cartItem.getMenuId().equals(menu.getId())) { + OrderItem orderItem = new OrderItem(menu.getId(), menu.getPrice(), cartItem.getAmount()); + orderItems.add(orderItem); + } + } + } + return orderItems; + } +} diff --git a/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java index e910ffe4..89098567 100644 --- a/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java +++ b/src/main/java/camp/woowak/lab/order/domain/vo/OrderItem.java @@ -5,13 +5,21 @@ @Embeddable public class OrderItem { private Long menuId; + private int price; private int quantity; + private int totalPrice; protected OrderItem() { } - public OrderItem(Long menuId, int quantity) { + public OrderItem(Long menuId, int price, int quantity) { this.menuId = menuId; + this.price = price; this.quantity = quantity; + this.totalPrice = price * quantity; + } + + public int getTotalPrice() { + return totalPrice; } } diff --git a/src/main/java/camp/woowak/lab/order/exception/NotFoundMenuException.java b/src/main/java/camp/woowak/lab/order/exception/NotFoundMenuException.java new file mode 100644 index 00000000..27c575bc --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/NotFoundMenuException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class NotFoundMenuException extends BadRequestException { + public NotFoundMenuException(String message) { + super(OrderErrorCode.NOT_FOUND_MENU, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java index 344b9488..7b26c45c 100644 --- a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java +++ b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java @@ -5,9 +5,10 @@ import camp.woowak.lab.common.exception.ErrorCode; public enum OrderErrorCode implements ErrorCode { - EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1_1", "장바구니가 비어있습니다."), - MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_2", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."), - NOT_ENOUGH_BALANCE(HttpStatus.BAD_REQUEST, "o_1_2", "잔고가 부족합니다."); + EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1_0", "장바구니가 비어있습니다."), + MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_1", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."), + NOT_ENOUGH_BALANCE(HttpStatus.BAD_REQUEST, "o_1_2", "잔고가 부족합니다."), + NOT_FOUND_MENU(HttpStatus.BAD_REQUEST, "o_1_4", "없는 메뉴입니다."); private final int status; private final String errorCode; From dba30132c38a12a9a3dcd36a1d89cf746f120936 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 22:10:45 +0900 Subject: [PATCH 17/33] =?UTF-8?q?[feat]=20=EC=A3=BC=EB=AC=B8=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 주문 금액만큼 사용자의 계좌에서 차감하는 서비스 --- .../camp/woowak/lab/order/domain/Order.java | 12 ++++--- .../order/domain/WithdrawPointService.java | 35 +++++++++++++++++++ .../order/service/OrderCreationService.java | 12 +++++-- 3 files changed, 52 insertions(+), 7 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/order/domain/WithdrawPointService.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index bf1337d8..8e8dd488 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -35,14 +35,16 @@ public class Order { @ElementCollection(fetch = FetchType.EAGER) private List orderItems = new ArrayList<>(); - public Order(Customer requester, Store store, List cartItems, SingleStoreOrderValidator validator, - StockRequester stockRequester, PriceChecker priceChecker) { - validator.check(store, cartItems); + public Order(Customer requester, Store store, List cartItems, + SingleStoreOrderValidator singleStoreOrderValidator, + StockRequester stockRequester, PriceChecker priceChecker, WithdrawPointService withdrawPointService) { + singleStoreOrderValidator.check(store, cartItems); stockRequester.request(cartItems); - List orderItem = priceChecker.check(cartItems); + List orderItems = priceChecker.check(cartItems); + withdrawPointService.withdraw(requester, orderItems); this.requester = requester; this.store = store; - this.orderItems = orderItem; + this.orderItems = orderItems; } public Long getId() { diff --git a/src/main/java/camp/woowak/lab/order/domain/WithdrawPointService.java b/src/main/java/camp/woowak/lab/order/domain/WithdrawPointService.java new file mode 100644 index 00000000..d3501d5b --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/domain/WithdrawPointService.java @@ -0,0 +1,35 @@ +package camp.woowak.lab.order.domain; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; + +@Component +public class WithdrawPointService { + private final PayAccountRepository payAccountRepository; + + public WithdrawPointService(PayAccountRepository payAccountRepository) { + this.payAccountRepository = payAccountRepository; + } + + public List withdraw(Customer customer, List orderItems) { + Optional findPayAccount = payAccountRepository.findByCustomerIdForUpdate(customer.getId()); + if (findPayAccount.isEmpty()) { + throw new NotFoundAccountException("주문을 처리할 계좌가 생성되지 않았습니다."); + } + PayAccount payAccount = findPayAccount.get(); + int totalPrice = 0; + for (OrderItem orderItem : orderItems) { + totalPrice += orderItem.getTotalPrice(); + } + payAccount.withdraw(totalPrice); + return orderItems; + } +} diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index 593891df..3873ade0 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -13,8 +13,10 @@ import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; import camp.woowak.lab.order.domain.Order; +import camp.woowak.lab.order.domain.PriceChecker; import camp.woowak.lab.order.domain.SingleStoreOrderValidator; import camp.woowak.lab.order.domain.StockRequester; +import camp.woowak.lab.order.domain.WithdrawPointService; import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.repository.OrderRepository; import camp.woowak.lab.order.service.command.OrderCreationCommand; @@ -31,16 +33,21 @@ public class OrderCreationService { private final CustomerRepository customerRepository; private final SingleStoreOrderValidator singleStoreOrderValidator; private final StockRequester stockRequester; + private final WithdrawPointService withdrawPointService; + private final PriceChecker priceChecker; public OrderCreationService(OrderRepository orderRepository, CartRepository cartRepository, StoreRepository storeRepository, CustomerRepository customerRepository, - SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester) { + SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester, + WithdrawPointService withdrawPointService, PriceChecker priceChecker) { this.orderRepository = orderRepository; this.cartRepository = cartRepository; this.storeRepository = storeRepository; this.customerRepository = customerRepository; this.singleStoreOrderValidator = singleStoreOrderValidator; this.stockRequester = stockRequester; + this.withdrawPointService = withdrawPointService; + this.priceChecker = priceChecker; } public Long create(OrderCreationCommand cmd) { @@ -56,7 +63,8 @@ public Long create(OrderCreationCommand cmd) { throw new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다."); } Order savedOrder = orderRepository.save( - new Order(requester, findStore.get(), cartItems, singleStoreOrderValidator, stockRequester)); + new Order(requester, findStore.get(), cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + withdrawPointService)); return savedOrder.getId(); } } From 141ff38e0b80cf3e6b4541d38c6f4b585b01e5df Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 22:11:18 +0900 Subject: [PATCH 18/33] =?UTF-8?q?[refactor]=20=EC=9E=90=EB=B0=94=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/camp/woowak/lab/cart/exception/CartErrorCode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java b/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java index f22a6ec5..ca3ed18a 100644 --- a/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java +++ b/src/main/java/camp/woowak/lab/cart/exception/CartErrorCode.java @@ -6,8 +6,8 @@ public enum CartErrorCode implements ErrorCode { MENU_NOT_FOUND(HttpStatus.NOT_FOUND, "ca_1_1", "해당 메뉴가 존재하지 않습니다."), - OTHER_STORE_MENU(HttpStatus.BAD_REQUEST,"ca_1_2","다른 매장의 메뉴는 등록할 수 없습니다."), - STORE_NOT_OPEN(HttpStatus.BAD_REQUEST,"ca_1_3","주문 가능한 시간이 아닙니다."); + OTHER_STORE_MENU(HttpStatus.BAD_REQUEST, "ca_1_2", "다른 매장의 메뉴는 등록할 수 없습니다."), + STORE_NOT_OPEN(HttpStatus.BAD_REQUEST, "ca_1_3", "주문 가능한 시간이 아닙니다."); private final int status; private final String errorCode; From 82b8c3d5cee4d250843856575eb5870762d38e39 Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 22:58:17 +0900 Subject: [PATCH 19/33] =?UTF-8?q?[refactor]=20OrderCreationService=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional.orElseThrow() 활용해서 if-else문 제거 --- .../order/service/OrderCreationService.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index 3873ade0..e5122e4f 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -1,7 +1,6 @@ package camp.woowak.lab.order.service; import java.util.List; -import java.util.Optional; import java.util.UUID; import org.springframework.stereotype.Service; @@ -53,18 +52,22 @@ public OrderCreationService(OrderRepository orderRepository, CartRepository cart public Long create(OrderCreationCommand cmd) { UUID requesterId = cmd.requesterId(); Customer requester = customerRepository.findByIdOrThrow(requesterId); - Optional findCart = cartRepository.findByCustomerId(requesterId.toString()); - List cartItems; - if (findCart.isEmpty() || (cartItems = findCart.get().getCartItems()) == null || cartItems.isEmpty()) { - throw new EmptyCartException("구매자 " + cmd.requesterId() + "가 비어있는 카트로 주문을 시도했습니다."); - } - Optional findStore = storeRepository.findById(cartItems.get(0).getStoreId()); - if (findStore.isEmpty()) { - throw new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다."); + + Cart cart = cartRepository.findByCustomerId(requesterId.toString()) + .orElseThrow(() -> new EmptyCartException("구매자 " + requesterId + "가 비어있는 카트로 주문을 시도했습니다.")); + List cartItems = cart.getCartItems(); + + if (cartItems == null || cartItems.isEmpty()) { + throw new EmptyCartException("구매자 " + requesterId + "가 비어있는 카트로 주문을 시도했습니다."); } + + Store store = storeRepository.findById(cartItems.get(0).getStoreId()) + .orElseThrow(() -> new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다.")); + Order savedOrder = orderRepository.save( - new Order(requester, findStore.get(), cartItems, singleStoreOrderValidator, stockRequester, priceChecker, - withdrawPointService)); + new Order(requester, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + withdrawPointService) + ); return savedOrder.getId(); } } From 677454001f97d5d4cf0873d972fc4d50b33ae16f Mon Sep 17 00:00:00 2001 From: donghar Date: Fri, 16 Aug 2024 23:20:06 +0900 Subject: [PATCH 20/33] =?UTF-8?q?[test]=20OrderCreationservice=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../camp/woowak/lab/order/domain/Order.java | 4 + .../woowak/lab/order/domain/OrderTest.java | 82 +++++++++++++ .../domain/SingleStoreOrderValidatorTest.java | 79 ++++++++++++ .../domain/WithdrawPointServiceTest.java | 69 +++++++++++ .../service/OrderCreationServiceTest.java | 112 ++++++++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/order/domain/OrderTest.java create mode 100644 src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java create mode 100644 src/test/java/camp/woowak/lab/order/domain/WithdrawPointServiceTest.java create mode 100644 src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index 8e8dd488..c4c90f7f 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -50,4 +50,8 @@ public Order(Customer requester, Store store, List cartItems, public Long getId() { return id; } + + public List getOrderItems() { + return orderItems; + } } diff --git a/src/test/java/camp/woowak/lab/order/domain/OrderTest.java b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java new file mode 100644 index 00000000..c1864c5b --- /dev/null +++ b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java @@ -0,0 +1,82 @@ +package camp.woowak.lab.order.domain; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.store.domain.Store; + +class OrderTest { + + private SingleStoreOrderValidator singleStoreOrderValidator; + private StockRequester stockRequester; + private PriceChecker priceChecker; + private WithdrawPointService withdrawPointService; + + @BeforeEach + void setUp() { + singleStoreOrderValidator = mock(SingleStoreOrderValidator.class); + stockRequester = mock(StockRequester.class); + priceChecker = mock(PriceChecker.class); + withdrawPointService = mock(WithdrawPointService.class); + } + + @Test + void createOrder_ValidInputs_Success() { + // Given + Customer customer = mock(Customer.class); + Store store = mock(Store.class); + CartItem cartItem = mock(CartItem.class); + List cartItems = List.of(cartItem); + + OrderItem orderItem = mock(OrderItem.class); + List orderItems = List.of(orderItem); + + // Mocking behavior + doNothing().when(singleStoreOrderValidator).check(store, cartItems); + doNothing().when(stockRequester).request(cartItems); + when(priceChecker.check(cartItems)).thenReturn(orderItems); + when(withdrawPointService.withdraw(customer, orderItems)).thenReturn(orderItems); + + // When + Order order = new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + withdrawPointService); + + // Then + assertEquals(orderItems, order.getOrderItems()); + + verify(singleStoreOrderValidator, times(1)).check(store, cartItems); + verify(stockRequester, times(1)).request(cartItems); + verify(priceChecker, times(1)).check(cartItems); + verify(withdrawPointService, times(1)).withdraw(customer, orderItems); + } + + @Test + void createOrder_InvalidStore_ThrowsException() { + // Given + Customer customer = mock(Customer.class); + Store store = mock(Store.class); + CartItem cartItem = mock(CartItem.class); + List cartItems = List.of(cartItem); + + // Mock behavior to throw exception + doThrow(new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다.")) + .when(singleStoreOrderValidator).check(store, cartItems); + + // When & Then + MultiStoreOrderException exception = assertThrows(MultiStoreOrderException.class, () -> { + new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + withdrawPointService); + }); + + assertEquals("다른 가게의 메뉴를 같이 주문할 수 없습니다.", exception.getMessage()); + } +} diff --git a/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java b/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java new file mode 100644 index 00000000..1ddd8c27 --- /dev/null +++ b/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java @@ -0,0 +1,79 @@ +package camp.woowak.lab.order.domain; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.store.domain.Store; + +class SingleStoreOrderValidatorTest { + + private SingleStoreOrderValidator validator; + + @BeforeEach + void setUp() { + validator = new SingleStoreOrderValidator(); + } + + @Test + void check_NullCartItems_ThrowsEmptyCartException() { + // Given + Store store = mock(Store.class); + + // When & Then + assertThrows(EmptyCartException.class, () -> validator.check(store, null)); + } + + @Test + void check_EmptyCartItems_ThrowsEmptyCartException() { + // Given + Store store = mock(Store.class); + + // When & Then + assertThrows(EmptyCartException.class, () -> validator.check(store, List.of())); + } + + @Test + void check_AllItemsBelongToSameStore_NoExceptionThrown() { + // Given + Store store = mock(Store.class); + Long storeId = 1L; + when(store.getId()).thenReturn(storeId); + + CartItem item1 = mock(CartItem.class); + CartItem item2 = mock(CartItem.class); + when(item1.getStoreId()).thenReturn(storeId); + when(item2.getStoreId()).thenReturn(storeId); + + List cartItems = List.of(item1, item2); + + // When & Then + assertDoesNotThrow(() -> validator.check(store, cartItems)); + } + + @Test + void check_ItemsFromDifferentStores_ThrowsMultiStoreOrderException() { + // Given + Store store = mock(Store.class); + Long storeId1 = 1L; + Long storeId2 = 2L; + when(store.getId()).thenReturn(storeId1); + + CartItem item1 = mock(CartItem.class); + CartItem item2 = mock(CartItem.class); + when(item1.getStoreId()).thenReturn(storeId1); + when(item2.getStoreId()).thenReturn(storeId2); + + List cartItems = List.of(item1, item2); + + // When & Then + assertThrows(MultiStoreOrderException.class, () -> validator.check(store, cartItems)); + } +} diff --git a/src/test/java/camp/woowak/lab/order/domain/WithdrawPointServiceTest.java b/src/test/java/camp/woowak/lab/order/domain/WithdrawPointServiceTest.java new file mode 100644 index 00000000..f98ec0cc --- /dev/null +++ b/src/test/java/camp/woowak/lab/order/domain/WithdrawPointServiceTest.java @@ -0,0 +1,69 @@ +package camp.woowak.lab.order.domain; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.payaccount.domain.PayAccount; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.payaccount.repository.PayAccountRepository; + +class WithdrawPointServiceTest { + + private WithdrawPointService withdrawPointService; + private PayAccountRepository payAccountRepository; + + @BeforeEach + void setUp() { + payAccountRepository = mock(PayAccountRepository.class); + withdrawPointService = new WithdrawPointService(payAccountRepository); + } + + @Test + void withdraw_NoPayAccount_ThrowsNotFoundAccountException() { + // Given + Customer customer = mock(Customer.class); + UUID customerId = UUID.randomUUID(); + when(customer.getId()).thenReturn(customerId); + + when(payAccountRepository.findByCustomerIdForUpdate(customerId)) + .thenReturn(Optional.empty()); + + // When & Then + assertThrows(NotFoundAccountException.class, () -> withdrawPointService.withdraw(customer, List.of())); + } + + @Test + void withdraw_ValidPayAccount_WithdrawsPointsSuccessfully() { + // Given + Customer customer = mock(Customer.class); + UUID customerId = UUID.randomUUID(); + when(customer.getId()).thenReturn(customerId); + + PayAccount payAccount = mock(PayAccount.class); + when(payAccountRepository.findByCustomerIdForUpdate(customerId)) + .thenReturn(Optional.of(payAccount)); + + OrderItem orderItem1 = mock(OrderItem.class); + OrderItem orderItem2 = mock(OrderItem.class); + when(orderItem1.getTotalPrice()).thenReturn(500); + when(orderItem2.getTotalPrice()).thenReturn(1500); + + List orderItems = List.of(orderItem1, orderItem2); + + // When + List result = withdrawPointService.withdraw(customer, orderItems); + + // Then + verify(payAccount).withdraw(2000); // 500 + 1500 + assertEquals(orderItems, result); + } +} diff --git a/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java new file mode 100644 index 00000000..5257d307 --- /dev/null +++ b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java @@ -0,0 +1,112 @@ +package camp.woowak.lab.order.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; + +import camp.woowak.lab.cart.domain.vo.CartItem; +import camp.woowak.lab.cart.repository.CartRepository; +import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.order.domain.Order; +import camp.woowak.lab.order.domain.PriceChecker; +import camp.woowak.lab.order.domain.SingleStoreOrderValidator; +import camp.woowak.lab.order.domain.StockRequester; +import camp.woowak.lab.order.domain.WithdrawPointService; +import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.repository.OrderRepository; +import camp.woowak.lab.order.service.command.OrderCreationCommand; +import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.repository.StoreRepository; + +@SpringBootTest +class OrderCreationServiceTest { + + @Mock + private OrderRepository orderRepository; + @Mock + private CartRepository cartRepository; + @Mock + private StoreRepository storeRepository; + @Mock + private CustomerRepository customerRepository; + @Mock + private SingleStoreOrderValidator singleStoreOrderValidator; + @Mock + private StockRequester stockRequester; + @Mock + private WithdrawPointService withdrawPointService; + @Mock + private PriceChecker priceChecker; + + @InjectMocks + private OrderCreationService orderCreationService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createOrder_Success() { + // Given + Customer customer = mock(Customer.class); + Store store = mock(Store.class); + CartItem cartItem = mock(CartItem.class); + List cartItems = List.of(cartItem); + + // Prepare OrderItems to return from priceChecker + OrderItem orderItem = mock(OrderItem.class); + List orderItems = List.of(orderItem); + + // Mocking behavior + when(store.getId()).thenReturn(1L); // Ensure this is mocked + when(cartItem.getStoreId()).thenReturn(1L); // Mock the cartItem's storeId + doNothing().when(singleStoreOrderValidator).check(any(Store.class), anyList()); + doNothing().when(stockRequester).request(anyList()); + when(priceChecker.check(anyList())).thenReturn(orderItems); + when(withdrawPointService.withdraw(any(Customer.class), anyList())).thenReturn(orderItems); + + // When + Order order = new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + withdrawPointService); + + // Then + assertEquals(orderItems, order.getOrderItems()); + + verify(singleStoreOrderValidator, times(1)).check(store, cartItems); + verify(stockRequester, times(1)).request(cartItems); + verify(priceChecker, times(1)).check(cartItems); + verify(withdrawPointService, times(1)).withdraw(customer, orderItems); + } + + @Test + void createOrder_EmptyCart_ThrowsException() { + // Given + UUID customerId = UUID.randomUUID(); + OrderCreationCommand command = new OrderCreationCommand(customerId); + Customer customer = mock(Customer.class); + + when(customerRepository.findByIdOrThrow(customerId)).thenReturn(customer); + when(cartRepository.findByCustomerId(customerId.toString())).thenReturn(Optional.empty()); + + // When & Then + EmptyCartException exception = assertThrows(EmptyCartException.class, () -> { + orderCreationService.create(command); + }); + + assertEquals("구매자 " + customerId + "가 비어있는 카트로 주문을 시도했습니다.", exception.getMessage()); + } +} From f7662b007596260132fc563b601546b45536144e Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 09:08:38 +0900 Subject: [PATCH 21/33] =?UTF-8?q?[fix]=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EA=B5=AC=EC=B2=B4=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조회 시도한 Customer의 id를 명시 --- .../camp/woowak/lab/customer/repository/CustomerRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java index 22cf2374..51acc5d3 100644 --- a/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java +++ b/src/main/java/camp/woowak/lab/customer/repository/CustomerRepository.java @@ -12,6 +12,6 @@ public interface CustomerRepository extends JpaRepository { Optional findByEmail(String email); default Customer findByIdOrThrow(UUID id) { - return findById(id).orElseThrow(() -> new NotFoundCustomerException("존재하지 않는 사용자를 조회했습니다.")); + return findById(id).orElseThrow(() -> new NotFoundCustomerException("존재하지 않는 사용자(id=" + id + ")를 조회했습니다.")); } } From 0b8c503203dbd48cd84f6c4ecba9fe4e60f7d2f9 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 09:50:21 +0900 Subject: [PATCH 22/33] =?UTF-8?q?[refactor]=20=EA=B0=80=EB=8F=85=EC=84=B1?= =?UTF-8?q?=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../woowak/lab/order/domain/PriceChecker.java | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java index 259dba1b..cfde0b82 100644 --- a/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java +++ b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java @@ -1,7 +1,12 @@ package camp.woowak.lab.order.domain; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.stereotype.Component; @@ -20,19 +25,41 @@ public PriceChecker(MenuRepository menuRepository) { } public List check(List cartItems) { - List menus = menuRepository.findAllById(cartItems.stream().map(CartItem::getMenuId).toList()); - if (menus.size() != cartItems.size()) { - throw new NotFoundMenuException("등록되지 않은 메뉴를 주문했습니다."); + Set cartItemMenuIds = extractMenuIds(cartItems); + List menus = menuRepository.findAllById(cartItemMenuIds); + Map menuMap = listToMap(menus); + Set missingMenuIds = new HashSet<>(); + for (Long menuId : cartItemMenuIds) { + if (!menuMap.containsKey(menuId)) { + missingMenuIds.add(menuId); + } + } + if (!missingMenuIds.isEmpty()) { + String missingIdsString = formatMissingIds(missingMenuIds); + throw new NotFoundMenuException("등록되지 않은 메뉴(id=" + missingIdsString + ")를 주문했습니다."); } List orderItems = new ArrayList<>(); - for (Menu menu : menus) { - for (CartItem cartItem : cartItems) { - if (cartItem.getMenuId().equals(menu.getId())) { - OrderItem orderItem = new OrderItem(menu.getId(), menu.getPrice(), cartItem.getAmount()); - orderItems.add(orderItem); - } - } + for (CartItem cartItem : cartItems) { + Menu menu = menuMap.get(cartItem.getMenuId()); + orderItems.add(new OrderItem(menu.getId(), menu.getPrice(), cartItem.getAmount())); } return orderItems; } + + private String formatMissingIds(Set missingMenuIds) { + return missingMenuIds.stream() + .map(String::valueOf) + .collect(Collectors.joining(", ")); + } + + private Map listToMap(List menus) { + return menus.stream() + .collect(Collectors.toMap(Menu::getId, Function.identity())); + } + + private Set extractMenuIds(List cartItems) { + return cartItems.stream() + .map(CartItem::getMenuId) + .collect(Collectors.toSet()); + } } From 1f9edeaefddf35e989c4c9d8385789fa0d396953 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 11:59:03 +0900 Subject: [PATCH 23/33] =?UTF-8?q?[feat]=20=EC=B5=9C=EC=86=8C=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=EA=B8=88=EC=95=A1=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 Menu의 가격을 조회하면서 최소 주문금액 이상을 주문하였는지 확인 --- .../java/camp/woowak/lab/order/domain/Order.java | 2 +- .../camp/woowak/lab/order/domain/PriceChecker.java | 12 +++++++++++- .../exception/MinimumOrderPriceNotMetException.java | 9 +++++++++ .../woowak/lab/order/exception/OrderErrorCode.java | 3 ++- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 src/main/java/camp/woowak/lab/order/exception/MinimumOrderPriceNotMetException.java diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index c4c90f7f..f515ec07 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -40,7 +40,7 @@ public Order(Customer requester, Store store, List cartItems, StockRequester stockRequester, PriceChecker priceChecker, WithdrawPointService withdrawPointService) { singleStoreOrderValidator.check(store, cartItems); stockRequester.request(cartItems); - List orderItems = priceChecker.check(cartItems); + List orderItems = priceChecker.check(store, cartItems); withdrawPointService.withdraw(requester, orderItems); this.requester = requester; this.store = store; diff --git a/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java index cfde0b82..64b10f96 100644 --- a/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java +++ b/src/main/java/camp/woowak/lab/order/domain/PriceChecker.java @@ -14,7 +14,9 @@ import camp.woowak.lab.menu.domain.Menu; import camp.woowak.lab.menu.repository.MenuRepository; import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.order.exception.MinimumOrderPriceNotMetException; import camp.woowak.lab.order.exception.NotFoundMenuException; +import camp.woowak.lab.store.domain.Store; @Component public class PriceChecker { @@ -24,7 +26,7 @@ public PriceChecker(MenuRepository menuRepository) { this.menuRepository = menuRepository; } - public List check(List cartItems) { + public List check(Store store, List cartItems) { Set cartItemMenuIds = extractMenuIds(cartItems); List menus = menuRepository.findAllById(cartItemMenuIds); Map menuMap = listToMap(menus); @@ -43,6 +45,14 @@ public List check(List cartItems) { Menu menu = menuMap.get(cartItem.getMenuId()); orderItems.add(new OrderItem(menu.getId(), menu.getPrice(), cartItem.getAmount())); } + int totalPrice = 0; + for (OrderItem orderItem : orderItems) { + totalPrice += orderItem.getTotalPrice(); + } + if (totalPrice < store.getMinOrderPrice()) { + throw new MinimumOrderPriceNotMetException( + "가게의 최소 주문금액(" + store.getMinOrderPrice() + ")보다 적은 금액(" + totalPrice + ")을 주문했습니다."); + } return orderItems; } diff --git a/src/main/java/camp/woowak/lab/order/exception/MinimumOrderPriceNotMetException.java b/src/main/java/camp/woowak/lab/order/exception/MinimumOrderPriceNotMetException.java new file mode 100644 index 00000000..4c0b00b2 --- /dev/null +++ b/src/main/java/camp/woowak/lab/order/exception/MinimumOrderPriceNotMetException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.order.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class MinimumOrderPriceNotMetException extends BadRequestException { + public MinimumOrderPriceNotMetException(String message) { + super(OrderErrorCode.MIN_ORDER_PRICE, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java index 7b26c45c..4b9ef16a 100644 --- a/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java +++ b/src/main/java/camp/woowak/lab/order/exception/OrderErrorCode.java @@ -8,7 +8,8 @@ public enum OrderErrorCode implements ErrorCode { EMPTY_CART(HttpStatus.BAD_REQUEST, "o_1_0", "장바구니가 비어있습니다."), MULTI_STORE_ORDER(HttpStatus.BAD_REQUEST, "o_1_1", "동시에 하나의 가게에 대한 메뉴만 주문할 수 있습니다."), NOT_ENOUGH_BALANCE(HttpStatus.BAD_REQUEST, "o_1_2", "잔고가 부족합니다."), - NOT_FOUND_MENU(HttpStatus.BAD_REQUEST, "o_1_4", "없는 메뉴입니다."); + NOT_FOUND_MENU(HttpStatus.BAD_REQUEST, "o_1_4", "없는 메뉴입니다."), + MIN_ORDER_PRICE(HttpStatus.BAD_REQUEST, "o_1_5", "최소 주문금액 이상을 주문해야 합니다."); private final int status; private final String errorCode; From db9732f39db78ad26cadad08e859a60f9201eb48 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 11:59:16 +0900 Subject: [PATCH 24/33] =?UTF-8?q?[test]=20=EC=B5=9C=EC=86=8C=20=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=EA=B8=88=EC=95=A1=20=ED=99=95=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 Menu의 가격을 조회하면서 최소 주문금액 이상을 주문하였는지 확인 --- src/test/java/camp/woowak/lab/order/domain/OrderTest.java | 4 ++-- .../woowak/lab/order/service/OrderCreationServiceTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/camp/woowak/lab/order/domain/OrderTest.java b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java index c1864c5b..75a75b7a 100644 --- a/src/test/java/camp/woowak/lab/order/domain/OrderTest.java +++ b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java @@ -43,7 +43,7 @@ void createOrder_ValidInputs_Success() { // Mocking behavior doNothing().when(singleStoreOrderValidator).check(store, cartItems); doNothing().when(stockRequester).request(cartItems); - when(priceChecker.check(cartItems)).thenReturn(orderItems); + when(priceChecker.check(store, cartItems)).thenReturn(orderItems); when(withdrawPointService.withdraw(customer, orderItems)).thenReturn(orderItems); // When @@ -55,7 +55,7 @@ void createOrder_ValidInputs_Success() { verify(singleStoreOrderValidator, times(1)).check(store, cartItems); verify(stockRequester, times(1)).request(cartItems); - verify(priceChecker, times(1)).check(cartItems); + verify(priceChecker, times(1)).check(store, cartItems); verify(withdrawPointService, times(1)).withdraw(customer, orderItems); } diff --git a/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java index 5257d307..18cb1606 100644 --- a/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java +++ b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java @@ -76,7 +76,7 @@ void createOrder_Success() { when(cartItem.getStoreId()).thenReturn(1L); // Mock the cartItem's storeId doNothing().when(singleStoreOrderValidator).check(any(Store.class), anyList()); doNothing().when(stockRequester).request(anyList()); - when(priceChecker.check(anyList())).thenReturn(orderItems); + when(priceChecker.check(any(), anyList())).thenReturn(orderItems); when(withdrawPointService.withdraw(any(Customer.class), anyList())).thenReturn(orderItems); // When @@ -88,7 +88,7 @@ void createOrder_Success() { verify(singleStoreOrderValidator, times(1)).check(store, cartItems); verify(stockRequester, times(1)).request(cartItems); - verify(priceChecker, times(1)).check(cartItems); + verify(priceChecker, times(1)).check(store, cartItems); verify(withdrawPointService, times(1)).withdraw(customer, orderItems); } From f4132ccfa16bcf63a187212b26fe78208c1dae02 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 16:17:24 +0900 Subject: [PATCH 25/33] =?UTF-8?q?[refactor]=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/camp/woowak/lab/order/domain/StockRequester.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/order/domain/StockRequester.java b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java index a86ffdeb..2ad635d4 100644 --- a/src/main/java/camp/woowak/lab/order/domain/StockRequester.java +++ b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java @@ -17,7 +17,8 @@ public StockRequester(MenuRepository menuRepository) { } public void request(List cartItems) { - List allById = menuRepository.findAllByIdForUpdate(cartItems.stream().map(CartItem::getMenuId).toList()); + List cartItemMenuIds = extractMenuIds(cartItems); + List allById = menuRepository.findAllByIdForUpdate(cartItemMenuIds); for (Menu menu : allById) { for (CartItem cartItem : cartItems) { if (cartItem.getMenuId().equals(menu.getId())) { @@ -26,4 +27,8 @@ public void request(List cartItems) { } } } + + private static List extractMenuIds(List cartItems) { + return cartItems.stream().map(CartItem::getMenuId).toList(); + } } From 3fdca84c46caddd5094c163020547fddae56c671 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 16:29:15 +0900 Subject: [PATCH 26/33] =?UTF-8?q?[refactor]=20RequiredArgsConstructor=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/order/service/OrderCreationService.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index e5122e4f..f59a4687 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -22,8 +22,10 @@ import camp.woowak.lab.store.domain.Store; import camp.woowak.lab.store.exception.NotFoundStoreException; import camp.woowak.lab.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; @Service +@RequiredArgsConstructor @Transactional public class OrderCreationService { private final OrderRepository orderRepository; @@ -35,20 +37,6 @@ public class OrderCreationService { private final WithdrawPointService withdrawPointService; private final PriceChecker priceChecker; - public OrderCreationService(OrderRepository orderRepository, CartRepository cartRepository, - StoreRepository storeRepository, CustomerRepository customerRepository, - SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester, - WithdrawPointService withdrawPointService, PriceChecker priceChecker) { - this.orderRepository = orderRepository; - this.cartRepository = cartRepository; - this.storeRepository = storeRepository; - this.customerRepository = customerRepository; - this.singleStoreOrderValidator = singleStoreOrderValidator; - this.stockRequester = stockRequester; - this.withdrawPointService = withdrawPointService; - this.priceChecker = priceChecker; - } - public Long create(OrderCreationCommand cmd) { UUID requesterId = cmd.requesterId(); Customer requester = customerRepository.findByIdOrThrow(requesterId); From 9f87dee8517725653ed5fe053741b222130abd6b Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:24:26 +0900 Subject: [PATCH 27/33] =?UTF-8?q?[fix]=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OrderCreationService에서 Store를 조회하던 로직 제거 SingleStoreOrderValidator에서 검증된 단일 Store를 반환 --- .../java/camp/woowak/lab/order/domain/Order.java | 4 ++-- .../order/domain/SingleStoreOrderValidator.java | 16 +++++++++++++++- .../lab/order/service/OrderCreationService.java | 11 +---------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index f515ec07..77bdadd0 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -35,10 +35,10 @@ public class Order { @ElementCollection(fetch = FetchType.EAGER) private List orderItems = new ArrayList<>(); - public Order(Customer requester, Store store, List cartItems, + public Order(Customer requester, List cartItems, SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester, PriceChecker priceChecker, WithdrawPointService withdrawPointService) { - singleStoreOrderValidator.check(store, cartItems); + Store store = singleStoreOrderValidator.check(cartItems); stockRequester.request(cartItems); List orderItems = priceChecker.check(store, cartItems); withdrawPointService.withdraw(requester, orderItems); diff --git a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java index 16404cb7..5f2948e1 100644 --- a/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java +++ b/src/main/java/camp/woowak/lab/order/domain/SingleStoreOrderValidator.java @@ -8,17 +8,31 @@ import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.exception.MultiStoreOrderException; import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.exception.NotFoundStoreException; +import camp.woowak.lab.store.repository.StoreRepository; @Component public class SingleStoreOrderValidator { - public void check(Store store, List cartItems) { + private final StoreRepository storeRepository; + + public SingleStoreOrderValidator(StoreRepository storeRepository) { + this.storeRepository = storeRepository; + } + + public Store check(List cartItems) { if (cartItems == null || cartItems.isEmpty()) { throw new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다."); } + + Store store = storeRepository.findById(cartItems.get(0).getStoreId()) + .orElseThrow(() -> new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다.")); + for (CartItem cartItem : cartItems) { if (!cartItem.getStoreId().equals(store.getId())) { throw new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다."); } } + + return store; } } diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index f59a4687..beb578a0 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -19,8 +19,6 @@ import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.repository.OrderRepository; import camp.woowak.lab.order.service.command.OrderCreationCommand; -import camp.woowak.lab.store.domain.Store; -import camp.woowak.lab.store.exception.NotFoundStoreException; import camp.woowak.lab.store.repository.StoreRepository; import lombok.RequiredArgsConstructor; @@ -45,15 +43,8 @@ public Long create(OrderCreationCommand cmd) { .orElseThrow(() -> new EmptyCartException("구매자 " + requesterId + "가 비어있는 카트로 주문을 시도했습니다.")); List cartItems = cart.getCartItems(); - if (cartItems == null || cartItems.isEmpty()) { - throw new EmptyCartException("구매자 " + requesterId + "가 비어있는 카트로 주문을 시도했습니다."); - } - - Store store = storeRepository.findById(cartItems.get(0).getStoreId()) - .orElseThrow(() -> new NotFoundStoreException("등록되지 않은 가게의 상품을 주문했습니다.")); - Order savedOrder = orderRepository.save( - new Order(requester, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + new Order(requester, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, withdrawPointService) ); return savedOrder.getId(); From a313fd42cb017768b4b33f4e1ab0251be437b469 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:24:32 +0900 Subject: [PATCH 28/33] =?UTF-8?q?[test]=20=EC=A4=91=EB=B3=B5=EB=90=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OrderCreationService에서 Store를 조회하던 로직 제거 SingleStoreOrderValidator에서 검증된 단일 Store를 반환 --- .../woowak/lab/order/domain/OrderTest.java | 10 +-- .../domain/SingleStoreOrderValidatorTest.java | 64 +++++++++++-------- .../service/OrderCreationServiceTest.java | 6 +- 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/src/test/java/camp/woowak/lab/order/domain/OrderTest.java b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java index 75a75b7a..47aca7f2 100644 --- a/src/test/java/camp/woowak/lab/order/domain/OrderTest.java +++ b/src/test/java/camp/woowak/lab/order/domain/OrderTest.java @@ -41,19 +41,19 @@ void createOrder_ValidInputs_Success() { List orderItems = List.of(orderItem); // Mocking behavior - doNothing().when(singleStoreOrderValidator).check(store, cartItems); + when(singleStoreOrderValidator.check(cartItems)).thenReturn(store); doNothing().when(stockRequester).request(cartItems); when(priceChecker.check(store, cartItems)).thenReturn(orderItems); when(withdrawPointService.withdraw(customer, orderItems)).thenReturn(orderItems); // When - Order order = new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + Order order = new Order(customer, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, withdrawPointService); // Then assertEquals(orderItems, order.getOrderItems()); - verify(singleStoreOrderValidator, times(1)).check(store, cartItems); + verify(singleStoreOrderValidator, times(1)).check(cartItems); verify(stockRequester, times(1)).request(cartItems); verify(priceChecker, times(1)).check(store, cartItems); verify(withdrawPointService, times(1)).withdraw(customer, orderItems); @@ -69,11 +69,11 @@ void createOrder_InvalidStore_ThrowsException() { // Mock behavior to throw exception doThrow(new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다.")) - .when(singleStoreOrderValidator).check(store, cartItems); + .when(singleStoreOrderValidator).check(cartItems); // When & Then MultiStoreOrderException exception = assertThrows(MultiStoreOrderException.class, () -> { - new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + new Order(customer, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, withdrawPointService); }); diff --git a/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java b/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java index 1ddd8c27..97685734 100644 --- a/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java +++ b/src/test/java/camp/woowak/lab/order/domain/SingleStoreOrderValidatorTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.*; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,14 +13,18 @@ import camp.woowak.lab.order.exception.EmptyCartException; import camp.woowak.lab.order.exception.MultiStoreOrderException; import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.exception.NotFoundStoreException; +import camp.woowak.lab.store.repository.StoreRepository; class SingleStoreOrderValidatorTest { private SingleStoreOrderValidator validator; + private StoreRepository storeRepository; @BeforeEach void setUp() { - validator = new SingleStoreOrderValidator(); + storeRepository = mock(StoreRepository.class); + validator = new SingleStoreOrderValidator(storeRepository); } @Test @@ -28,52 +33,59 @@ void check_NullCartItems_ThrowsEmptyCartException() { Store store = mock(Store.class); // When & Then - assertThrows(EmptyCartException.class, () -> validator.check(store, null)); + assertThrows(EmptyCartException.class, () -> validator.check(null)); } @Test - void check_EmptyCartItems_ThrowsEmptyCartException() { + void check_EmptyCart_ThrowsEmptyCartException() { + // When & Then + assertThrows(EmptyCartException.class, () -> validator.check(List.of())); + } + + @Test + void check_StoreNotFound_ThrowsNotFoundStoreException() { // Given - Store store = mock(Store.class); + CartItem cartItem = mock(CartItem.class); + when(cartItem.getStoreId()).thenReturn(1L); + when(storeRepository.findById(1L)).thenReturn(Optional.empty()); // When & Then - assertThrows(EmptyCartException.class, () -> validator.check(store, List.of())); + assertThrows(NotFoundStoreException.class, () -> validator.check(List.of(cartItem))); } @Test - void check_AllItemsBelongToSameStore_NoExceptionThrown() { + void check_ValidCart_ReturnsStore() { // Given Store store = mock(Store.class); - Long storeId = 1L; - when(store.getId()).thenReturn(storeId); + when(store.getId()).thenReturn(1L); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - CartItem item1 = mock(CartItem.class); - CartItem item2 = mock(CartItem.class); - when(item1.getStoreId()).thenReturn(storeId); - when(item2.getStoreId()).thenReturn(storeId); + CartItem cartItem1 = mock(CartItem.class); + CartItem cartItem2 = mock(CartItem.class); + when(cartItem1.getStoreId()).thenReturn(1L); + when(cartItem2.getStoreId()).thenReturn(1L); - List cartItems = List.of(item1, item2); + // When + Store result = validator.check(List.of(cartItem1, cartItem2)); - // When & Then - assertDoesNotThrow(() -> validator.check(store, cartItems)); + // Then + assertEquals(store, result); + verify(storeRepository).findById(1L); } @Test - void check_ItemsFromDifferentStores_ThrowsMultiStoreOrderException() { + void check_MultiStoreOrder_ThrowsMultiStoreOrderException() { // Given Store store = mock(Store.class); - Long storeId1 = 1L; - Long storeId2 = 2L; - when(store.getId()).thenReturn(storeId1); - - CartItem item1 = mock(CartItem.class); - CartItem item2 = mock(CartItem.class); - when(item1.getStoreId()).thenReturn(storeId1); - when(item2.getStoreId()).thenReturn(storeId2); + when(store.getId()).thenReturn(1L); + when(storeRepository.findById(1L)).thenReturn(Optional.of(store)); - List cartItems = List.of(item1, item2); + CartItem cartItem1 = mock(CartItem.class); + CartItem cartItem2 = mock(CartItem.class); + when(cartItem1.getStoreId()).thenReturn(1L); + when(cartItem2.getStoreId()).thenReturn(2L); // When & Then - assertThrows(MultiStoreOrderException.class, () -> validator.check(store, cartItems)); + assertThrows(MultiStoreOrderException.class, () -> validator.check(List.of(cartItem1, cartItem2))); } } diff --git a/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java index 18cb1606..992dce98 100644 --- a/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java +++ b/src/test/java/camp/woowak/lab/order/service/OrderCreationServiceTest.java @@ -74,19 +74,19 @@ void createOrder_Success() { // Mocking behavior when(store.getId()).thenReturn(1L); // Ensure this is mocked when(cartItem.getStoreId()).thenReturn(1L); // Mock the cartItem's storeId - doNothing().when(singleStoreOrderValidator).check(any(Store.class), anyList()); + when(singleStoreOrderValidator.check(anyList())).thenReturn(store); doNothing().when(stockRequester).request(anyList()); when(priceChecker.check(any(), anyList())).thenReturn(orderItems); when(withdrawPointService.withdraw(any(Customer.class), anyList())).thenReturn(orderItems); // When - Order order = new Order(customer, store, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, + Order order = new Order(customer, cartItems, singleStoreOrderValidator, stockRequester, priceChecker, withdrawPointService); // Then assertEquals(orderItems, order.getOrderItems()); - verify(singleStoreOrderValidator, times(1)).check(store, cartItems); + verify(singleStoreOrderValidator, times(1)).check(cartItems); verify(stockRequester, times(1)).request(cartItems); verify(priceChecker, times(1)).check(store, cartItems); verify(withdrawPointService, times(1)).withdraw(customer, orderItems); From 3c5376abc622d81f8c32801b31a6756389bc2ae2 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:40:53 +0900 Subject: [PATCH 29/33] =?UTF-8?q?[feat]=20=EC=9E=AC=EA=B3=A0=20=EB=B6=80?= =?UTF-8?q?=EC=A1=B1=ED=95=9C=20=EA=B2=BD=EC=9A=B0=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 메뉴의 재고가 부족하면 NotEnoughStockExceptioh이 발생합니다. --- .../java/camp/woowak/lab/menu/domain/Menu.java | 4 ++++ .../lab/menu/exception/MenuErrorCode.java | 3 ++- .../exception/NotEnoughStockException.java | 9 +++++++++ .../camp/woowak/lab/order/domain/Order.java | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/main/java/camp/woowak/lab/menu/exception/NotEnoughStockException.java diff --git a/src/main/java/camp/woowak/lab/menu/domain/Menu.java b/src/main/java/camp/woowak/lab/menu/domain/Menu.java index 499a0b04..7604d9f5 100644 --- a/src/main/java/camp/woowak/lab/menu/domain/Menu.java +++ b/src/main/java/camp/woowak/lab/menu/domain/Menu.java @@ -1,5 +1,6 @@ package camp.woowak.lab.menu.domain; +import camp.woowak.lab.menu.exception.NotEnoughStockException; import camp.woowak.lab.store.domain.Store; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -59,6 +60,9 @@ public Long getId() { } public void decrementStockCount(int amount) { + if (stockCount < amount) { + throw new NotEnoughStockException("메뉴(id=" + id + "의 재고가 부족합니다."); + } stockCount -= amount; } } diff --git a/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java b/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java index 8d72c39f..c3b15d1d 100644 --- a/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java +++ b/src/main/java/camp/woowak/lab/menu/exception/MenuErrorCode.java @@ -16,7 +16,8 @@ public enum MenuErrorCode implements ErrorCode { INVALID_PRICE(HttpStatus.BAD_REQUEST, "m_6", "메뉴의 가격 범위를 벗어났습니다."), INVALID_STOCK_COUNT(HttpStatus.BAD_REQUEST, "m_7", "메뉴의 재고 개수는 1개 이상이어야 합니다."), - NOT_FOUND_MENU_CATEGORY(HttpStatus.BAD_REQUEST, "M3", "메뉴 카테고리를 찾을 수 없습니다."); + NOT_FOUND_MENU_CATEGORY(HttpStatus.BAD_REQUEST, "M3", "메뉴 카테고리를 찾을 수 없습니다."), + NOT_ENOUGH_STOCK(HttpStatus.BAD_REQUEST, "M4", "재고가 부족합니다."); private final int status; private final String errorCode; diff --git a/src/main/java/camp/woowak/lab/menu/exception/NotEnoughStockException.java b/src/main/java/camp/woowak/lab/menu/exception/NotEnoughStockException.java new file mode 100644 index 00000000..12811e02 --- /dev/null +++ b/src/main/java/camp/woowak/lab/menu/exception/NotEnoughStockException.java @@ -0,0 +1,9 @@ +package camp.woowak.lab.menu.exception; + +import camp.woowak.lab.common.exception.BadRequestException; + +public class NotEnoughStockException extends BadRequestException { + public NotEnoughStockException(String message) { + super(MenuErrorCode.NOT_ENOUGH_STOCK, message); + } +} diff --git a/src/main/java/camp/woowak/lab/order/domain/Order.java b/src/main/java/camp/woowak/lab/order/domain/Order.java index 77bdadd0..6d6ad1e4 100644 --- a/src/main/java/camp/woowak/lab/order/domain/Order.java +++ b/src/main/java/camp/woowak/lab/order/domain/Order.java @@ -5,8 +5,16 @@ import camp.woowak.lab.cart.domain.vo.CartItem; import camp.woowak.lab.customer.domain.Customer; +import camp.woowak.lab.menu.exception.NotEnoughStockException; import camp.woowak.lab.order.domain.vo.OrderItem; +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.exception.MinimumOrderPriceNotMetException; +import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.order.exception.NotFoundMenuException; +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; import camp.woowak.lab.store.domain.Store; +import camp.woowak.lab.store.exception.NotFoundStoreException; import jakarta.persistence.CollectionTable; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; @@ -35,6 +43,16 @@ public class Order { @ElementCollection(fetch = FetchType.EAGER) private List orderItems = new ArrayList<>(); + /** + * @throws EmptyCartException 카트가 비어 있는 경우 + * @throws NotFoundStoreException 가게가 조회되지 않는 경우 + * @throws MultiStoreOrderException 여러 가게의 메뉴를 주문한 경우 + * @throws NotEnoughStockException 메뉴의 재고가 부족한 경우 + * @throws NotFoundMenuException 주문한 메뉴가 조회되지 않는 경우 + * @throws MinimumOrderPriceNotMetException 가게의 최소 주문금액보다 적은 금액을 주문한 경우 + * @throws NotFoundAccountException 구매자의 계좌가 조회되지 않는 경우 + * @throws InsufficientBalanceException 구매자의 계좌에 잔액이 충분하지 않은 경우 + */ public Order(Customer requester, List cartItems, SingleStoreOrderValidator singleStoreOrderValidator, StockRequester stockRequester, PriceChecker priceChecker, WithdrawPointService withdrawPointService) { From 0209aefa347c13dc51b16b987c5560849daf34af Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:41:44 +0900 Subject: [PATCH 30/33] =?UTF-8?q?[docs]=20=EB=82=B4=EB=B6=80=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 내부적으로 발생하는 RuntimeException에 대한 주석 추가 --- .../order/service/OrderCreationService.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java index beb578a0..53373193 100644 --- a/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java +++ b/src/main/java/camp/woowak/lab/order/service/OrderCreationService.java @@ -11,15 +11,21 @@ import camp.woowak.lab.cart.repository.CartRepository; import camp.woowak.lab.customer.domain.Customer; import camp.woowak.lab.customer.repository.CustomerRepository; +import camp.woowak.lab.menu.exception.NotEnoughStockException; import camp.woowak.lab.order.domain.Order; import camp.woowak.lab.order.domain.PriceChecker; import camp.woowak.lab.order.domain.SingleStoreOrderValidator; import camp.woowak.lab.order.domain.StockRequester; import camp.woowak.lab.order.domain.WithdrawPointService; import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.exception.MinimumOrderPriceNotMetException; +import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.order.exception.NotFoundMenuException; import camp.woowak.lab.order.repository.OrderRepository; import camp.woowak.lab.order.service.command.OrderCreationCommand; -import camp.woowak.lab.store.repository.StoreRepository; +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.payaccount.exception.NotFoundAccountException; +import camp.woowak.lab.store.exception.NotFoundStoreException; import lombok.RequiredArgsConstructor; @Service @@ -28,13 +34,22 @@ public class OrderCreationService { private final OrderRepository orderRepository; private final CartRepository cartRepository; - private final StoreRepository storeRepository; private final CustomerRepository customerRepository; private final SingleStoreOrderValidator singleStoreOrderValidator; private final StockRequester stockRequester; private final WithdrawPointService withdrawPointService; private final PriceChecker priceChecker; + /** + * @throws EmptyCartException 카트가 비어 있는 경우 + * @throws NotFoundStoreException 가게가 조회되지 않는 경우 + * @throws MultiStoreOrderException 여러 가게의 메뉴를 주문한 경우 + * @throws NotEnoughStockException 메뉴의 재고가 부족한 경우 + * @throws NotFoundMenuException 주문한 메뉴가 조회되지 않는 경우 + * @throws MinimumOrderPriceNotMetException 가게의 최소 주문금액보다 적은 금액을 주문한 경우 + * @throws NotFoundAccountException 구매자의 계좌가 조회되지 않는 경우 + * @throws InsufficientBalanceException 구매자의 계좌에 잔액이 충분하지 않은 경우 + */ public Long create(OrderCreationCommand cmd) { UUID requesterId = cmd.requesterId(); Customer requester = customerRepository.findByIdOrThrow(requesterId); From 953049f602f0560e1b54fc80a92ff3dfbb941c07 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:52:45 +0900 Subject: [PATCH 31/33] =?UTF-8?q?[feat]=20=EC=A3=BC=EB=AC=B8=20endpoint=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lab/web/api/order/OrderApiController.java | 32 +++++++++++++++++++ .../response/order/OrderCreationResponse.java | 6 ++++ 2 files changed, 38 insertions(+) create mode 100644 src/main/java/camp/woowak/lab/web/api/order/OrderApiController.java create mode 100644 src/main/java/camp/woowak/lab/web/dto/response/order/OrderCreationResponse.java diff --git a/src/main/java/camp/woowak/lab/web/api/order/OrderApiController.java b/src/main/java/camp/woowak/lab/web/api/order/OrderApiController.java new file mode 100644 index 00000000..270d77ad --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/api/order/OrderApiController.java @@ -0,0 +1,32 @@ +package camp.woowak.lab.web.api.order; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import camp.woowak.lab.order.service.OrderCreationService; +import camp.woowak.lab.order.service.command.OrderCreationCommand; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.authentication.annotation.AuthenticationPrincipal; +import camp.woowak.lab.web.dto.response.order.OrderCreationResponse; +import lombok.extern.slf4j.Slf4j; + +@RestController +@Slf4j +public class OrderApiController { + private final OrderCreationService orderCreationService; + + public OrderApiController(OrderCreationService orderCreationService) { + this.orderCreationService = orderCreationService; + } + + @PostMapping("/orders") + @ResponseStatus(HttpStatus.CREATED) + public OrderCreationResponse order(@AuthenticationPrincipal LoginCustomer loginCustomer) { + OrderCreationCommand command = new OrderCreationCommand(loginCustomer.getId()); + Long createdId = orderCreationService.create(command); + log.info("Created order for customer {} with id {}", loginCustomer.getId(), createdId); + return new OrderCreationResponse(createdId); + } +} diff --git a/src/main/java/camp/woowak/lab/web/dto/response/order/OrderCreationResponse.java b/src/main/java/camp/woowak/lab/web/dto/response/order/OrderCreationResponse.java new file mode 100644 index 00000000..120875ef --- /dev/null +++ b/src/main/java/camp/woowak/lab/web/dto/response/order/OrderCreationResponse.java @@ -0,0 +1,6 @@ +package camp.woowak.lab.web.dto.response.order; + +public record OrderCreationResponse( + Long orderId +) { +} From 60f863a83dcf8f658bcc261a9965c8f9a544d8e7 Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:52:52 +0900 Subject: [PATCH 32/33] =?UTF-8?q?[test]=20=EC=A3=BC=EB=AC=B8=20endpoint=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/api/order/OrderApiControllerTest.java | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/test/java/camp/woowak/lab/web/api/order/OrderApiControllerTest.java diff --git a/src/test/java/camp/woowak/lab/web/api/order/OrderApiControllerTest.java b/src/test/java/camp/woowak/lab/web/api/order/OrderApiControllerTest.java new file mode 100644 index 00000000..b946a2b3 --- /dev/null +++ b/src/test/java/camp/woowak/lab/web/api/order/OrderApiControllerTest.java @@ -0,0 +1,181 @@ +package camp.woowak.lab.web.api.order; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import camp.woowak.lab.order.exception.EmptyCartException; +import camp.woowak.lab.order.exception.MinimumOrderPriceNotMetException; +import camp.woowak.lab.order.exception.MultiStoreOrderException; +import camp.woowak.lab.order.service.OrderCreationService; +import camp.woowak.lab.order.service.command.OrderCreationCommand; +import camp.woowak.lab.payaccount.exception.InsufficientBalanceException; +import camp.woowak.lab.web.authentication.LoginCustomer; +import camp.woowak.lab.web.resolver.session.SessionConst; + +@WebMvcTest(controllers = OrderApiController.class) +@MockBean(JpaMetamodelMappingContext.class) +class OrderApiControllerTest { + @Autowired + private MockMvc mockMvc; + @MockBean + private OrderCreationService orderCreationService; + + @Nested + @DisplayName("판매자 회원가입: POST /vendors") + class SignUpVendor { + @Test + @DisplayName("[성공] 201") + void success() throws Exception { + LoginCustomer loginCustomer = new LoginCustomer(UUID.randomUUID()); + Long fakeOrderId = 1L; + BDDMockito.given(orderCreationService.create(BDDMockito.any(OrderCreationCommand.class))) + .willReturn(fakeOrderId); + + // when + ResultActions actions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .sessionAttr(SessionConst.SESSION_CUSTOMER_KEY, loginCustomer) + ); + + // then + actions.andExpect(status().isCreated()) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) + .andExpect(jsonPath("$.data.orderId").value(fakeOrderId)) + .andDo(print()); + } + + @Nested + @DisplayName("[실패] 400") + class FailWith400 { + @Nested + @DisplayName("카트가") + class CartMust { + @Test + @DisplayName("비어있는 경우") + void failWithEmptyCart() throws Exception { + LoginCustomer loginCustomer = new LoginCustomer(UUID.randomUUID()); + BDDMockito.given(orderCreationService.create(BDDMockito.any(OrderCreationCommand.class))) + .willThrow(new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다.")); + + // when + ResultActions actions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .sessionAttr(SessionConst.SESSION_CUSTOMER_KEY, loginCustomer) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/orders")) + .andDo(print()); + } + } + + @Nested + @DisplayName("가게는") + class StoreMust { + @Test + @DisplayName("단일 가게에 대한 주문만 가능하다.") + void failWithEmptyCart() throws Exception { + LoginCustomer loginCustomer = new LoginCustomer(UUID.randomUUID()); + BDDMockito.given(orderCreationService.create(BDDMockito.any(OrderCreationCommand.class))) + .willThrow(new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다.")); + + // when + ResultActions actions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .sessionAttr(SessionConst.SESSION_CUSTOMER_KEY, loginCustomer) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/orders")) + .andDo(print()); + } + } + + @Nested + @DisplayName("주문 금액은") + class OrderPriceMust { + @Test + @DisplayName("가게의 최소 주문금액보다 커야한다.") + void failWithEmptyCart() throws Exception { + LoginCustomer loginCustomer = new LoginCustomer(UUID.randomUUID()); + BDDMockito.given(orderCreationService.create(BDDMockito.any(OrderCreationCommand.class))) + .willThrow(new MinimumOrderPriceNotMetException("주문 금액이 최소 주문금액보다 적습니다.")); + + // when + ResultActions actions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .sessionAttr(SessionConst.SESSION_CUSTOMER_KEY, loginCustomer) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/orders")) + .andDo(print()); + } + } + + @Nested + @DisplayName("계좌의 잔액은") + class PayAccountMust { + @Test + @DisplayName("주문 금액보다 커야한다.") + void failWithEmptyCart() throws Exception { + LoginCustomer loginCustomer = new LoginCustomer(UUID.randomUUID()); + BDDMockito.given(orderCreationService.create(BDDMockito.any(OrderCreationCommand.class))) + .willThrow(new InsufficientBalanceException("잔액이 부족합니다.")); + + // when + ResultActions actions = mockMvc.perform( + post("/orders") + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .sessionAttr(SessionConst.SESSION_CUSTOMER_KEY, loginCustomer) + ); + + // then + actions.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.type").value("about:blank")) + .andExpect(jsonPath("$.title").value("Bad Request")) + .andExpect(jsonPath("$.status").value(400)) + .andExpect(jsonPath("$.instance").value("/orders")) + .andDo(print()); + } + } + } + } +} From 5da3da638fae878d538b809e619eb09d1787e97b Mon Sep 17 00:00:00 2001 From: donghar Date: Sat, 17 Aug 2024 17:55:06 +0900 Subject: [PATCH 33/33] =?UTF-8?q?[fix]=20static=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/camp/woowak/lab/order/domain/StockRequester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/camp/woowak/lab/order/domain/StockRequester.java b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java index 2ad635d4..f51abf92 100644 --- a/src/main/java/camp/woowak/lab/order/domain/StockRequester.java +++ b/src/main/java/camp/woowak/lab/order/domain/StockRequester.java @@ -28,7 +28,7 @@ public void request(List cartItems) { } } - private static List extractMenuIds(List cartItems) { + private List extractMenuIds(List cartItems) { return cartItems.stream().map(CartItem::getMenuId).toList(); } }