Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 구매자는 장바구니에 담긴 상품을 주문할 수 있다 #80

Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
49af9bd
[feat] Order 생성자 수정
Dr-KoKo Aug 15, 2024
a590a9f
[feat] SingleStoreOrderValidator
Dr-KoKo Aug 15, 2024
edb05f3
[feat] PayAmountValidator
Dr-KoKo Aug 15, 2024
7a7a8f6
[fix] CustomerRepository의 Id 변경
Dr-KoKo Aug 15, 2024
5a022e6
[chore] .gitkeep 삭제
Dr-KoKo Aug 15, 2024
04fa033
[feat] OrderCreationService 구현
Dr-KoKo Aug 15, 2024
517be58
[feat] OrderRepository
Dr-KoKo Aug 15, 2024
cb53e35
[feat] OrderItem
Dr-KoKo Aug 15, 2024
046e02b
[fix] Cart는 Menu가 아니라 CartItem 리스트를 가진다.
Dr-KoKo Aug 16, 2024
9f8996e
[fix] 카트에 담긴 상품의 총합을 조회하기 위해서는 Repository를 조회한다
Dr-KoKo Aug 16, 2024
61a98e1
[test] Cart 도메인 변경으로 인한 테스트 코드 수정
Dr-KoKo Aug 16, 2024
20bfb3b
[feat] Order 도메인 설계
Dr-KoKo Aug 16, 2024
681e0f0
[feat] Menu 재고 삭감 서비스
Dr-KoKo Aug 16, 2024
2b99e66
[feat] Menu 검증 서비스
Dr-KoKo Aug 16, 2024
cb113bd
[feat] 주문 생성 서비스
Dr-KoKo Aug 16, 2024
001c0b3
[feat] PriceChecker 구현
Dr-KoKo Aug 16, 2024
dba3013
[feat] 주문 결제 서비스
Dr-KoKo Aug 16, 2024
141ff38
[refactor] 자바 컨벤션에 맞게 수정
Dr-KoKo Aug 16, 2024
82b8c3d
[refactor] OrderCreationService 내부 코드 정리
Dr-KoKo Aug 16, 2024
6774540
[test] OrderCreationservice 테스트 코드 작성
Dr-KoKo Aug 16, 2024
f7662b0
[fix] 에러 메시지 구체화
Dr-KoKo Aug 17, 2024
0b8c503
[refactor] 가독성 향상
Dr-KoKo Aug 17, 2024
1f9edea
[feat] 최소 주문금액 확인 로직 추가
Dr-KoKo Aug 17, 2024
db9732f
[test] 최소 주문금액 확인 로직 추가
Dr-KoKo Aug 17, 2024
f4132cc
[refactor] 메서드로 추출
Dr-KoKo Aug 17, 2024
3fdca84
[refactor] RequiredArgsConstructor 적용
Dr-KoKo Aug 17, 2024
9f87dee
[fix] 중복된 검증 로직 제거
Dr-KoKo Aug 17, 2024
a313fd4
[test] 중복된 검증 로직 제거
Dr-KoKo Aug 17, 2024
3c5376a
[feat] 재고 부족한 경우 예외 처리
Dr-KoKo Aug 17, 2024
0209aef
[docs] 내부 예외 주석
Dr-KoKo Aug 17, 2024
953049f
[feat] 주문 endpoint 생성
Dr-KoKo Aug 17, 2024
60f863a
[test] 주문 endpoint 테스트
Dr-KoKo Aug 17, 2024
5da3da6
[fix] static 삭제
Dr-KoKo Aug 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 23 additions & 15 deletions src/main/java/camp/woowak/lab/cart/domain/Cart.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,7 +17,7 @@
@Getter
public class Cart {
private final String customerId;
private final List<Menu> menuList;
private final List<CartItem> cartItems;

/**
* 생성될 때 무조건 cart가 비어있도록 구현
Expand All @@ -31,27 +32,36 @@ public Cart(String customerId) {
* 해당 Domain을 사용하는 같은 패키지내의 클래스, 혹은 자식 클래스는 List를 커스텀할 수 있습니다.
*
* @param customerId 장바구니 소유주의 ID값입니다.
* @param menuList 장바구니에 사용될 List입니다.
* @param cartItems 장바구니에 사용될 List입니다.
*/
protected Cart(String customerId, List<Menu> menuList) {
protected Cart(String customerId, List<CartItem> 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) {
Expand All @@ -72,10 +82,8 @@ private void validateOtherStore(Long menuStoreId) {
}

private Set<Long> getStoreIds() {
return this.menuList.stream()
.map(Menu::getStore)
.mapToLong(Store::getId)
.boxed()
return this.cartItems.stream()
.map(CartItem::getStoreId)
.collect(Collectors.toSet());
}
}
46 changes: 46 additions & 0 deletions src/main/java/camp/woowak/lab/cart/domain/vo/CartItem.java
Original file line number Diff line number Diff line change
@@ -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 int 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 int 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/camp/woowak/lab/cart/service/CartService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Menu> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Customer, UUID> {
Optional<Customer> findByEmail(String email);

default Customer findByIdOrThrow(UUID id) {
return findById(id).orElseThrow(() -> new NotFoundCustomerException("존재하지 않는 사용자(id=" + id + ")를 조회했습니다."));
}
}
3 changes: 3 additions & 0 deletions src/main/java/camp/woowak/lab/menu/domain/Menu.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,7 @@ public Long getId() {
return id;
}

public void decrementStockCount(int amount) {
stockCount -= amount;
}
}
Original file line number Diff line number Diff line change
@@ -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<Menu, Long> {
@Query("SELECT m FROM Menu m JOIN FETCH m.store WHERE m.id = :id")
Optional<Menu> findByIdWithStore(@Param("id") Long id);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT m FROM Menu m where m.id in :ids")
List<Menu> findAllByIdForUpdate(List<Long> ids);
}
34 changes: 34 additions & 0 deletions src/main/java/camp/woowak/lab/order/domain/Order.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package camp.woowak.lab.order.domain;

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.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)
Expand All @@ -20,4 +31,27 @@ 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<OrderItem> orderItems = new ArrayList<>();

Dr-KoKo marked this conversation as resolved.
Show resolved Hide resolved
public Order(Customer requester, Store store, List<CartItem> cartItems,
SingleStoreOrderValidator singleStoreOrderValidator,
StockRequester stockRequester, PriceChecker priceChecker, WithdrawPointService withdrawPointService) {
singleStoreOrderValidator.check(store, cartItems);
stockRequester.request(cartItems);
List<OrderItem> orderItems = priceChecker.check(cartItems);
withdrawPointService.withdraw(requester, orderItems);
this.requester = requester;
this.store = store;
this.orderItems = orderItems;
}

public Long getId() {
return id;
}

public List<OrderItem> getOrderItems() {
return orderItems;
}
}
65 changes: 65 additions & 0 deletions src/main/java/camp/woowak/lab/order/domain/PriceChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;

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<OrderItem> check(List<CartItem> cartItems) {
Set<Long> cartItemMenuIds = extractMenuIds(cartItems);
List<Menu> menus = menuRepository.findAllById(cartItemMenuIds);
Map<Long, Menu> menuMap = listToMap(menus);
Set<Long> 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<OrderItem> orderItems = new ArrayList<>();
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<Long> missingMenuIds) {
return missingMenuIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
}

private Map<Long, Menu> listToMap(List<Menu> menus) {
return menus.stream()
.collect(Collectors.toMap(Menu::getId, Function.identity()));
}

private Set<Long> extractMenuIds(List<CartItem> cartItems) {
return cartItems.stream()
.map(CartItem::getMenuId)
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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.order.exception.EmptyCartException;
import camp.woowak.lab.order.exception.MultiStoreOrderException;
import camp.woowak.lab.store.domain.Store;

@Component
public class SingleStoreOrderValidator {
public void check(Store store, List<CartItem> cartItems) {
if (cartItems == null || cartItems.isEmpty()) {
throw new EmptyCartException("최소 하나 이상의 메뉴를 주문해야 합니다.");
}
for (CartItem cartItem : cartItems) {
if (!cartItem.getStoreId().equals(store.getId())) {
throw new MultiStoreOrderException("다른 가게의 메뉴를 같이 주문할 수 없습니다.");
}
}
}
}
Loading