From 06d6bd1692357e29caf28d2d5ebe37b8989b7867 Mon Sep 17 00:00:00 2001 From: sieun0322 Date: Thu, 13 Nov 2025 20:17:45 +0900 Subject: [PATCH 1/3] feat: domain modeling --- .../application/brand/BrandFacade.java | 18 +++ .../loopers/application/brand/BrandInfo.java | 16 ++ .../loopers/application/like/LikeInfo.java | 10 ++ .../application/order/CreateOrderCommand.java | 21 +++ .../application/order/OrderFacade.java | 70 ++++++++ .../loopers/application/order/OrderInfo.java | 28 ++++ .../application/order/OrderItemInfo.java | 24 +++ .../product/ProductDetailInfo.java | 24 +++ .../application/product/ProductFacade.java | 29 ++++ .../java/com/loopers/domain/brand/Brand.java | 32 ++++ .../loopers/domain/brand/BrandRepository.java | 10 ++ .../loopers/domain/brand/BrandService.java | 27 ++++ .../java/com/loopers/domain/like/Like.java | 43 +++++ .../loopers/domain/like/LikeRepository.java | 21 +++ .../com/loopers/domain/like/LikeService.java | 74 +++++++++ .../java/com/loopers/domain/order/Order.java | 83 ++++++++++ .../com/loopers/domain/order/OrderItem.java | 42 +++++ .../loopers/domain/order/OrderRepository.java | 15 ++ .../loopers/domain/order/OrderService.java | 66 ++++++++ .../com/loopers/domain/order/OrderStatus.java | 19 +++ .../loopers/domain/point/PointService.java | 11 ++ .../com/loopers/domain/product/Product.java | 60 +++++++ .../domain/product/ProductRepository.java | 23 +++ .../domain/product/ProductService.java | 92 +++++++++++ .../domain/product/ProductStockService.java | 25 +++ .../loopers/domain/user/UserRepository.java | 2 +- .../com/loopers/domain/user/UserService.java | 6 +- .../brand/BrandJpaRepository.java | 12 ++ .../brand/BrandRepositoryImpl.java | 24 +++ .../like/LikeJpaRepository.java | 29 ++++ .../like/LikeRepositoryImpl.java | 47 ++++++ .../order/OrderJpaRepository.java | 15 ++ .../order/OrderRepositoryImpl.java | 32 ++++ .../product/ProductJpaRepository.java | 33 ++++ .../product/ProductRepositoryImpl.java | 50 ++++++ .../user/UserJpaRepository.java | 3 +- .../user/UserRepositoryImpl.java | 4 +- .../api/order/OrderCreateV1Dto.java | 37 +++++ .../interfaces/api/order/OrderV1ApiSpec.java | 55 +++++++ .../api/order/OrderV1Controller.java | 63 ++++++++ .../product/ProductFacadeIntegrationTest.java | 122 ++++++++++++++ .../loopers/domain/brand/BrandAssertions.java | 15 ++ .../loopers/domain/brand/BrandModelTest.java | 49 ++++++ .../brand/BrandServiceIntegrationTest.java | 76 +++++++++ .../domain/like/FakeLikeRepository.java | 93 +++++++++++ .../loopers/domain/like/LikeAssertions.java | 16 ++ .../loopers/domain/like/LikeModelTest.java | 69 ++++++++ .../like/LikeServiceIntegrationTest.java | 153 ++++++++++++++++++ .../loopers/domain/like/LikeServiceTest.java | 150 +++++++++++++++++ .../loopers/domain/order/OrderAssertions.java | 16 ++ .../loopers/domain/order/OrderModelTest.java | 103 ++++++++++++ .../order/OrderServiceIntegrationTest.java | 120 ++++++++++++++ .../domain/product/ProductAssertions.java | 18 +++ .../domain/product/ProductModelTest.java | 93 +++++++++++ .../ProductServiceIntegrationTest.java | 118 ++++++++++++++ 55 files changed, 2499 insertions(+), 7 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java new file mode 100644 index 000000000..c0e4d5da9 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandFacade.java @@ -0,0 +1,18 @@ +package com.loopers.application.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class BrandFacade { + private final BrandService brandService; + + public BrandInfo getBrandDetail(long id) { + Brand brand = brandService.getBrand(id); + return BrandInfo.from(brand); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java new file mode 100644 index 000000000..5a2ae251c --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/brand/BrandInfo.java @@ -0,0 +1,16 @@ +package com.loopers.application.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +public record BrandInfo(Long id, String name, String story) { + public static BrandInfo from(Brand model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); + return new BrandInfo( + model.getId(), + model.getName(), + model.getStory() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java new file mode 100644 index 000000000..1685623a2 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/like/LikeInfo.java @@ -0,0 +1,10 @@ +package com.loopers.application.like; + +public record LikeInfo(long likeCount, boolean isLiked) { + public static LikeInfo from(long likeCount, boolean isLiked) { + return new LikeInfo( + likeCount, + isLiked + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java new file mode 100644 index 000000000..76f9c2c74 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java @@ -0,0 +1,21 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.interfaces.api.order.OrderCreateV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +public record CreateOrderCommand(Long userId, List orderItemInfo) { + public record ItemCommand(Long productId, Long quantity) { + } + + public static CreateOrderCommand from(Long userId, OrderCreateV1Dto.OrderRequest request) { + return new CreateOrderCommand( + userId, + request.items().stream().map(i -> new ItemCommand(i.productId(), i.quantity())).toList()); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java new file mode 100644 index 000000000..9aa8f0570 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -0,0 +1,70 @@ +package com.loopers.application.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderService; +import com.loopers.domain.order.OrderStatus; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.product.ProductStockService; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class OrderFacade { + private final UserService userService; + private final PointService pointService; + private final ProductService productService; + private final OrderService orderService; + + private final ProductStockService productStockService; + + public Page getOrderList(Long userId, + String sortType, + int page, + int size) { + return orderService.getOrders(userId, sortType, page, size); + } + + public OrderInfo getOrderDetail(Long orderId) { + Order order = orderService.getOrder(orderId); + return OrderInfo.from(order); + } + + @Transactional + public OrderInfo createOrder(Long userId, Map productQuantityMap) { + //사용자 존재 확인 + UserModel user = userService.getUser(userId); + + //상품 존재 확인 + List productList = productService.getExistingProducts(productQuantityMap.keySet()); + + //재고확인및 차감 + List deductedProducts = productStockService.deductStock(productList, productQuantityMap); + productService.save(deductedProducts); + + //포인트 확인 및 차감 + BigDecimal totalPrice = deductedProducts.stream() + .map(Product::getPrice) + .reduce(BigDecimal.ZERO, BigDecimal::add); + pointService.use(user, totalPrice); + + //주문 생성 + Order order = Order.create(userId, OrderStatus.PAID, totalPrice, totalPrice, ZonedDateTime.now()); + order.setOrderItems(deductedProducts.stream().map(i -> OrderItem.create(i.getId(), productQuantityMap.get(i.getId()), i.getPrice())).toList()); + Order savedOrder = orderService.save(order); + + return OrderInfo.from(savedOrder); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java new file mode 100644 index 000000000..d83bb2494 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -0,0 +1,28 @@ +package com.loopers.application.order; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.loopers.application.brand.BrandInfo; +import com.loopers.domain.order.Order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +public record OrderInfo(long id, String status, BigDecimal paymentPrice, BigDecimal totalPrice + , ZonedDateTime orderAt, List orderItemInfo) { + public static OrderInfo from(Order model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "주문정보를 찾을수 없습니다."); + return new OrderInfo( + model.getId(), + model.getStatus().name(), + model.getPaymentPrice(), + model.getTotalPrice(), + model.getOrderAt(), + OrderItemInfo.from(model.getOrderItems()) + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java new file mode 100644 index 000000000..92a66aa2f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderItemInfo.java @@ -0,0 +1,24 @@ +package com.loopers.application.order; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.loopers.domain.order.OrderItem; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; +import java.util.List; + +public record OrderItemInfo(Long id, long quantity + , @JsonSerialize(using = ToStringSerializer.class) BigDecimal unitPrice, + @JsonSerialize(using = ToStringSerializer.class) BigDecimal totalPrice) { + public static List from(List model) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "주문상세정보를 찾을수 없습니다."); + return model.stream().map(item -> new OrderItemInfo( + item.getId(), + item.getQuantity(), + item.getUnitPrice(), + item.getTotalPrice() + )).toList(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java new file mode 100644 index 000000000..fadaa5cc8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductDetailInfo.java @@ -0,0 +1,24 @@ +package com.loopers.application.product; + +import com.loopers.application.brand.BrandInfo; +import com.loopers.application.like.LikeInfo; +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; + +import java.math.BigDecimal; + +public record ProductDetailInfo(Long id, String name, BigDecimal price, long stock + , BrandInfo brandInfo, LikeInfo likeInfo) { + public static ProductDetailInfo from(Product model, boolean isLiked) { + if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "상품정보를 찾을수 없습니다."); + return new ProductDetailInfo( + model.getId(), + model.getName(), + model.getPrice(), + model.getStock(), + BrandInfo.from(model.getBrand()), + LikeInfo.from(model.getLikeCount(), isLiked) + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java new file mode 100644 index 000000000..b19585419 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -0,0 +1,29 @@ +package com.loopers.application.product; + +import com.loopers.domain.like.LikeService; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class ProductFacade { + private final ProductService productService; + private final LikeService likeService; + + public Page getProductList(Long brandId, + String sortType, + int page, + int size) { + return productService.getProducts(brandId, sortType, page, size); + } + + public ProductDetailInfo getProductDetail(long userId, long productId) { + Product product = productService.getProduct(productId); + boolean isLiked = likeService.isLiked(userId, productId); + return ProductDetailInfo.from(product, isLiked); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java new file mode 100644 index 000000000..39067ec5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java @@ -0,0 +1,32 @@ +package com.loopers.domain.brand; + +import com.loopers.domain.BaseEntity; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +@Entity +@Table(name = "brand") +@Getter +public class Brand extends BaseEntity { + private String name; + private String story; + + protected Brand() { + this.name = ""; + this.story = ""; + } + + private Brand(String name, String story) { + this.name = name; + this.story = story; + } + + public static Brand create(String name, String story) { + if (name.isBlank()) { + throw new IllegalArgumentException("브랜드명은 비어있을 수 없습니다."); + } + return new Brand(name, story); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java new file mode 100644 index 000000000..ceb5791f4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandRepository.java @@ -0,0 +1,10 @@ +package com.loopers.domain.brand; + +import java.util.Optional; + +public interface BrandRepository { + Optional findById(Long id); + + Brand save(Brand brand); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java new file mode 100644 index 000000000..0e05dead6 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/brand/BrandService.java @@ -0,0 +1,27 @@ +package com.loopers.domain.brand; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Component +public class BrandService { + + private final BrandRepository brandRepository; + + @Transactional(readOnly = true) + public Brand getBrand(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return brandRepository.findById(id).orElse(null); + } + + public Brand save(Brand brand) { + return brandRepository.save(brand); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java new file mode 100644 index 000000000..425316747 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java @@ -0,0 +1,43 @@ +package com.loopers.domain.like; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.UserModel; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +@Entity +@Table(name = "likes", uniqueConstraints = { + @UniqueConstraint(columnNames = {"ref_user_id", "ref_product_id"}) +}) +@Getter +public class Like extends BaseEntity { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ref_user_Id", nullable = false) + private UserModel user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ref_product_Id", nullable = false) + private Product product; + + protected Like() { + } + + private Like(UserModel user, Product product) { + this.user = user; + this.product = product; + } + + public static Like create(UserModel user, Product product) { + if (user == null || user.getId() == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "사용자ID는 비어있을 수 없습니다."); + } + if (product == null || product.getId() == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품ID는 비어있을 수 없습니다."); + } + return new Like(user, product); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java new file mode 100644 index 000000000..b0a87fb3a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeRepository.java @@ -0,0 +1,21 @@ +package com.loopers.domain.like; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface LikeRepository { + + Optional findById(Long userId, Long productid); + + Like save(Like like); + + long remove(Long userId, Long productId); + + boolean isLiked(Long userId, Long productId); + + long getLikeCount(Long productId); + + Page getLikedProducts(Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java new file mode 100644 index 000000000..01610a251 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/LikeService.java @@ -0,0 +1,74 @@ + +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.*; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class LikeService { + + private final LikeRepository likeRepository; + + @Transactional + public Like save(Like like) { + Long userId = like.getUser().getId(); + Long productId = like.getProduct().getId(); + Optional existingLike = likeRepository.findById(userId, productId); + + if (existingLike.isPresent()) { + return existingLike.get(); + } + return likeRepository.save(like); + } + + + @Transactional + public long remove(Long userId, Long productId) { + return likeRepository.remove(userId, productId); + } + + @Transactional + public boolean isLiked(Long userId, Long productId) { + return likeRepository.isLiked(userId, productId); + } + + public Page getLikedProducts( + Long userId, + String sortType, + int page, + int size + ) { + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page likePage = likeRepository.getLikedProducts(userId, pageable); + List likedProducts = likePage.getContent().stream().map(Like::getProduct).toList(); + return new PageImpl<>(likedProducts, pageable, likePage.getTotalElements()); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price_asc": + return Sort.by("price").ascending().and(latestSort); + case "likes_desc": + return Sort.by("likesCount").descending().and(latestSort); + default: + return latestSort; + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java new file mode 100644 index 000000000..3b285f582 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -0,0 +1,83 @@ +package com.loopers.domain.order; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "order") +@Getter +public class Order extends BaseEntity { + + private Long refUserId; + + private OrderStatus status; + + BigDecimal paymentPrice; + + BigDecimal totalPrice; + + ZonedDateTime orderAt; + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "ref_order_id") + private List orderItems = new ArrayList<>(); + + public void setOrderItems(List orderItems) { + if (orderItems == null || orderItems.isEmpty()) { + throw new CoreException(ErrorType.BAD_REQUEST, "주문 상세내역이 없습니다."); + } + this.orderItems = orderItems; + } + + protected Order() { + } + + private Order(long refUserId, OrderStatus status, BigDecimal paymentPrice, BigDecimal totalPrice, ZonedDateTime orderAt) { + this.refUserId = refUserId; + this.status = status; + this.paymentPrice = paymentPrice; + this.totalPrice = totalPrice; + this.orderAt = orderAt; + } + + public static Order create(long refUserId, OrderStatus status, BigDecimal paymentPrice, BigDecimal totalPrice, ZonedDateTime orderAt) { + if (status == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "상태 정보는 비어있을 수 없습니다."); + } + if (paymentPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "지불금액은 음수일수 없습니다."); + } + if (totalPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "총가격은 음수일수 없습니다."); + } + return new Order(refUserId, status, paymentPrice, totalPrice, orderAt); + } + + public void cancel() { + if (status == OrderStatus.CANCELLED) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 취소된 주문입니다."); + } + this.status = OrderStatus.CANCELLED; + } + + public void preparing() { + if (status == OrderStatus.PENDING) { + throw new CoreException(ErrorType.BAD_REQUEST, "결재대기중 주문입니다."); + } + if (status == OrderStatus.PREPARING) { + throw new CoreException(ErrorType.BAD_REQUEST, "이미 준비중인 주문입니다."); + } + if (OrderStatus.PAID.getSequence() < status.getSequence()) { + throw new CoreException(ErrorType.BAD_REQUEST, "상품준비 완료된 주문입니다."); + } + this.status = OrderStatus.PREPARING; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java new file mode 100644 index 000000000..07e51557e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -0,0 +1,42 @@ +package com.loopers.domain.order; + +import com.loopers.domain.BaseEntity; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "orderItem") +@Getter +public class OrderItem extends BaseEntity { + + private long refProductId; + + private long quantity; + + private BigDecimal unitPrice; + + private BigDecimal totalPrice; + + protected OrderItem() { + } + + private OrderItem(long refProductId, long quantity, BigDecimal unitPrice, BigDecimal totalPrice) { + this.refProductId = refProductId; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.totalPrice = totalPrice; + } + + public static OrderItem create(long refProductId, long quantity, BigDecimal unitPrice) { + if (unitPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "가격은 음수일수 없습니다."); + } + BigDecimal totalPrice = unitPrice.multiply(new BigDecimal(quantity)); + return new OrderItem(refProductId, quantity, unitPrice, totalPrice); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java new file mode 100644 index 000000000..c03bd66a8 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -0,0 +1,15 @@ +package com.loopers.domain.order; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.Optional; + +public interface OrderRepository { + Optional findById(Long id); + + Page findByUserId(Long userId, Pageable pageable); + + Order save(Order product); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java new file mode 100644 index 000000000..e02487b7b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java @@ -0,0 +1,66 @@ + +package com.loopers.domain.order; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.domain.product.ProductRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class OrderService { + private final ProductRepository productRepository; + private final OrderRepository orderRepository; + + public Page getOrders( + Long userId, + String sortType, + int page, + int size + ) { + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page orders = null; + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "유저 정보가 없습니다."); + } + orders = orderRepository.findByUserId(userId, pageable); + return orders; + } + + @Transactional(readOnly = true) + public Order getOrder(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return orderRepository.findById(id).orElse(null); + } + + public Order save(Order order) { + return orderRepository.save(order); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price": + return Sort.by("status").descending(); + default: + return latestSort; + } + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java new file mode 100644 index 000000000..b93673616 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java @@ -0,0 +1,19 @@ +package com.loopers.domain.order; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum OrderStatus { + PENDING("결제 대기", 10), + PAID("결제 완료", 20), + PREPARING("상품 준비 중", 30), + SHIPPED("배송 중", 40), + DELIVERED("배송 완료", 50), + CANCELLED("주문 취소", 90), + REFUNDED("환불 완료", 100); + + private final String description; + private final int sequence; +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 9554dcac0..9b953581d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -34,4 +34,15 @@ public BigDecimal charge(UserModel user, BigDecimal chargeAmt) { pointRepository.save(pointOpt.get()); return getAmount(user.getUserId()); } + + @Transactional + public BigDecimal use(UserModel user, BigDecimal useAmt) { + Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getUserId()); + if (pointOpt.isEmpty()) { + throw new CoreException(ErrorType.NOT_FOUND, "현재 포인트 정보를 찾을수 없습니다."); + } + pointOpt.get().use(useAmt); + pointRepository.save(pointOpt.get()); + return getAmount(user.getUserId()); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java new file mode 100644 index 000000000..f5df0758f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -0,0 +1,60 @@ +package com.loopers.domain.product; + +import com.loopers.domain.BaseEntity; +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import jakarta.persistence.*; +import lombok.Getter; + +import java.math.BigDecimal; + +@Entity +@Table(name = "product") +@Getter +public class Product extends BaseEntity { + private String name; + + @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 설정 (성능상 권장) + @JoinColumn(name = "ref_brand_Id") + private Brand brand; + + private BigDecimal price; + private long stock; + + @Transient + private long likeCount; + + public void setBrand(Brand brand) { + this.brand = brand; + } + + public void setLikeCount(long likeCount) { + this.likeCount = likeCount; + } + + protected Product() { + this.stock = 0; + } + + private Product(Brand brand, String name, BigDecimal price, long stock) { + this.setBrand(brand); + this.name = name; + this.price = price; + this.stock = stock; + } + + public static Product create(Brand brand, String name, BigDecimal price, long stock) { + return new Product(brand, name, price, stock); + } + + public void deductStock(long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 0보다 커야 합니다."); + } + if (this.stock < quantity) { + throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); + } + this.stock -= quantity; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java new file mode 100644 index 000000000..ae1031751 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductRepository.java @@ -0,0 +1,23 @@ +package com.loopers.domain.product; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface ProductRepository { + Optional findById(Long id); + + List findAllById(Set id); + + Page findByBrandId(Long brandId, Pageable pageable); + + Page findAll(Pageable pageable); + + Product save(Product product); + + List save(List products); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java new file mode 100644 index 000000000..9819ad90b --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -0,0 +1,92 @@ + +package com.loopers.domain.product; + +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.domain.like.LikeRepository; +import com.loopers.domain.order.Order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@RequiredArgsConstructor +@Component +public class ProductService { + + private final ProductRepository productRepository; + private final LikeRepository likeRepository; + + public Page getProducts( + Long brandId, + String sortType, + int page, + int size + ) { + Sort sort = this.getSortBySortType(sortType); + Pageable pageable = PageRequest.of(page, size, sort); + Page products = null; + if (brandId != null) { + products = productRepository.findByBrandId(brandId, pageable); + } else { + products = productRepository.findAll(pageable); + } + return products; + } + + @Transactional(readOnly = true) + public Product getProduct(Long id) { + if (id == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + Optional product = productRepository.findById(id); + return product.orElse(null); + } + + @Transactional(readOnly = true) + public List getExistingProducts(Set productIds) { + + if (productIds == null || productIds.isEmpty()) { + return Collections.emptyList(); + } + List products = productRepository.findAllById(productIds); + + if (products.size() != productIds.size()) { + throw new CoreException( + ErrorType.NOT_FOUND, + "다음 상품 ID들은 찾을 수 없습니다: " + ); + } + return products; + } + + public Product save(Product product) { + return productRepository.save(product); + } + + public List save(List product) { + return productRepository.save(product); + } + + private Sort getSortBySortType(String sortType) { + if (sortType == null) sortType = "latest"; + Sort latestSort = Sort.by("createdAt").descending(); + switch (sortType.toLowerCase()) { + case "latest": + return latestSort; + case "price_asc": + return Sort.by("price").ascending().and(latestSort); + case "likes_desc": + return Sort.by("likesCount").descending().and(latestSort); + default: + return latestSort; + } + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java new file mode 100644 index 000000000..b3f359d5e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java @@ -0,0 +1,25 @@ + +package com.loopers.domain.product; + +import com.loopers.domain.like.LikeRepository; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@RequiredArgsConstructor +@Component +public class ProductStockService { + + public List deductStock(List products, Map quantityMap) { + products.forEach(i -> i.deductStock(quantityMap.get(i.getId()))); + return products; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index b7f64a18f..a6fbad11d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -7,5 +7,5 @@ public interface UserRepository { boolean existsByUserId(String userId); - Optional findByUserId(String userId); + Optional findById(Long userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index dbb459db6..89e1d73ff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -24,11 +24,11 @@ public UserModel join(UserModel user) { } @Transactional(readOnly = true) - public UserModel getUser(String userId) { + public UserModel getUser(Long userId) { //Ask: userId도 UserModel 안에 넣어서 왔으면, userId Model에서 검증 가능 - if (userId == null || userId.isBlank()) { + if (userId == null) { throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); } - return userRepository.findByUserId(userId).orElse(null); + return userRepository.findById(userId).orElse(null); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java new file mode 100644 index 000000000..5ca589a5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandJpaRepository.java @@ -0,0 +1,12 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface BrandJpaRepository extends JpaRepository { + Optional findById(Long id); + + Brand save(Brand brand); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java new file mode 100644 index 000000000..60f26343a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/brand/BrandRepositoryImpl.java @@ -0,0 +1,24 @@ +package com.loopers.infrastructure.brand; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.brand.BrandRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class BrandRepositoryImpl implements BrandRepository { + private final BrandJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public Brand save(Brand brand) { + return jpaRepository.save(brand); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java new file mode 100644 index 000000000..033408bbb --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java @@ -0,0 +1,29 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface LikeJpaRepository extends JpaRepository { + Optional findByUserIdAndProductId(Long userId, Long productId); + + Like save(Like like); + + long deleteByUserIdAndProductId(Long userId, Long productId); + + boolean existsByUserIdAndProductId(Long userId, Long productId); + + long countByProductId(Long productId); + +// @Query( +// value = "SELECT l FROM Like l JOIN FETCH l.product p WHERE l.user.id = :userId", +// countQuery = "SELECT COUNT(l) FROM Like l WHERE l.user.id = :userId" +// ) +// Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); + + Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java new file mode 100644 index 000000000..bc6aa1f6e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java @@ -0,0 +1,47 @@ +package com.loopers.infrastructure.like; + +import com.loopers.domain.like.Like; +import com.loopers.domain.like.LikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class LikeRepositoryImpl implements LikeRepository { + private final LikeJpaRepository jpaRepository; + + @Override + public Optional findById(Long userId, Long productId) { + return jpaRepository.findByUserIdAndProductId(userId, productId); + } + + @Override + public Like save(Like like) { + return jpaRepository.save(like); + } + + @Override + public long remove(Long userId, Long productId) { + return jpaRepository.deleteByUserIdAndProductId(userId, productId); + } + + @Override + public boolean isLiked(Long userId, Long productId) { + return jpaRepository.existsByUserIdAndProductId(userId, productId); + } + + @Override + public long getLikeCount(Long productId) { + return jpaRepository.countByProductId(productId); + } + + @Override + public Page getLikedProducts(Long userId, Pageable pageable) { + return jpaRepository.getLikedProducts(userId, pageable); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java new file mode 100644 index 000000000..2d9c05f07 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderJpaRepository.java @@ -0,0 +1,15 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface OrderJpaRepository extends JpaRepository { + Optional findById(Long id); + + Page findByRefUserId(Long userId, Pageable pageable); + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java new file mode 100644 index 000000000..3e29c2a90 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/order/OrderRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.loopers.infrastructure.order; + +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderRepository; +import com.loopers.domain.product.ProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@RequiredArgsConstructor +@Component +public class OrderRepositoryImpl implements OrderRepository { + private final OrderJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public Page findByUserId(Long userId, Pageable pageable) { + return jpaRepository.findByRefUserId(userId, pageable); + } + + @Override + public Order save(Order order) { + return jpaRepository.save(order); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java new file mode 100644 index 000000000..b595f6115 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -0,0 +1,33 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public interface ProductJpaRepository extends JpaRepository { + Optional findById(Long id); + + List findAllById(Set id); + + Page findByBrandId(Long brandId, Pageable pageable); + + List save(List product); + +// @Query(value = """ +// SELECT new com.loopers.api.product.dto.Product( +// p, +// COUNT(l.id) +// ) +// FROM Product p +// LEFT JOIN Like l ON l.product = p +// GROUP BY p +// ORDER BY p.id DESC +// """, +// countQuery = "SELECT COUNT(p.id) FROM Product p") + // Page findAllWithLikeCount(Pageable pageable); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java new file mode 100644 index 000000000..b14e4326a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -0,0 +1,50 @@ +package com.loopers.infrastructure.product; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@RequiredArgsConstructor +@Component +public class ProductRepositoryImpl implements ProductRepository { + private final ProductJpaRepository jpaRepository; + + @Override + public Optional findById(Long id) { + return jpaRepository.findById(id); + } + + @Override + public List findAllById(Set id) { + return jpaRepository.findAllById(id); + } + + @Override + public Page findByBrandId(Long brandId, Pageable pageable) { + return jpaRepository.findByBrandId(brandId, pageable); + } + + @Override + public Page findAll(Pageable pageable) { + return jpaRepository.findAll(pageable); + } + + @Override + public Product save(Product product) { + return jpaRepository.save(product); + } + + @Override + public List save(List products) { + return jpaRepository.save(products); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java index bd867ae25..c4af76308 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -7,5 +7,6 @@ public interface UserJpaRepository extends JpaRepository { boolean existsByUserId(String userId); - Optional findByUserId(String userId); + + Optional findById(Long id); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java index 3e33c3ff7..f1b0ec1a6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -23,7 +23,7 @@ public boolean existsByUserId(String userId) { } @Override - public Optional findByUserId(String userId) { - return jpaRepository.findByUserId(userId); + public Optional findById(Long userId) { + return jpaRepository.findById(userId); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java new file mode 100644 index 000000000..bbae88628 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java @@ -0,0 +1,37 @@ +package com.loopers.interfaces.api.order; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.loopers.application.order.OrderInfo; +import com.loopers.application.order.OrderItemInfo; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; +import java.util.List; + +public class OrderCreateV1Dto { + public record OrderItemRequest(long productId, long quantity) { + } + + public record OrderRequest(List items) { + } + + public record OrderResponse(long id, String status, + @JsonSerialize(using = ToStringSerializer.class) BigDecimal paymentPrice + , @JsonSerialize(using = ToStringSerializer.class) BigDecimal totalPrice + , @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") ZonedDateTime orderAt + , List orderItemInfo) { + public static OrderResponse from(OrderInfo info) { + if (info == null) return null; + return new OrderResponse( + info.id(), + info.status(), + info.paymentPrice(), + info.totalPrice(), + info.orderAt(), + info.orderItemInfo() + ); + } + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java new file mode 100644 index 000000000..ddaf2bc04 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1ApiSpec.java @@ -0,0 +1,55 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.domain.order.Order; +import com.loopers.interfaces.api.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; + +import java.math.BigDecimal; +import java.util.List; + +@Tag(name = "Order V1 API", description = "Loopers 예시 API 입니다.") +public interface OrderV1ApiSpec { + @Operation( + summary = "주문 목록 조회", + description = "사용자 ID로 주문 목록 조회합니다." + ) + @Valid + ApiResponse> getOrderList( + @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId, + @RequestParam(name = "sortType", defaultValue = "latest") String sortType, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ); + + @Operation( + summary = "주문 요청", + description = "ID로 주문를 충전합니다." + ) + @Valid + ApiResponse createOrder( + @Schema(name = "사용자 ID", description = "충전할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId + , @RequestBody OrderCreateV1Dto.OrderRequest request + ); + + + @Operation( + summary = "주문 상세조회", + description = "주문 ID로 주문 상세조회합니다." + ) + @Valid + ApiResponse getOrderDetail( + @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") + @RequestHeader(value = "X-USER-ID", required = false) Long userId + , Long orderId + ); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java new file mode 100644 index 000000000..8f4f4a144 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java @@ -0,0 +1,63 @@ +package com.loopers.interfaces.api.order; + +import com.loopers.application.example.ExampleInfo; +import com.loopers.application.order.CreateOrderCommand; +import com.loopers.application.order.OrderFacade; +import com.loopers.application.order.OrderInfo; +import com.loopers.domain.order.Order; +import com.loopers.interfaces.api.ApiResponse; +import com.loopers.interfaces.api.example.ExampleV1Dto; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/orders") +public class OrderV1Controller implements OrderV1ApiSpec { + + private final OrderFacade orderFacade; + + @GetMapping("") + @Override + public ApiResponse> getOrderList( + @RequestHeader(value = "X-USER-ID", required = false) Long userId, + @RequestParam(name = "sortType", defaultValue = "latest") String sortType, + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size + ) { + Page info = orderFacade.getOrderList(userId, sortType, page, size); + return ApiResponse.success(info); + } + + @PostMapping("") + @Override + public ApiResponse createOrder(@RequestHeader(value = "X-USER-ID", required = false) Long userId + , @RequestBody OrderCreateV1Dto.OrderRequest request + ) { + Map orderQuantityMap = request.items().stream() + .collect(Collectors.toMap( + OrderCreateV1Dto.OrderItemRequest::productId, + OrderCreateV1Dto.OrderItemRequest::quantity, + Long::sum + )); + OrderInfo info = orderFacade.createOrder(userId, orderQuantityMap); + OrderCreateV1Dto.OrderResponse response = OrderCreateV1Dto.OrderResponse.from(info); + return ApiResponse.success(response); + } + + @GetMapping("/{orderId}") + @Override + public ApiResponse getOrderDetail(@RequestHeader(value = "X-USER-ID", required = false) Long userId + , @PathVariable(value = "orderId") Long orderId + ) { + OrderInfo info = orderFacade.getOrderDetail(orderId); + OrderCreateV1Dto.OrderResponse response = OrderCreateV1Dto.OrderResponse.from(info); + return ApiResponse.success(response); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java new file mode 100644 index 000000000..4edc54cb4 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java @@ -0,0 +1,122 @@ +package com.loopers.application.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.UserModel; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.support.error.CoreException; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + + +@SpringBootTest +@Transactional +class ProductFacadeIntegrationTest { + @Autowired + private ProductFacade productFacade; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + UserModel savedUser; + List savedProducts; + + @BeforeEach + void setup() { + // arrange + UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrandList = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrandList.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrandList.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrandList.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // act + Page productsPage = productFacade.getProductList(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + // act + Page productsPage = productFacade.getProductList(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + ProductDetailInfo result = productFacade.getProductDetail(savedUser.getId(), savedProducts.get(0).getId()); + + // assert + assertThat(result.brandInfo().name()).isEqualTo(savedProducts.get(0).getBrand().getName()); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + // act + // assert + assertThrows(CoreException.class, () -> { + productFacade.getProductDetail(savedUser.getId(), (long) 100); + }); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java new file mode 100644 index 000000000..bdf45dac1 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandAssertions.java @@ -0,0 +1,15 @@ +package com.loopers.domain.brand; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BrandAssertions { + public static void assertBrand(Brand actual, Brand expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getStory()).isEqualTo(expected.getStory()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java new file mode 100644 index 000000000..083acdf16 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java @@ -0,0 +1,49 @@ +package com.loopers.domain.brand; + +import com.loopers.domain.product.Product; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BrandModelTest { + Brand brand; + String validMsg = ""; + + @DisplayName("브랜드 모델을 생성할 때, ") + @Nested + class Create_Brand { + @DisplayName("브랜드명과 스토리가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Brand_객체생성() { + //given + brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + + //assert + assertThat(brand).isNotNull(); + assertThat(brand.getName()).isEqualTo("레이브"); + assertThat(brand.getStory()).isEqualTo("레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + } + } + + @Nested + class Valid_Brand { + @BeforeEach + void setup() { + validMsg = "브랜드명은 비어있을 수 없습니다."; + } + + @Test + void 실패_이름_오류() { + assertThatThrownBy(() -> { + brand = Brand.create("", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java new file mode 100644 index 000000000..1a7346003 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandServiceIntegrationTest.java @@ -0,0 +1,76 @@ +package com.loopers.domain.brand; + +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +class BrandServiceIntegrationTest { + @Autowired + private BrandService brandService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + Brand brand; + + @BeforeEach + void setup() { + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("브랜드를 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 브랜드 ID를 주면, 해당 브랜드 정보를 반환한다.") + @Test + void 성공_존재하는_브랜드ID() { + // arrange + List brands = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrands = brands.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + // act + Brand result = brandService.getBrand(savedBrands.get(0).getId()); + + // assert + assertBrand(result, savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 브랜드 ID를 주면, NOT_FOUND 예외가 발생한다.") + @Test + void 실패_존재하지_않는_브랜드ID() { + // arrange + + // act + Brand result = brandService.getBrand((long) 1); + + // assert + assertThat(result).isNull(); + } + } + + public static void assertBrand(Brand actual, Brand expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getStory()).isEqualTo(expected.getStory()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java new file mode 100644 index 000000000..e6e6a40ec --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/FakeLikeRepository.java @@ -0,0 +1,93 @@ +package com.loopers.domain.like; + +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Component +public class FakeLikeRepository implements LikeRepository { + private final Map list = new HashMap<>(); + + @Override + public Optional findById(Long userId, Long productId) { + Long id = (long) 0; + for (Long key : list.keySet()) { + id = key; + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + return Optional.of(like); + } + } + return null; + } + + @Override + public Like save(Like entity) { + Long id = (long) 0; + for (Long key : list.keySet()) { + id = key; + Like like = list.get(key); + if (like.getUser().getId() == entity.getUser().getId() + && like.getProduct().getId() == entity.getUser().getId()) { + throw new CoreException(ErrorType.BAD_REQUEST, "중복 좋아요 요청입니다."); + } + } + id = id == (long) 0 ? 0 : id + 1; + list.put(id, entity); + return entity; + } + + @Override + public long remove(Long userId, Long productId) { + long cnt = 0; + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + list.remove(key); + cnt++; + } + } + return cnt; + } + + @Override + public boolean isLiked(Long userId, Long productId) { + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getUser().getId() == userId + && like.getProduct().getId() == productId) { + return true; + } + } + return false; + } + + @Override + public long getLikeCount(Long productId) { + long cnt = 0; + for (Long key : list.keySet()) { + Like like = list.get(key); + if (like.getProduct().getId() == productId) { + cnt++; + } + } + return cnt; + } + + @Override + public Page getLikedProducts(Long userId, Pageable pageable) { + return new PageImpl<>(list.values().stream().filter(i -> i.getUser().getId() == userId) + .collect(Collectors.toList()), pageable, list.size()); + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java new file mode 100644 index 000000000..4f8201b0c --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java @@ -0,0 +1,16 @@ +package com.loopers.domain.like; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.user.UserModel; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LikeAssertions { + public static void assertLike(Like actual, Like expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getUser().getId()).isEqualTo(expected.getUser().getId()); + assertThat(actual.getProduct().getId()).isEqualTo(expected.getProduct().getId()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java new file mode 100644 index 000000000..a26061747 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java @@ -0,0 +1,69 @@ +package com.loopers.domain.like; + +import com.loopers.domain.product.Product; +import com.loopers.domain.user.UserModel; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LikeModelTest { + Like like; + UserModel mockUser = mock(UserModel.class); + Product mockProduct = mock(Product.class); + + @DisplayName("좋아요 모델을 생성할 때, ") + @Nested + class Create_Like { + @DisplayName("사용자id와 상품id가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Like_객체생성() { + //given + mockUser = mock(UserModel.class); + when(mockUser.getId()).thenReturn(1L); + mockProduct = mock(Product.class); + when(mockProduct.getId()).thenReturn(10L); + //act + like = Like.create(mockUser, mockProduct); + //assert + assertThat(like).isNotNull(); + assertThat(like.getUser().getId()).isEqualTo(mockUser.getId()); + assertThat(like.getProduct().getId()).isEqualTo(mockProduct.getId()); + } + } + + @Nested + class Valid_Like { + @BeforeEach + void setup() { + mockUser = mock(UserModel.class); + when(mockUser.getId()).thenReturn(1L); + mockProduct = mock(Product.class); + when(mockProduct.getId()).thenReturn(10L); + } + + @Test + void 실패_사용자ID_오류() { + //given + when(mockUser.getId()).thenReturn(null); + assertThatThrownBy(() -> { + like = Like.create(mockUser, mockProduct); + }).isInstanceOf(CoreException.class).hasMessageContaining("사용자ID는 비어있을 수 없습니다."); + } + + @Test + void 실패_상품ID_오류() { + //given + when(mockProduct.getId()).thenReturn(null); + assertThatThrownBy(() -> { + like = Like.create(mockUser, mockProduct); + }).isInstanceOf(CoreException.class).hasMessageContaining("상품ID는 비어있을 수 없습니다."); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java new file mode 100644 index 000000000..4fbe583f7 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java @@ -0,0 +1,153 @@ +package com.loopers.domain.like; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.UserModel; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.like.LikeJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.like.LikeAssertions.assertLike; +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@SpringBootTest +class LikeServiceIntegrationTest { + @Autowired + private LikeService likeService; + + @MockitoSpyBean + private LikeJpaRepository likeJpaRepository; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + UserModel savedUser; + Brand savedBrand; + Product savedProduct; + + @TestConfiguration + static class FakeRepositoryConfig { + @Bean + public LikeRepository likeRepository() { + return new FakeLikeRepository(); + } + } + + @BeforeEach + void setup() { + UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + savedBrand = brandJpaRepository.save(brand); + Product product = Product.create(savedBrand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + savedProduct = productJpaRepository.save(product); + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("좋아요를 조회할 때,") + @Nested + class Create { + @DisplayName("존재하는 좋아요 ID를 주면, 해당 좋아요 정보를 반환한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + + // act + Like result = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result, Like.create(savedUser, savedProduct)); + } + + @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Like result2 = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result1, result2); + } + } + + @DisplayName("좋아요를 삭제할 때,") + @Nested + class Delete { + @DisplayName("존재하는 좋아요를 삭제한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + verify(likeJpaRepository, times(1)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); + //assertThrows(NotFoundException.class, () -> service.read(id)); + } + + @DisplayName("존재하지 않는 좋아요 ID를 삭제하면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_삭제된_좋아요() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + verify(likeJpaRepository, times(1)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); + + } + } + + @DisplayName("좋아요 상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_좋아요_상품목록조회() { + // arrange + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Page productsPage = likeService.getLikedProducts(savedUser.getId(), "latest", 0, 20); + List products = productsPage.getContent(); + + // assert + assertThat(products).isNotEmpty().hasSize(1); + } + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java new file mode 100644 index 000000000..5ebaef8db --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java @@ -0,0 +1,150 @@ +package com.loopers.domain.like; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.user.UserModel; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.like.LikeJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.like.LikeAssertions.assertLike; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + + +@SpringBootTest +class LikeServiceTest { + @Autowired + private LikeService likeService; + + @TestConfiguration + static class FakeRepositoryConfig { + + @Primary + @Bean + public LikeRepository likeRepository() { + return new FakeLikeRepository(); + } + } + + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + UserModel savedUser; + Brand savedBrand; + Product savedProduct; + + @BeforeEach + void setup() { + UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + savedBrand = brandJpaRepository.save(brand); + Product product = Product.create(savedBrand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + savedProduct = productJpaRepository.save(product); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("좋아요를 조회할 때,") + @Nested + class Create { + @DisplayName("존재하는 좋아요 ID를 주면, 해당 좋아요 정보를 반환한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + + // act + Like result = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result, Like.create(savedUser, savedProduct)); + } + + @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Like result2 = likeService.save(Like.create(savedUser, savedProduct)); + + // assert + assertLike(result1, result2); + } + } + + @DisplayName("좋아요를 삭제할 때,") + @Nested + class Delete { + @DisplayName("존재하는 좋아요를 삭제한다.") + @Test + void 성공_존재하는_좋아요ID() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + //assert + + } + + @DisplayName("존재하지 않는 좋아요 ID를 삭제하면, 예외가 발생하지 않는다.") + @Test + void 성공_이미_삭제된_좋아요() { + // arrange + Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + + // act + likeService.remove(savedUser.getId(), savedProduct.getId()); + likeService.remove(savedUser.getId(), savedProduct.getId()); + + // assert + } + } + + @DisplayName("좋아요 상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_좋아요_상품목록조회() { + // arrange + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); + + // act + Page productsPage = likeService.getLikedProducts(savedUser.getId(), "latest", 0, 20); + List products = productsPage.getContent(); + + // assert + assertThat(products).isNotEmpty().hasSize(1); + } + + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java new file mode 100644 index 000000000..dedfe2fe2 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java @@ -0,0 +1,16 @@ +package com.loopers.domain.order; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OrderAssertions { + public static void assertOrder(Order actual, Order expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getRefUserId()).isEqualTo(expected.getRefUserId()); + assertThat(actual.getStatus()).isEqualTo(expected.getStatus()); + assertThat(actual.getPaymentPrice()).isEqualByComparingTo(expected.getPaymentPrice()); + assertThat(actual.getTotalPrice()).isEqualByComparingTo(expected.getTotalPrice()); + assertThat(actual.getOrderAt()).isEqualTo(expected.getOrderAt()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java new file mode 100644 index 000000000..ace14126f --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java @@ -0,0 +1,103 @@ +package com.loopers.domain.order; + +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + +class OrderModelTest { + Order order; + + @DisplayName("주문 모델을 생성할 때, ") + @Nested + class Create_Order { + @DisplayName("사용자ID, 상태, 지불금액,총금액, 주문일자가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Order_객체생성() { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + assertThat(order.getRefUserId()).isEqualTo(1); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); + + } + } + + @DisplayName("주문 모델을 생성할 때, 검증") + @Nested + class Valid_Order { + + @Test + void 실패_지불가격_음수오류() { + assertThrows(CoreException.class, () -> { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(-10_000), new BigDecimal(10_000), ZonedDateTime.now()); + }); + } + + @Test + void 실패_총가격_음수오류() { + assertThrows(CoreException.class, () -> { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(-10_000), ZonedDateTime.now()); + }); + } + } + + @DisplayName("주문 확인후, 상품준비중 상태로 변경") + @Nested + class Valid_상품준비중 { + @Test + void 실패_주문확인_결재전오류() { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(-10_000), ZonedDateTime.now()); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("결재대기중"); + } + + @Test + void 실패_주문확인_상품준비중오류() { + order = Order.create(1, OrderStatus.PREPARING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("이미 준비중"); + } + + @Test + void 실패_주문확인_상품준비완료오류() { + order = Order.create(1, OrderStatus.SHIPPED, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("상품준비 완료"); + } + + @Test + void 성공_상품준비중() { + order = Order.create(1, OrderStatus.PAID, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order.preparing(); + assertThat(order.getStatus()).isEqualTo(OrderStatus.PREPARING); + } + } + + @DisplayName("상품 모델을 생성후, 주문취소") + @Nested + class Valid_주문취소 { + @Test + void 실패_주문취소_이미취소된주문오류() { + order = Order.create(1, OrderStatus.CANCELLED, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + assertThatThrownBy(() -> { + order.preparing(); + }).isInstanceOf(CoreException.class).hasMessageContaining("이미 취소"); + } + + @Test + void 성공_주문취소() { + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order.cancel(); + assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java new file mode 100644 index 000000000..a3e21b571 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderServiceIntegrationTest.java @@ -0,0 +1,120 @@ +package com.loopers.domain.order; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.brand.BrandAssertions.assertBrand; +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@Transactional +class OrderServiceIntegrationTest { + @Autowired + private ProductService productService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + List savedBrands; + List savedProducts; + + @BeforeEach + void setup() { + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + savedBrands = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrands.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrands.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrands.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // arrange + + // act + Page productsPage = productService.getProducts(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + + // act + Page productsPage = productService.getProducts(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + Product result = productService.getProduct(savedProducts.get(0).getId()); + + // assert + assertProduct(result, savedProducts.get(0)); + assertBrand(result.getBrand(), savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, null이 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + + // act + Product result = productService.getProduct((long) 1000); + + // assert + assertThat(result).isNull(); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java new file mode 100644 index 000000000..2c437632d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductAssertions.java @@ -0,0 +1,18 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ProductAssertions { + public static void assertProduct(Product actual, Product expected) { + assertThat(actual).isNotNull(); + assertThat(expected).isNotNull(); + assertThat(actual.getId()).isEqualTo(expected.getId()); + assertThat(actual.getName()).isEqualTo(expected.getName()); + assertThat(actual.getPrice()).isEqualByComparingTo(expected.getPrice()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java new file mode 100644 index 000000000..91cf461b0 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java @@ -0,0 +1,93 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.support.error.CoreException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.Assert.assertThrows; + +class ProductModelTest { + Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); + Product product; + String validMsg = ""; + + @DisplayName("상품 모델을 생성할 때, ") + @Nested + class Create_Product { + @DisplayName("브랜드모델, 상품명, 가격, 재고, 예약재고가 모두 주어지면, 정상적으로 생성된다.") + @Test + void 성공_Product_객체생성() { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + assertThat(product).isNotNull(); + assertThat(product.getName()).isEqualTo("Wild Faith Rose Sweatshirt"); + assertThat(product.getPrice()).isEqualTo(new BigDecimal(80_000)); + assertThat(product.getStock()).isEqualTo(10); + } + } + + @DisplayName("상품 모델을 생성할 때, 검증") + @Nested + class Valid_Product { + @BeforeEach + void setup() { + validMsg = "형식이 유효하지 않습니다."; + } + + @Test + void 실패_가격_음수오류() { + assertThrows(CoreException.class, () -> { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(-1000), 0); + }); + } + + @Test + void 실패_재고_음수오류() { + assertThrows(CoreException.class, () -> { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), -1); + }); + } + + @Test + void 실패_예약재고_음수오류() { + assertThrows(CoreException.class, () -> { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); + }); + } + } + + @DisplayName("상품 모델을 생성후, 재고 예약") + @Nested + class Valid_재고차감 { + @BeforeEach + void setup() { + product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(1000), 10); + } + + @Test + void 실패_예약재고0_차감오류() { + assertThatThrownBy(() -> { + product.deductStock(0); + }).isInstanceOf(CoreException.class).hasMessageContaining("재고차감 이상"); + } + + @Test + void 실패_예약재고20_차감오류() { + assertThatThrownBy(() -> { + product.deductStock(20); + }).isInstanceOf(CoreException.class).hasMessageContaining("재고차감 이상"); + } + + @Test + void 성공_재고예약() { + product.deductStock(2); + assertThat(product.getStock()).isEqualTo(8); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java new file mode 100644 index 000000000..b1693262e --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductServiceIntegrationTest.java @@ -0,0 +1,118 @@ +package com.loopers.domain.product; + +import com.loopers.domain.brand.Brand; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; + +import static com.loopers.domain.brand.BrandAssertions.assertBrand; +import static com.loopers.domain.product.ProductAssertions.assertProduct; +import static org.assertj.core.api.Assertions.assertThat; + + +@SpringBootTest +@Transactional +class ProductServiceIntegrationTest { + @Autowired + private ProductService productService; + + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductJpaRepository productJpaRepository; + + @Autowired + private DatabaseCleanUp databaseCleanUp; + + List savedBrands; + List savedProducts; + + @BeforeEach + void setup() { + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + savedBrands = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrands.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10) + , Product.create(savedBrands.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + , Product.create(savedBrands.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("상품목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_상품목록조회() { + // arrange + + // act + Page productsPage = productService.getProducts(null, "latest", 0, 20); + List products = productsPage.getContent(); + // assert + assertThat(products).isNotEmpty().hasSize(3); + } + + @DisplayName("브랜드ID 검색조건 포함시, 해당 브랜드의 상품 목록이 조회된다.") + @Test + void 성공_상품목록조회_브랜드ID() { + // arrange + + // act + Page productsPage = productService.getProducts(savedProducts.get(0).getId(), null, 0, 20); + List productList = productsPage.getContent(); + + // assert + assertThat(productList).isNotEmpty().hasSize(2); + + assertProduct(productList.get(0), savedProducts.get(1)); + assertProduct(productList.get(1), savedProducts.get(0)); + } + } + + @DisplayName("상품을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + Product result = productService.getProduct(savedProducts.get(0).getId()); + + // assert + assertProduct(result, savedProducts.get(0)); + assertBrand(result.getBrand(), savedBrands.get(0)); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, null이 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + + // act + Product result = productService.getProduct((long) 1000); + + // assert + assertThat(result).isNull(); + } + } + +} From e4b871106dc00fbea5013b6e1a5a52397db2a46d Mon Sep 17 00:00:00 2001 From: sieun0322 Date: Fri, 14 Nov 2025 12:18:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20OrderFacade=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/order/CreateOrderCommand.java | 19 +- .../application/order/OrderFacade.java | 38 ++-- .../loopers/application/order/OrderInfo.java | 7 +- .../application/point/PointFacade.java | 15 +- .../loopers/application/point/PointInfo.java | 4 +- .../loopers/application/user/UserFacade.java | 6 +- .../loopers/application/user/UserInfo.java | 4 +- .../java/com/loopers/domain/like/Like.java | 8 +- .../domain/order/CreateOrderService.java | 40 +++++ .../java/com/loopers/domain/order/Order.java | 19 +- .../com/loopers/domain/order/OrderItem.java | 2 +- .../loopers/domain/order/OrderPreparer.java | 35 ++++ .../loopers/domain/order/OrderRepository.java | 2 +- .../com/loopers/domain/order/OrderStatus.java | 8 + .../point/{PointModel.java => Point.java} | 22 +-- .../loopers/domain/point/PointRepository.java | 7 +- .../loopers/domain/point/PointService.java | 28 +-- .../com/loopers/domain/product/Product.java | 15 +- .../domain/product/ProductService.java | 13 +- .../domain/product/ProductStockService.java | 25 +-- .../domain/user/{UserModel.java => User.java} | 18 +- .../loopers/domain/user/UserPointService.java | 27 +++ .../loopers/domain/user/UserRepository.java | 7 +- .../com/loopers/domain/user/UserService.java | 26 ++- .../like/LikeJpaRepository.java | 13 +- .../like/LikeRepositoryImpl.java | 2 + .../point/PointJpaRepository.java | 13 +- .../point/PointRepositoryImpl.java | 16 +- .../product/ProductJpaRepository.java | 3 +- .../product/ProductRepositoryImpl.java | 4 +- .../user/UserJpaRepository.java | 8 +- .../user/UserRepositoryImpl.java | 11 +- .../api/order/OrderCreateV1Dto.java | 4 +- .../api/order/OrderV1Controller.java | 16 +- .../interfaces/api/point/PointV1ApiSpec.java | 4 +- .../api/point/PointV1Controller.java | 4 +- .../com/loopers/support/error/ErrorType.java | 20 ++- .../order/OrderFacadeIntegrationTest.java | 168 ++++++++++++++++++ .../product/ProductFacadeIntegrationTest.java | 6 +- .../loopers/domain/brand/BrandModelTest.java | 2 +- .../loopers/domain/like/LikeAssertions.java | 3 - .../loopers/domain/like/LikeModelTest.java | 8 +- .../like/LikeServiceIntegrationTest.java | 18 +- .../loopers/domain/like/LikeServiceTest.java | 21 ++- .../loopers/domain/order/OrderAssertions.java | 1 - .../loopers/domain/order/OrderModelTest.java | 32 ++-- .../point/PointServiceIntegrationTest.java | 24 +-- .../{PointModelTest.java => PointTest.java} | 30 ++-- .../domain/product/ProductModelTest.java | 14 +- .../user/UserServiceIntegrationTest.java | 36 ++-- .../{UserModelTest.java => UserTest.java} | 26 +-- .../interfaces/api/PointV1ApiE2ETest.java | 23 +-- .../interfaces/api/UserV1ApiE2ETest.java | 11 +- 53 files changed, 613 insertions(+), 323 deletions(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java rename apps/commerce-api/src/main/java/com/loopers/domain/point/{PointModel.java => Point.java} (67%) rename apps/commerce-api/src/main/java/com/loopers/domain/user/{UserModel.java => User.java} (78%) create mode 100644 apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java rename apps/commerce-api/src/test/java/com/loopers/domain/point/{PointModelTest.java => PointTest.java} (62%) rename apps/commerce-api/src/test/java/com/loopers/domain/user/{UserModelTest.java => UserTest.java} (72%) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java index 76f9c2c74..61aa9f7e3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/CreateOrderCommand.java @@ -1,21 +1,22 @@ package com.loopers.application.order; -import com.loopers.domain.order.Order; import com.loopers.interfaces.api.order.OrderCreateV1Dto; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; -import java.math.BigDecimal; -import java.time.ZonedDateTime; -import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; -public record CreateOrderCommand(Long userId, List orderItemInfo) { - public record ItemCommand(Long productId, Long quantity) { +public record CreateOrderCommand(Long userId, Map orderItemInfo) { + public record ItemCommand(Long productId, Map quantity) { } public static CreateOrderCommand from(Long userId, OrderCreateV1Dto.OrderRequest request) { return new CreateOrderCommand( userId, - request.items().stream().map(i -> new ItemCommand(i.productId(), i.quantity())).toList()); + request.items().stream() + .collect(Collectors.toMap( + OrderCreateV1Dto.OrderItemRequest::productId, + OrderCreateV1Dto.OrderItemRequest::quantity, + Long::sum + ))); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java index 9aa8f0570..63fcc98bc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -1,22 +1,20 @@ package com.loopers.application.order; +import com.loopers.domain.order.CreateOrderService; import com.loopers.domain.order.Order; -import com.loopers.domain.order.OrderItem; import com.loopers.domain.order.OrderService; -import com.loopers.domain.order.OrderStatus; import com.loopers.domain.point.PointService; import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductService; import com.loopers.domain.product.ProductStockService; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserPointService; import com.loopers.domain.user.UserService; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; -import java.math.BigDecimal; -import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -27,8 +25,10 @@ public class OrderFacade { private final PointService pointService; private final ProductService productService; private final OrderService orderService; + private final CreateOrderService createOrderService; private final ProductStockService productStockService; + private final UserPointService userPointService; public Page getOrderList(Long userId, String sortType, @@ -43,28 +43,16 @@ public OrderInfo getOrderDetail(Long orderId) { } @Transactional - public OrderInfo createOrder(Long userId, Map productQuantityMap) { - //사용자 존재 확인 - UserModel user = userService.getUser(userId); + public OrderInfo createOrder(CreateOrderCommand command) { - //상품 존재 확인 - List productList = productService.getExistingProducts(productQuantityMap.keySet()); - - //재고확인및 차감 - List deductedProducts = productStockService.deductStock(productList, productQuantityMap); - productService.save(deductedProducts); - - //포인트 확인 및 차감 - BigDecimal totalPrice = deductedProducts.stream() - .map(Product::getPrice) - .reduce(BigDecimal.ZERO, BigDecimal::add); - pointService.use(user, totalPrice); - - //주문 생성 - Order order = Order.create(userId, OrderStatus.PAID, totalPrice, totalPrice, ZonedDateTime.now()); - order.setOrderItems(deductedProducts.stream().map(i -> OrderItem.create(i.getId(), productQuantityMap.get(i.getId()), i.getPrice())).toList()); - Order savedOrder = orderService.save(order); + Map quantityMap = command.orderItemInfo(); + User user = userService.getActiveUser(command.userId()); + List productList = productService.getExistingProducts(quantityMap.keySet()); + Order savedOrder = null; + productStockService.deduct(productList, quantityMap); + userPointService.use(user, productList, quantityMap); + savedOrder = createOrderService.save(user, productList, quantityMap); return OrderInfo.from(savedOrder); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java index d83bb2494..95d20d92b 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderInfo.java @@ -1,9 +1,5 @@ package com.loopers.application.order; -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import com.loopers.application.brand.BrandInfo; import com.loopers.domain.order.Order; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; @@ -12,14 +8,13 @@ import java.time.ZonedDateTime; import java.util.List; -public record OrderInfo(long id, String status, BigDecimal paymentPrice, BigDecimal totalPrice +public record OrderInfo(long id, String status, BigDecimal totalPrice , ZonedDateTime orderAt, List orderItemInfo) { public static OrderInfo from(Order model) { if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "주문정보를 찾을수 없습니다."); return new OrderInfo( model.getId(), model.getStatus().name(), - model.getPaymentPrice(), model.getTotalPrice(), model.getOrderAt(), OrderItemInfo.from(model.getOrderItems()) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java index 92b3efd79..8a85275ff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointFacade.java @@ -1,10 +1,8 @@ package com.loopers.application.point; import com.loopers.domain.point.PointService; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -16,14 +14,13 @@ public class PointFacade { private final UserService userService; private final PointService pointService; - public BigDecimal getPoint(String userId) { - UserModel user = userService.getUser(userId); - return pointService.getAmount(user.getUserId()); + public BigDecimal getPoint(Long userId) { + User user = userService.getActiveUser(userId); + return user.getPoint().getAmount(); } - public BigDecimal charge(String userId, BigDecimal chargeAmt) { - UserModel user = userService.getUser(userId); - if (user == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); + public BigDecimal charge(Long userId, BigDecimal chargeAmt) { + User user = userService.getActiveUser(userId); return pointService.charge(user, chargeAmt); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java index ff51218d9..ca76d1644 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java @@ -1,11 +1,11 @@ package com.loopers.application.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import java.math.BigDecimal; public record PointInfo(String userId, BigDecimal amount) { - public static PointInfo from(PointModel model) { + public static PointInfo from(Point model) { if (model == null) return null; return new PointInfo( model.getUser().getUserId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java index f4e3e79c3..9e7703061 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserFacade.java @@ -1,6 +1,6 @@ package com.loopers.application.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.user.UserCreateV1Dto; import lombok.RequiredArgsConstructor; @@ -12,12 +12,12 @@ public class UserFacade { private final UserService userService; public UserInfo join(UserCreateV1Dto.UserRequest requestDto) { - UserModel user = userService.join(UserModel.create(requestDto.userId(), requestDto.email(), requestDto.birthday(), requestDto.gender())); + User user = userService.join(User.create(requestDto.userId(), requestDto.email(), requestDto.birthday(), requestDto.gender())); return UserInfo.from(user); } public UserInfo getUser(String userId) { - UserModel user = userService.getUser(userId); + User user = userService.getUser(userId); return UserInfo.from(user); } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java index df777bb86..16b11be7d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/UserInfo.java @@ -1,11 +1,11 @@ package com.loopers.application.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; public record UserInfo(String userId, String email, String birthday, String gender) { - public static UserInfo from(UserModel model) { + public static UserInfo from(User model) { if (model == null) throw new CoreException(ErrorType.NOT_FOUND, "유저정보를 찾을수 없습니다."); return new UserInfo( model.getUserId(), diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java index 425316747..7015ee53d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/like/Like.java @@ -2,7 +2,7 @@ import com.loopers.domain.BaseEntity; import com.loopers.domain.product.Product; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.*; @@ -16,7 +16,7 @@ public class Like extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ref_user_Id", nullable = false) - private UserModel user; + private User user; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ref_product_Id", nullable = false) @@ -25,12 +25,12 @@ public class Like extends BaseEntity { protected Like() { } - private Like(UserModel user, Product product) { + private Like(User user, Product product) { this.user = user; this.product = product; } - public static Like create(UserModel user, Product product) { + public static Like create(User user, Product product) { if (user == null || user.getId() == null) { throw new CoreException(ErrorType.BAD_REQUEST, "사용자ID는 비어있을 수 없습니다."); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java new file mode 100644 index 000000000..78cd49740 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java @@ -0,0 +1,40 @@ + +package com.loopers.domain.order; + +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductRepository; +import com.loopers.domain.user.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class CreateOrderService { + private final ProductRepository productRepository; + private final OrderRepository orderRepository; + + public Order save(User user, List productList, Map quantityMap) { + + List orderItems = new ArrayList<>(); + + BigDecimal totalPrice = productList.stream().map(product -> { + long quantity = quantityMap.get(product.getId()); + BigDecimal unitPrice = product.getPrice(); + BigDecimal itemTotalPrice = unitPrice.multiply(new BigDecimal(quantity)); + + OrderItem orderItem = OrderItem.create(product.getId(), quantity, unitPrice); + orderItems.add(orderItem); + + return itemTotalPrice; + }).reduce(BigDecimal.ZERO, BigDecimal::add); + + Order order = Order.create(user.getId(), OrderStatus.PAID, totalPrice, orderItems); + return orderRepository.save(order); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java index 3b285f582..157f231b7 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java @@ -12,7 +12,7 @@ import java.util.List; @Entity -@Table(name = "order") +@Table(name = "orders") @Getter public class Order extends BaseEntity { @@ -20,8 +20,6 @@ public class Order extends BaseEntity { private OrderStatus status; - BigDecimal paymentPrice; - BigDecimal totalPrice; ZonedDateTime orderAt; @@ -40,25 +38,22 @@ public void setOrderItems(List orderItems) { protected Order() { } - private Order(long refUserId, OrderStatus status, BigDecimal paymentPrice, BigDecimal totalPrice, ZonedDateTime orderAt) { + private Order(long refUserId, OrderStatus status, BigDecimal totalPrice, List orderItems) { this.refUserId = refUserId; this.status = status; - this.paymentPrice = paymentPrice; this.totalPrice = totalPrice; - this.orderAt = orderAt; + this.orderAt = ZonedDateTime.now(); + this.orderItems = orderItems; } - public static Order create(long refUserId, OrderStatus status, BigDecimal paymentPrice, BigDecimal totalPrice, ZonedDateTime orderAt) { + public static Order create(long refUserId, OrderStatus status, BigDecimal totalPrice, List orderItems) { if (status == null) { throw new CoreException(ErrorType.BAD_REQUEST, "상태 정보는 비어있을 수 없습니다."); } - if (paymentPrice.compareTo(BigDecimal.ZERO) < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "지불금액은 음수일수 없습니다."); - } if (totalPrice.compareTo(BigDecimal.ZERO) < 0) { throw new CoreException(ErrorType.BAD_REQUEST, "총가격은 음수일수 없습니다."); } - return new Order(refUserId, status, paymentPrice, totalPrice, orderAt); + return new Order(refUserId, status, totalPrice, orderItems); } public void cancel() { @@ -75,7 +70,7 @@ public void preparing() { if (status == OrderStatus.PREPARING) { throw new CoreException(ErrorType.BAD_REQUEST, "이미 준비중인 주문입니다."); } - if (OrderStatus.PAID.getSequence() < status.getSequence()) { + if (status.compare(OrderStatus.PREPARING) > 0) { throw new CoreException(ErrorType.BAD_REQUEST, "상품준비 완료된 주문입니다."); } this.status = OrderStatus.PREPARING; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java index 07e51557e..17c324510 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderItem.java @@ -10,7 +10,7 @@ import java.math.BigDecimal; @Entity -@Table(name = "orderItem") +@Table(name = "order_item") @Getter public class OrderItem extends BaseEntity { diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java new file mode 100644 index 000000000..6a4a29d5a --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderPreparer.java @@ -0,0 +1,35 @@ + +package com.loopers.domain.order; + +import com.loopers.domain.point.Point; +import com.loopers.domain.product.Product; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class OrderPreparer { + public static List verifyProductStockDeductable(List products, Map quantityMap) { + products.forEach(i -> i.deductStock(quantityMap.get(i.getId()))); + return products; + } + public static List verifyProductStockAddable(List products, Map quantityMap) { + products.forEach(i -> i.addStock(quantityMap.get(i.getId()))); + return products; + } + + public static BigDecimal getTotalAmt(Point point, List products, Map quantityMap) { + return products.stream() + .map(product -> { + Long productId = product.getId(); + Long quantity = quantityMap.getOrDefault(productId, 0L); + return product.getPrice().multiply(BigDecimal.valueOf(quantity)); + }) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java index c03bd66a8..4ba06bc61 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderRepository.java @@ -10,6 +10,6 @@ public interface OrderRepository { Page findByUserId(Long userId, Pageable pageable); - Order save(Order product); + Order save(Order order); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java index b93673616..8dd95ef7e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/OrderStatus.java @@ -1,5 +1,8 @@ package com.loopers.domain.order; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -15,5 +18,10 @@ public enum OrderStatus { REFUNDED("환불 완료", 100); private final String description; + @Getter(AccessLevel.NONE) private final int sequence; + + public Integer compare(OrderStatus other) { + return Integer.compare(this.sequence, other.sequence); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java similarity index 67% rename from apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java rename to apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index db3321685..0264af717 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -1,7 +1,7 @@ package com.loopers.domain.point; import com.loopers.domain.BaseEntity; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.*; @@ -13,33 +13,33 @@ @Entity @Table(name = "point") @Getter -public class PointModel extends BaseEntity { +public class Point extends BaseEntity { private BigDecimal amount; @OneToOne - @JoinColumn(name = "userId", unique = true, nullable = false) - private UserModel user; + @JoinColumn(name = "ref_user_Id", unique = true, nullable = false) + private User user; - public void setUser(UserModel user) { + public void setUser(User user) { this.user = user; if (user.getPoint() != this) { user.setPoint(this); } } - protected PointModel() { + protected Point() { this.amount = BigDecimal.ZERO; } - private PointModel(UserModel user, BigDecimal amount) { - Objects.requireNonNull(user, "UserModel must not be null for PointModel creation."); + private Point(User user, BigDecimal amount) { + Objects.requireNonNull(user, "유저 정보가 없습니다."); this.setUser(user); this.amount = amount; } - public static PointModel create(UserModel user, BigDecimal amount) { - return new PointModel(user, amount); + public static Point create(User user, BigDecimal amount) { + return new Point(user, amount); } public void charge(BigDecimal amountToChange) { @@ -54,7 +54,7 @@ public void use(BigDecimal amountToChange) { throw new CoreException(ErrorType.BAD_REQUEST, "차감 금액은 0보다 커야 합니다."); } if (this.amount.compareTo(amountToChange) < 0) { - throw new CoreException(ErrorType.BAD_REQUEST, "잔액이 부족합니다."); + throw new CoreException(ErrorType.INSUFFICIENT_POINT, "포인트이가 부족합니다."); } this.amount = this.amount.subtract(amountToChange); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java index 35d2dc95c..0524bc7ac 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointRepository.java @@ -6,12 +6,13 @@ import java.util.Optional; public interface PointRepository { + Optional findByUserId(Long userId); - Optional findByUserId(String userId); + Optional findByUserId(String userId); @Lock(LockModeType.PESSIMISTIC_WRITE) - Optional findByUserIdForUpdate(String userId); + Optional findByUserIdForUpdate(Long userId); - PointModel save(PointModel point); + Point save(Point point); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java index 9b953581d..9482ade25 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/PointService.java @@ -1,7 +1,7 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -18,31 +18,37 @@ public class PointService { private final PointRepository pointRepository; @Transactional(readOnly = true) - public BigDecimal getAmount(String userId) { + public Point getAvailablePoints(Long userId) { return pointRepository.findByUserId(userId) - .map(PointModel::getAmount) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "포인트 정보를 찾을 수 없습니다.")); + } + + @Transactional(readOnly = true) + public BigDecimal getAmount(Long userId) { + return pointRepository.findByUserId(userId) + .map(Point::getAmount) .orElse(null); } @Transactional - public BigDecimal charge(UserModel user, BigDecimal chargeAmt) { - Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getUserId()); - if (pointOpt.isEmpty()) { + public BigDecimal charge(User user, BigDecimal chargeAmt) { + Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getId()); + if (!pointOpt.isPresent()) { throw new CoreException(ErrorType.NOT_FOUND, "현재 포인트 정보를 찾을수 없습니다."); } pointOpt.get().charge(chargeAmt); pointRepository.save(pointOpt.get()); - return getAmount(user.getUserId()); + return getAmount(user.getId()); } @Transactional - public BigDecimal use(UserModel user, BigDecimal useAmt) { - Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getUserId()); - if (pointOpt.isEmpty()) { + public BigDecimal use(User user, BigDecimal useAmt) { + Optional pointOpt = pointRepository.findByUserIdForUpdate(user.getId()); + if (!pointOpt.isPresent()) { throw new CoreException(ErrorType.NOT_FOUND, "현재 포인트 정보를 찾을수 없습니다."); } pointOpt.get().use(useAmt); pointRepository.save(pointOpt.get()); - return getAmount(user.getUserId()); + return getAmount(user.getId()); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java index f5df0758f..0a536113f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java @@ -45,6 +45,12 @@ private Product(Brand brand, String name, BigDecimal price, long stock) { } public static Product create(Brand brand, String name, BigDecimal price, long stock) { + if (price.compareTo(BigDecimal.ZERO) < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "가격은 음수 일수 없습니다."); + } + if (stock < 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "재고는 0보다 커야 합니다."); + } return new Product(brand, name, price, stock); } @@ -53,8 +59,15 @@ public void deductStock(long quantity) { throw new CoreException(ErrorType.BAD_REQUEST, "수량은 0보다 커야 합니다."); } if (this.stock < quantity) { - throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다."); + throw new CoreException(ErrorType.INSUFFICIENT_STOCK, "재고가 부족합니다."); } this.stock -= quantity; } + + public void addStock(long quantity) { + if (quantity <= 0) { + throw new CoreException(ErrorType.BAD_REQUEST, "수량은 0보다 커야 합니다."); + } + this.stock += quantity; + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java index 9819ad90b..ef8fb2cd7 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -1,9 +1,7 @@ package com.loopers.domain.product; -import com.loopers.application.order.CreateOrderCommand; import com.loopers.domain.like.LikeRepository; -import com.loopers.domain.order.Order; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import lombok.RequiredArgsConstructor; @@ -15,6 +13,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.*; +import java.util.stream.Collectors; @RequiredArgsConstructor @Component @@ -55,12 +54,18 @@ public List getExistingProducts(Set productIds) { if (productIds == null || productIds.isEmpty()) { return Collections.emptyList(); } + Set uniqueIds = new HashSet<>(productIds); List products = productRepository.findAllById(productIds); - if (products.size() != productIds.size()) { + if (products.size() != uniqueIds.size()) { + Set foundIds = products.stream() + .map(Product::getId) + .collect(Collectors.toSet()); + + uniqueIds.removeAll(foundIds); throw new CoreException( ErrorType.NOT_FOUND, - "다음 상품 ID들은 찾을 수 없습니다: " + "다음 상품 ID들은 찾을 수 없습니다: " + uniqueIds.toString() ); } return products; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java index b3f359d5e..075b25d6e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java @@ -1,25 +1,26 @@ package com.loopers.domain.product; -import com.loopers.domain.like.LikeRepository; -import com.loopers.support.error.CoreException; -import com.loopers.support.error.ErrorType; +import com.loopers.domain.order.OrderPreparer; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; -import java.util.*; +import java.util.List; +import java.util.Map; @RequiredArgsConstructor @Component public class ProductStockService { + private final ProductRepository productService; - public List deductStock(List products, Map quantityMap) { - products.forEach(i -> i.deductStock(quantityMap.get(i.getId()))); - return products; + public void deduct(List products, Map quantityMap) { + List deductedProducts = OrderPreparer.verifyProductStockDeductable(products, quantityMap); + productService.save(deductedProducts); } + + public void add(List products, Map quantityMap) { + List addedProducts = OrderPreparer.verifyProductStockAddable(products, quantityMap); + productService.save(addedProducts); + } + } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java similarity index 78% rename from apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java rename to apps/commerce-api/src/main/java/com/loopers/domain/user/User.java index 500eae5ef..61c8167c0 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserModel.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/User.java @@ -1,7 +1,7 @@ package com.loopers.domain.user; import com.loopers.domain.BaseEntity; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import com.loopers.support.error.CoreException; import com.loopers.support.error.ErrorType; import jakarta.persistence.*; @@ -15,26 +15,26 @@ @Entity @Table(name = "user") @Getter -public class UserModel extends BaseEntity { +public class User extends BaseEntity { private String userId; private String email; private LocalDate birthday; private String gender; @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false) - private PointModel point; + private Point point; - public void setPoint(PointModel point) { + public void setPoint(Point point) { this.point = point; if (point.getUser() != this) { point.setUser(this); } } - protected UserModel() { + protected User() { } - private UserModel(String userId, String email, String birthday, String gender) { + private User(String userId, String email, String birthday, String gender) { if (!Pattern.compile("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]{1,10}$").matcher(userId).matches()) { throw new CoreException( @@ -67,11 +67,11 @@ private UserModel(String userId, String email, String birthday, String gender) { this.email = email; this.birthday = LocalDate.parse(birthday); this.gender = gender; - this.point = PointModel.create(this, BigDecimal.TEN); + this.point = Point.create(this, BigDecimal.TEN); } - public static UserModel create(String userId, String email, String birthday, String gender) { - return new UserModel(userId, email, birthday, gender); + public static User create(String userId, String email, String birthday, String gender) { + return new User(userId, email, birthday, gender); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java new file mode 100644 index 000000000..467bbcd63 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java @@ -0,0 +1,27 @@ + +package com.loopers.domain.user; + +import com.loopers.domain.order.OrderPreparer; +import com.loopers.domain.point.PointService; +import com.loopers.domain.product.Product; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +public class UserPointService { + + private final PointService pointService; + + @Transactional + public void use(User user, List productList, Map quantityMap) { + + BigDecimal totalAmt = OrderPreparer.getTotalAmt(user.getPoint(), productList, quantityMap); + pointService.use(user, totalAmt); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java index a6fbad11d..2ac424125 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserRepository.java @@ -3,9 +3,12 @@ import java.util.Optional; public interface UserRepository { - UserModel save(UserModel user); + User save(User user); boolean existsByUserId(String userId); - Optional findById(Long userId); + Optional findById(Long id); + + Optional findByUserId(String userId); + } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java index 89e1d73ff..7481cf1cb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserService.java @@ -7,8 +7,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - @RequiredArgsConstructor @Component public class UserService { @@ -16,7 +14,7 @@ public class UserService { private final UserRepository userRepository; @Transactional - public UserModel join(UserModel user) { + public User join(User user) { if (userRepository.existsByUserId(user.getUserId())) { throw new CoreException(ErrorType.BAD_REQUEST, "이미 가입된 ID 입니다."); } @@ -24,11 +22,27 @@ public UserModel join(UserModel user) { } @Transactional(readOnly = true) - public UserModel getUser(Long userId) { - //Ask: userId도 UserModel 안에 넣어서 왔으면, userId Model에서 검증 가능 + public User getUser(Long userId) { if (userId == null) { - throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + throw new CoreException(ErrorType.NOT_FOUND, "ID가 없습니다."); } return userRepository.findById(userId).orElse(null); } + + @Transactional(readOnly = true) + public User getUser(String userId) { + if (userId == null) { + throw new CoreException(ErrorType.NOT_FOUND, "ID가 없습니다."); + } + return userRepository.findByUserId(userId).orElse(null); + } + + @Transactional(readOnly = true) + public User getActiveUser(Long userId) { + if (userId == null) { + throw new CoreException(ErrorType.BAD_REQUEST, "ID가 없습니다."); + } + return userRepository.findById(userId) + .orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "사용자 정보를 찾을 수 없습니다.")); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java index 033408bbb..1008e0adb 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeJpaRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -19,11 +20,11 @@ public interface LikeJpaRepository extends JpaRepository { long countByProductId(Long productId); -// @Query( -// value = "SELECT l FROM Like l JOIN FETCH l.product p WHERE l.user.id = :userId", -// countQuery = "SELECT COUNT(l) FROM Like l WHERE l.user.id = :userId" -// ) -// Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); - + @Query( + value = "SELECT l FROM Like l JOIN FETCH l.product p WHERE l.user.id = :userId", + countQuery = "SELECT COUNT(l) FROM Like l WHERE l.user.id = :userId" + ) Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); + + //Page getLikedProducts(@Param("userId") Long userId, Pageable pageable); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java index bc6aa1f6e..74507560e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/like/LikeRepositoryImpl.java @@ -3,6 +3,7 @@ import com.loopers.domain.like.Like; import com.loopers.domain.like.LikeRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Primary; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Component; @@ -11,6 +12,7 @@ @RequiredArgsConstructor @Component +@Primary public class LikeRepositoryImpl implements LikeRepository { private final LikeJpaRepository jpaRepository; diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java index 2c436109d..a5a85c432 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointJpaRepository.java @@ -1,18 +1,21 @@ package com.loopers.infrastructure.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; -public interface PointJpaRepository extends JpaRepository { +public interface PointJpaRepository extends JpaRepository { boolean existsByUserUserId(String userId); - Optional findByUserUserId(String userId); + Optional findByUserUserId(String userId); + + Optional findByUserId(Long userId); + + @Query("SELECT p FROM Point p WHERE p.user.id = :userId") + Optional findByUserIdForUpdate(@Param("userId") Long userId); - @Query("SELECT p FROM PointModel p WHERE p.user.userId = :userId") - Optional findByUserUserIdForUpdate(@Param("userId") String userId); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java index cce08040d..b9ef6bf0e 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/point/PointRepositoryImpl.java @@ -1,6 +1,6 @@ package com.loopers.infrastructure.point; -import com.loopers.domain.point.PointModel; +import com.loopers.domain.point.Point; import com.loopers.domain.point.PointRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,18 +13,22 @@ public class PointRepositoryImpl implements PointRepository { private final PointJpaRepository jpaRepository; @Override - public PointModel save(PointModel point) { + public Point save(Point point) { return jpaRepository.save(point); } @Override - public Optional findByUserId(String userId) { - return jpaRepository.findByUserUserId(userId); + public Optional findByUserId(Long userId) { + return jpaRepository.findByUserId(userId); } @Override - public Optional findByUserIdForUpdate(String userId) { - return jpaRepository.findByUserUserIdForUpdate(userId); + public Optional findByUserId(String userId) { + return jpaRepository.findByUserUserId(userId); } + @Override + public Optional findByUserIdForUpdate(Long userId) { + return jpaRepository.findByUserIdForUpdate(userId); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java index b595f6115..e2119e400 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductJpaRepository.java @@ -7,12 +7,11 @@ import java.util.List; import java.util.Optional; -import java.util.Set; public interface ProductJpaRepository extends JpaRepository { Optional findById(Long id); - List findAllById(Set id); + List findAllById(Iterable id); Page findByBrandId(Long brandId, Pageable pageable); diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java index b14e4326a..584ac2a5c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/product/ProductRepositoryImpl.java @@ -2,8 +2,6 @@ import com.loopers.domain.product.Product; import com.loopers.domain.product.ProductRepository; -import com.loopers.domain.user.UserModel; -import com.loopers.domain.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -45,6 +43,6 @@ public Product save(Product product) { @Override public List save(List products) { - return jpaRepository.save(products); + return jpaRepository.saveAll(products); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java index c4af76308..015978561 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserJpaRepository.java @@ -1,12 +1,14 @@ package com.loopers.infrastructure.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; -public interface UserJpaRepository extends JpaRepository { +public interface UserJpaRepository extends JpaRepository { boolean existsByUserId(String userId); - Optional findById(Long id); + Optional findById(Long id); + + Optional findByUserId(String id); } diff --git a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java index f1b0ec1a6..e4e85f02d 100644 --- a/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java +++ b/apps/commerce-api/src/main/java/com/loopers/infrastructure/user/UserRepositoryImpl.java @@ -1,6 +1,6 @@ package com.loopers.infrastructure.user; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -13,7 +13,7 @@ public class UserRepositoryImpl implements UserRepository { private final UserJpaRepository jpaRepository; @Override - public UserModel save(UserModel user) { + public User save(User user) { return jpaRepository.save(user); } @@ -23,7 +23,12 @@ public boolean existsByUserId(String userId) { } @Override - public Optional findById(Long userId) { + public Optional findById(Long userId) { return jpaRepository.findById(userId); } + + @Override + public Optional findByUserId(String userId) { + return jpaRepository.findByUserId(userId); + } } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java index bbae88628..0ef09e8b2 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderCreateV1Dto.java @@ -17,8 +17,7 @@ public record OrderItemRequest(long productId, long quantity) { public record OrderRequest(List items) { } - public record OrderResponse(long id, String status, - @JsonSerialize(using = ToStringSerializer.class) BigDecimal paymentPrice + public record OrderResponse(long id, String status , @JsonSerialize(using = ToStringSerializer.class) BigDecimal totalPrice , @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") ZonedDateTime orderAt , List orderItemInfo) { @@ -27,7 +26,6 @@ public static OrderResponse from(OrderInfo info) { return new OrderResponse( info.id(), info.status(), - info.paymentPrice(), info.totalPrice(), info.orderAt(), info.orderItemInfo() diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java index 8f4f4a144..b42521578 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/order/OrderV1Controller.java @@ -1,21 +1,14 @@ package com.loopers.interfaces.api.order; -import com.loopers.application.example.ExampleInfo; import com.loopers.application.order.CreateOrderCommand; import com.loopers.application.order.OrderFacade; import com.loopers.application.order.OrderInfo; import com.loopers.domain.order.Order; import com.loopers.interfaces.api.ApiResponse; -import com.loopers.interfaces.api.example.ExampleV1Dto; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; -import java.math.BigDecimal; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - @RequiredArgsConstructor @RestController @RequestMapping("/api/v1/orders") @@ -40,13 +33,8 @@ public ApiResponse> getOrderList( public ApiResponse createOrder(@RequestHeader(value = "X-USER-ID", required = false) Long userId , @RequestBody OrderCreateV1Dto.OrderRequest request ) { - Map orderQuantityMap = request.items().stream() - .collect(Collectors.toMap( - OrderCreateV1Dto.OrderItemRequest::productId, - OrderCreateV1Dto.OrderItemRequest::quantity, - Long::sum - )); - OrderInfo info = orderFacade.createOrder(userId, orderQuantityMap); + CreateOrderCommand command = CreateOrderCommand.from(userId, request); + OrderInfo info = orderFacade.createOrder(command); OrderCreateV1Dto.OrderResponse response = OrderCreateV1Dto.OrderResponse.from(info); return ApiResponse.success(response); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java index 7e86ed59c..47b7660c9 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1ApiSpec.java @@ -19,7 +19,7 @@ public interface PointV1ApiSpec { @Valid ApiResponse getPoint( @Schema(name = "사용자 ID", description = "조회할 사용자의 ID") - @RequestHeader(value = "X-USER-ID", required = false) String userId + @RequestHeader(value = "X-USER-ID", required = false) Long userId ); @Operation( @@ -29,7 +29,7 @@ ApiResponse getPoint( @Valid ApiResponse charge( @Schema(name = "사용자 ID", description = "충전할 사용자의 ID") - @RequestHeader(value = "X-USER-ID", required = false) String userId + @RequestHeader(value = "X-USER-ID", required = false) Long userId , @RequestBody BigDecimal amount ); } diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java index 211efbe2a..5adeadb43 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/api/point/PointV1Controller.java @@ -16,14 +16,14 @@ public class PointV1Controller implements PointV1ApiSpec { @GetMapping("") @Override - public ApiResponse getPoint(@RequestHeader(value = "X-USER-ID", required = false) String userId + public ApiResponse getPoint(@RequestHeader(value = "X-USER-ID", required = false) Long userId ) { return ApiResponse.success(pointFacade.getPoint(userId)); } @PostMapping("/charge") @Override - public ApiResponse charge(@RequestHeader(value = "X-USER-ID", required = false) String userId + public ApiResponse charge(@RequestHeader(value = "X-USER-ID", required = false) Long userId , @RequestBody BigDecimal amount ) { return ApiResponse.success(pointFacade.charge(userId, amount)); diff --git a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java index 5d142efbf..8bb2324d6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java +++ b/apps/commerce-api/src/main/java/com/loopers/support/error/ErrorType.java @@ -7,13 +7,17 @@ @Getter @RequiredArgsConstructor public enum ErrorType { - /** 범용 에러 */ - INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), - BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), - NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), - CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."); + /** + * 범용 에러 + */ + INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "일시적인 오류가 발생했습니다."), + BAD_REQUEST(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase(), "잘못된 요청입니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(), "존재하지 않는 요청입니다."), + CONFLICT(HttpStatus.CONFLICT, HttpStatus.CONFLICT.getReasonPhrase(), "이미 존재하는 리소스입니다."), - private final HttpStatus status; - private final String code; - private final String message; + INSUFFICIENT_STOCK(HttpStatus.BAD_REQUEST, "INSUFFICIENT_STOCK", "요청하신 상품의 재고가 부족합니다."), + INSUFFICIENT_POINT(HttpStatus.BAD_REQUEST, "INSUFFICIENT_POINT", "사용 가능한 포인트가 부족합니다."); + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java new file mode 100644 index 000000000..cf9123d78 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java @@ -0,0 +1,168 @@ +package com.loopers.application.order; + +import com.loopers.domain.brand.Brand; +import com.loopers.domain.order.Order; +import com.loopers.domain.order.OrderItem; +import com.loopers.domain.order.OrderStatus; +import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; +import com.loopers.domain.user.User; +import com.loopers.infrastructure.brand.BrandJpaRepository; +import com.loopers.infrastructure.order.OrderJpaRepository; +import com.loopers.infrastructure.product.ProductJpaRepository; +import com.loopers.infrastructure.user.UserJpaRepository; +import com.loopers.interfaces.api.order.OrderCreateV1Dto; +import com.loopers.support.error.CoreException; +import com.loopers.support.error.ErrorType; +import com.loopers.utils.DatabaseCleanUp; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + + +@SpringBootTest +@Transactional +class OrderFacadeIntegrationTest { + @Autowired + private OrderFacade orderFacade; + @MockitoSpyBean + private UserJpaRepository userJpaRepository; + @MockitoSpyBean + private BrandJpaRepository brandJpaRepository; + @MockitoSpyBean + private ProductService productService; + + @MockitoSpyBean + private OrderJpaRepository orderJpaRepository; + @Autowired + private DatabaseCleanUp databaseCleanUp; + + User savedUser; + List savedProducts; + Order savedOrder; + + @BeforeEach + void setup() { + // arrange + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + savedUser = userJpaRepository.save(user); + List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") + , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); + List savedBrandList = brandList.stream().map((brand) -> brandJpaRepository.save(brand)).toList(); + + List productList = List.of(Product.create(savedBrandList.get(0), "Wild Faith Rose Sweatshirt", new BigDecimal(8), 10) + , Product.create(savedBrandList.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(4), 10) + , Product.create(savedBrandList.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) + ); + savedProducts = productService.save(productList); + List orderItems = new ArrayList<>(); + orderItems.add(OrderItem.create(user.getId(), 2L, new BigDecimal(5_000))); + Order order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); + savedOrder = orderJpaRepository.save(order); + + } + + @AfterEach + void tearDown() { + databaseCleanUp.truncateAllTables(); + } + + @DisplayName("주문목록을 조회할 때,") + @Nested + class GetList { + @DisplayName("페이징 처리되어, 초기설정시 size=0, sort=최신순으로 목록이 조회된다.") + @Test + void 성공_주문목록조회() { + // act + Page ordersPage = orderFacade.getOrderList(savedUser.getId(), "latest", 0, 20); + List orders = ordersPage.getContent(); + // assert + assertThat(orders).isNotEmpty().hasSize(1); + } + } + + @DisplayName("주문을 조회할 때,") + @Nested + class Get { + @DisplayName("존재하는 상품 ID를 주면, 해당 상품 정보를 반환한다.") + @Test + void 성공_존재하는_상품ID() { + // arrange + // act + OrderInfo result = orderFacade.getOrderDetail(savedOrder.getId()); + + // assert + assertThat(result.id()).isEqualTo(savedOrder.getId()); + assertThat(result.status().toString()).isEqualTo(savedOrder.getStatus().toString()); + assertThat(result.totalPrice()).isEqualTo(savedOrder.getTotalPrice()); + } + + @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") + @Test + void 실패_존재하지_않는_상품ID() { + // arrange + // act + // assert + assertThrows(CoreException.class, () -> { + orderFacade.getOrderDetail(0L); + }); + } + } + + @DisplayName("주문을 생성할 때,") + @Nested + class Post { + @DisplayName("10재고가 있는 8원의 상품 1건 주문시, 기본 포인트10으로 결재가 된다.") + @Test + void 성공_단건주문생성() { + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(savedProducts.get(0).getId(), 1)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + OrderInfo savedOrder = orderFacade.createOrder(orderCommand); + // assert + assertThat(savedOrder).isNotNull(); + assertThat(savedOrder.totalPrice()).isEqualTo(savedProducts.get(0).getPrice()); + assertThat(savedOrder.orderItemInfo()).hasSize(1); + } + + @DisplayName("10재고가 있는 8원의 상품 20건 주문시, 재고 없음 오류가 발생한다.") + @Test + void 실패_재고없음오류() { + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(savedProducts.get(0).getId(), 20)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + // assert + assertThrows(CoreException.class, () -> orderFacade.createOrder(orderCommand)).getErrorType().equals(ErrorType.INSUFFICIENT_STOCK); + } + + @DisplayName("10재고가 있는 4원의 상품 3건 주문시, 포인트 부족 오류가 발생한다.") + @Test + void 실패_포인트부족오류() { + long productId = savedProducts.get(1).getId(); + long quantity = 3L; + List items = new ArrayList<>(); + items.add(new OrderCreateV1Dto.OrderItemRequest(productId, quantity)); + OrderCreateV1Dto.OrderRequest request = new OrderCreateV1Dto.OrderRequest(items); + CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); + // act + // assert + assertThrows(CoreException.class, () -> orderFacade.createOrder(orderCommand)).getErrorType().equals(ErrorType.INSUFFICIENT_POINT); + } + } + +} diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java index 4edc54cb4..d958c1b6d 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java @@ -2,7 +2,7 @@ import com.loopers.domain.brand.Brand; import com.loopers.domain.product.Product; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.infrastructure.brand.BrandJpaRepository; import com.loopers.infrastructure.product.ProductJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; @@ -39,13 +39,13 @@ class ProductFacadeIntegrationTest { @Autowired private DatabaseCleanUp databaseCleanUp; - UserModel savedUser; + User savedUser; List savedProducts; @BeforeEach void setup() { // arrange - UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); savedUser = userJpaRepository.save(user); List brandList = List.of(Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다.") , Brand.create("마뗑킴", "마뗑킴은 트렌디하면서도 편안함을 더한 디자인을 선보입니다. 일상에서 조화롭게 적용할 수 있는 자연스러운 패션 문화를 지향합니다.")); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java index 083acdf16..57e57f3da 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/brand/BrandModelTest.java @@ -43,7 +43,7 @@ void setup() { void 실패_이름_오류() { assertThatThrownBy(() -> { brand = Brand.create("", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); - }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); + }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining(validMsg); } } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java index 4f8201b0c..0c62874c5 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeAssertions.java @@ -1,8 +1,5 @@ package com.loopers.domain.like; -import com.loopers.domain.brand.Brand; -import com.loopers.domain.user.UserModel; - import static org.assertj.core.api.Assertions.assertThat; public class LikeAssertions { diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java index a26061747..a977dc616 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeModelTest.java @@ -1,7 +1,7 @@ package com.loopers.domain.like; import com.loopers.domain.product.Product; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -15,7 +15,7 @@ class LikeModelTest { Like like; - UserModel mockUser = mock(UserModel.class); + User mockUser = mock(User.class); Product mockProduct = mock(Product.class); @DisplayName("좋아요 모델을 생성할 때, ") @@ -25,7 +25,7 @@ class Create_Like { @Test void 성공_Like_객체생성() { //given - mockUser = mock(UserModel.class); + mockUser = mock(User.class); when(mockUser.getId()).thenReturn(1L); mockProduct = mock(Product.class); when(mockProduct.getId()).thenReturn(10L); @@ -42,7 +42,7 @@ class Create_Like { class Valid_Like { @BeforeEach void setup() { - mockUser = mock(UserModel.class); + mockUser = mock(User.class); when(mockUser.getId()).thenReturn(1L); mockProduct = mock(Product.class); when(mockProduct.getId()).thenReturn(10L); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java index 4fbe583f7..ccb7fbeab 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceIntegrationTest.java @@ -2,7 +2,7 @@ import com.loopers.domain.brand.Brand; import com.loopers.domain.product.Product; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.infrastructure.brand.BrandJpaRepository; import com.loopers.infrastructure.like.LikeJpaRepository; import com.loopers.infrastructure.product.ProductJpaRepository; @@ -18,9 +18,9 @@ import java.math.BigDecimal; import java.util.List; +import java.util.Optional; import static com.loopers.domain.like.LikeAssertions.assertLike; -import static com.loopers.domain.product.ProductAssertions.assertProduct; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -43,7 +43,7 @@ class LikeServiceIntegrationTest { @Autowired private DatabaseCleanUp databaseCleanUp; - UserModel savedUser; + User savedUser; Brand savedBrand; Product savedProduct; @@ -57,7 +57,7 @@ public LikeRepository likeRepository() { @BeforeEach void setup() { - UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); savedUser = userJpaRepository.save(user); Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); savedBrand = brandJpaRepository.save(brand); @@ -80,9 +80,9 @@ class Create { // act Like result = likeService.save(Like.create(savedUser, savedProduct)); - + Optional savedLike = likeJpaRepository.findById(result.getId()); // assert - assertLike(result, Like.create(savedUser, savedProduct)); + assertLike(result, savedLike.get()); } @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") @@ -120,14 +120,14 @@ class Delete { @Test void 성공_이미_삭제된_좋아요() { // arrange - Like result1 = likeService.save(Like.create(savedUser, savedProduct)); + Like savedLike = likeService.save(Like.create(savedUser, savedProduct)); // act likeService.remove(savedUser.getId(), savedProduct.getId()); - likeService.remove(savedUser.getId(), savedProduct.getId()); + Long result1 = likeService.remove(savedUser.getId(), savedProduct.getId()); // assert - verify(likeJpaRepository, times(1)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); + verify(likeJpaRepository, times(3)).deleteByUserIdAndProductId(savedUser.getId(), savedProduct.getId()); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java index 5ebaef8db..729d1b8c3 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/like/LikeServiceTest.java @@ -2,9 +2,8 @@ import com.loopers.domain.brand.Brand; import com.loopers.domain.product.Product; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.infrastructure.brand.BrandJpaRepository; -import com.loopers.infrastructure.like.LikeJpaRepository; import com.loopers.infrastructure.product.ProductJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.utils.DatabaseCleanUp; @@ -22,19 +21,22 @@ import static com.loopers.domain.like.LikeAssertions.assertLike; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @SpringBootTest class LikeServiceTest { + + private final LikeService likeService; + @Autowired - private LikeService likeService; + public LikeServiceTest(LikeService likeService) { + this.likeService = likeService; + } @TestConfiguration static class FakeRepositoryConfig { - @Primary @Bean public LikeRepository likeRepository() { return new FakeLikeRepository(); @@ -51,13 +53,13 @@ public LikeRepository likeRepository() { @Autowired private DatabaseCleanUp databaseCleanUp; - UserModel savedUser; + User savedUser; Brand savedBrand; Product savedProduct; @BeforeEach void setup() { - UserModel user = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); savedUser = userJpaRepository.save(user); Brand brand = Brand.create("레이브", "레이브는 음악, 영화, 예술 등 다양한 문화에서 영감을 받아 경계 없고 자유분방한 스타일을 제안하는 패션 레이블입니다."); savedBrand = brandJpaRepository.save(brand); @@ -83,7 +85,8 @@ class Create { Like result = likeService.save(Like.create(savedUser, savedProduct)); // assert - assertLike(result, Like.create(savedUser, savedProduct)); + assertThat(result).isNotNull(); + assertThat(result.getUser()).isNotNull(); } @DisplayName("존재하지 않는 좋아요 ID를 주면, 예외가 발생하지 않는다.") @@ -112,7 +115,7 @@ class Delete { // act likeService.remove(savedUser.getId(), savedProduct.getId()); //assert - + } @DisplayName("존재하지 않는 좋아요 ID를 삭제하면, 예외가 발생하지 않는다.") diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java index dedfe2fe2..65a876a15 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderAssertions.java @@ -9,7 +9,6 @@ public static void assertOrder(Order actual, Order expected) { assertThat(actual.getId()).isEqualTo(expected.getId()); assertThat(actual.getRefUserId()).isEqualTo(expected.getRefUserId()); assertThat(actual.getStatus()).isEqualTo(expected.getStatus()); - assertThat(actual.getPaymentPrice()).isEqualByComparingTo(expected.getPaymentPrice()); assertThat(actual.getTotalPrice()).isEqualByComparingTo(expected.getTotalPrice()); assertThat(actual.getOrderAt()).isEqualTo(expected.getOrderAt()); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java index ace14126f..82d2f1d60 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/order/OrderModelTest.java @@ -6,7 +6,8 @@ import org.junit.jupiter.api.Test; import java.math.BigDecimal; -import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -14,6 +15,7 @@ class OrderModelTest { Order order; + List orderItems; @DisplayName("주문 모델을 생성할 때, ") @Nested @@ -21,7 +23,8 @@ class Create_Order { @DisplayName("사용자ID, 상태, 지불금액,총금액, 주문일자가 모두 주어지면, 정상적으로 생성된다.") @Test void 성공_Order_객체생성() { - order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + orderItems = new ArrayList<>(); + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); assertThat(order.getRefUserId()).isEqualTo(1); assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING); @@ -32,17 +35,10 @@ class Create_Order { @Nested class Valid_Order { - @Test - void 실패_지불가격_음수오류() { - assertThrows(CoreException.class, () -> { - order = Order.create(1, OrderStatus.PENDING, new BigDecimal(-10_000), new BigDecimal(10_000), ZonedDateTime.now()); - }); - } - @Test void 실패_총가격_음수오류() { assertThrows(CoreException.class, () -> { - order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(-10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(-10_000), orderItems); }); } } @@ -52,7 +48,7 @@ class Valid_Order { class Valid_상품준비중 { @Test void 실패_주문확인_결재전오류() { - order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(-10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); assertThatThrownBy(() -> { order.preparing(); }).isInstanceOf(CoreException.class).hasMessageContaining("결재대기중"); @@ -60,7 +56,7 @@ class Valid_상품준비중 { @Test void 실패_주문확인_상품준비중오류() { - order = Order.create(1, OrderStatus.PREPARING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.PREPARING, new BigDecimal(10_000), orderItems); assertThatThrownBy(() -> { order.preparing(); }).isInstanceOf(CoreException.class).hasMessageContaining("이미 준비중"); @@ -68,7 +64,7 @@ class Valid_상품준비중 { @Test void 실패_주문확인_상품준비완료오류() { - order = Order.create(1, OrderStatus.SHIPPED, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.SHIPPED, new BigDecimal(10_000), orderItems); assertThatThrownBy(() -> { order.preparing(); }).isInstanceOf(CoreException.class).hasMessageContaining("상품준비 완료"); @@ -76,7 +72,7 @@ class Valid_상품준비중 { @Test void 성공_상품준비중() { - order = Order.create(1, OrderStatus.PAID, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.PAID, new BigDecimal(10_000), orderItems); order.preparing(); assertThat(order.getStatus()).isEqualTo(OrderStatus.PREPARING); } @@ -87,15 +83,15 @@ class Valid_상품준비중 { class Valid_주문취소 { @Test void 실패_주문취소_이미취소된주문오류() { - order = Order.create(1, OrderStatus.CANCELLED, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); - assertThatThrownBy(() -> { + order = Order.create(1, OrderStatus.CANCELLED, new BigDecimal(10_000), orderItems); + assertThrows(CoreException.class, () -> { order.preparing(); - }).isInstanceOf(CoreException.class).hasMessageContaining("이미 취소"); + }); } @Test void 성공_주문취소() { - order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), new BigDecimal(10_000), ZonedDateTime.now()); + order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); order.cancel(); assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED); } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java index ac000465c..c86ebaea9 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointServiceIntegrationTest.java @@ -1,6 +1,6 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.infrastructure.point.PointJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.support.error.CoreException; @@ -49,11 +49,11 @@ class Get { void 성공_존재하는_유저ID() { // arrange BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User saved = userJpaRepository.save(user); // act - BigDecimal pointAmt = pointService.getAmount(userModel.getUserId()); + BigDecimal pointAmt = pointService.getAmount(saved.getId()); // assert(회원가입시, 기본포인트 10) assertAll( @@ -66,10 +66,10 @@ class Get { @Test void 실패_존재하지않는_유저ID() { // arrange (등록된 회원 없음) - String userId = "userId"; + Long id = 5L; // act - BigDecimal pointAmt = pointService.getAmount(userId); + BigDecimal pointAmt = pointService.getAmount(id); // assert assertThat(pointAmt).isNull(); @@ -84,11 +84,11 @@ class Charge { @Test void 실패_존재하지않는_유저ID() { // arrange (등록된 회원 없음) - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act, assert assertThatThrownBy(() -> { - pointService.charge(userModel, BigDecimal.TEN); + pointService.charge(user, BigDecimal.TEN); }).isInstanceOf(CoreException.class).hasMessageContaining("현재 포인트 정보를 찾을수 없습니다."); } @@ -98,11 +98,11 @@ class Charge { void 성공_존재하는_유저ID() { // arrange BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); - Optional savedUserModel = userJpaRepository.findByUserId(userModel.getUserId()); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userJpaRepository.save(user); + // act - BigDecimal pointAmt = pointService.charge(savedUserModel.get(), BigDecimal.TEN); + BigDecimal pointAmt = pointService.charge(savedUser, BigDecimal.TEN); // assert(회원가입시, 기본포인트 10) assertAll( diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java similarity index 62% rename from apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java index 02bcd0387..345e7f0e6 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -1,6 +1,6 @@ package com.loopers.domain.point; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.support.error.CoreException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -12,13 +12,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class PointModelTest { - UserModel userModel; - PointModel pointModel; +class PointTest { + User user; + Point point; @BeforeEach void setup() { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); } @DisplayName("포인트 충전") @@ -28,24 +28,24 @@ class Charge { @Test void 실패_포인트충전_0() { // arrange - pointModel = PointModel.create(userModel, BigDecimal.ZERO); + point = Point.create(user, BigDecimal.ZERO); // act, assert assertThatThrownBy(() -> { - pointModel.charge(BigDecimal.ZERO); + point.charge(BigDecimal.ZERO); }).isInstanceOf(CoreException.class).hasMessageContaining("충전 금액은 0보다 커야 합니다."); } @Test void 성공_포인트충전() { // arrange - pointModel = PointModel.create(userModel, new BigDecimal(20)); + point = Point.create(user, new BigDecimal(20)); // act - pointModel.charge(new BigDecimal(5)); + point.charge(new BigDecimal(5)); // assert - assertThat(pointModel.getAmount()).isEqualTo(new BigDecimal(25)); + assertThat(point.getAmount()).isEqualTo(new BigDecimal(25)); } } @@ -55,24 +55,24 @@ class Use { @Test void 실패_포인트사용() { // arrange - pointModel = PointModel.create(userModel, BigDecimal.ZERO); + point = Point.create(user, BigDecimal.ZERO); // act, assert assertThatThrownBy(() -> { - pointModel.use(BigDecimal.TEN); + point.use(BigDecimal.TEN); }).isInstanceOf(CoreException.class).hasMessageContaining("잔액이 부족합니다."); } @Test void 성공_포인트사용() { // arrange - pointModel = PointModel.create(userModel, new BigDecimal(20)); + point = Point.create(user, new BigDecimal(20)); // act - pointModel.use(new BigDecimal(5)); + point.use(new BigDecimal(5)); // assert - assertThat(pointModel.getAmount()).isEqualTo(new BigDecimal(15)); + assertThat(point.getAmount()).isEqualTo(new BigDecimal(15)); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java index 91cf461b0..cf44ba293 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/product/ProductModelTest.java @@ -54,12 +54,6 @@ void setup() { }); } - @Test - void 실패_예약재고_음수오류() { - assertThrows(CoreException.class, () -> { - product = Product.create(brand, "Wild Faith Rose Sweatshirt", new BigDecimal(80_000), 10); - }); - } } @DisplayName("상품 모델을 생성후, 재고 예약") @@ -72,16 +66,16 @@ void setup() { @Test void 실패_예약재고0_차감오류() { - assertThatThrownBy(() -> { + assertThrows(CoreException.class, () -> { product.deductStock(0); - }).isInstanceOf(CoreException.class).hasMessageContaining("재고차감 이상"); + }); } @Test void 실패_예약재고20_차감오류() { - assertThatThrownBy(() -> { + assertThrows(CoreException.class, () -> { product.deductStock(20); - }).isInstanceOf(CoreException.class).hasMessageContaining("재고차감 이상"); + }); } @Test diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java index a3a300d5c..7cc9e69a0 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserServiceIntegrationTest.java @@ -39,15 +39,15 @@ class Join { @Test void 성공_회원가입() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act - userService.join(userModel); + userService.join(user); // assert assertAll( - () -> verify(userJpaRepository, times(1)).save(userModel), - () -> assertThrows(CoreException.class, () -> userService.join(userModel)) + () -> verify(userJpaRepository, times(1)).save(user), + () -> assertThrows(CoreException.class, () -> userService.join(user)) ); } @@ -55,15 +55,15 @@ class Join { @Test void 실패_이미_가입된ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); // act - verify(userJpaRepository, times(1)).save(userModel); + verify(userJpaRepository, times(1)).save(user); assertThatThrownBy(() -> { - userService.join(userModel); + userService.join(user); }).isInstanceOf(CoreException.class).hasMessageContaining("이미 가입된 ID 입니다."); - verify(userJpaRepository, times(1)).save(userModel); + verify(userJpaRepository, times(1)).save(user); } } @@ -74,19 +74,19 @@ class Get { @Test void 성공_존재하는_유저ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); // act - UserModel result = userService.getUser(userModel.getUserId()); + User result = userService.getUser(user.getUserId()); // assert assertAll( () -> assertThat(result).isNotNull(), - () -> assertThat(result.getUserId()).isEqualTo(userModel.getUserId()), - () -> assertThat(result.getEmail()).isEqualTo(userModel.getEmail()), - () -> assertThat(result.getBirthday()).isEqualTo(userModel.getBirthday()), - () -> assertThat(result.getGender()).isEqualTo(userModel.getGender()) + () -> assertThat(result.getUserId()).isEqualTo(user.getUserId()), + () -> assertThat(result.getEmail()).isEqualTo(user.getEmail()), + () -> assertThat(result.getBirthday()).isEqualTo(user.getBirthday()), + () -> assertThat(result.getGender()).isEqualTo(user.getGender()) ); } @@ -94,10 +94,10 @@ class Get { @Test void 실패_존재하지_않는_유저ID() { // arrange - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); // act - UserModel result = userService.getUser(userModel.getUserId()); + User result = userService.getUser(user.getUserId()); // assert assertThat(result).isNull(); diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java similarity index 72% rename from apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java rename to apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java index 159dbdde4..91eb7b75f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/user/UserModelTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/user/UserTest.java @@ -9,8 +9,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -class UserModelTest { - UserModel userModel; +class UserTest { + User user; String validMsg = ""; @DisplayName("회원가입-단위테스트1") @@ -24,28 +24,28 @@ void setup() { @Test void 실패_ID_숫자만() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_문자만() { assertThatThrownBy(() -> { - userModel = UserModel.create("hello", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("hello", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_특수문자() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345!", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345!", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_ID_10자이상오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("12345678910", "user1@test.XXX", "1999-01-01", "F"); + user = User.create("12345678910", "user1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @@ -61,14 +61,14 @@ void setup() { @Test void 실패_이메일_기호없음() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1test.XXX", "1999-01-01", "F"); + user = User.create("user1", "user1test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_이메일_한글포함() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "ㄱuser1@test.XXX", "1999-01-01", "F"); + user = User.create("user1", "ㄱuser1@test.XXX", "1999-01-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @@ -85,22 +85,22 @@ void setup() { @Test void 실패_생년월일_형식오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1@test.XXX", "19990101", "F"); + user = User.create("user1", "user1@test.XXX", "19990101", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } @Test void 실패_생년월일_날짜오류() { assertThatThrownBy(() -> { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-13-01", "F"); + user = User.create("user1", "user1@test.XXX", "1999-13-01", "F"); }).isInstanceOf(CoreException.class).hasMessageContaining(validMsg); } } @Test void 성공_User_객체생성() { - userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - assertThat(userModel).isNotNull(); - assertThat(userModel.getUserId()).isEqualTo("user1"); + user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + assertThat(user).isNotNull(); + assertThat(user.getUserId()).isEqualTo("user1"); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java index 4914412f7..d1730a77f 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/PointV1ApiE2ETest.java @@ -1,6 +1,7 @@ package com.loopers.interfaces.api; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; +import com.loopers.domain.user.UserService; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; @@ -22,17 +23,17 @@ public class PointV1ApiE2ETest { private final TestRestTemplate testRestTemplate; - private final UserJpaRepository userJpaRepository; + private final UserService userService; private final DatabaseCleanUp databaseCleanUp; @Autowired public PointV1ApiE2ETest( TestRestTemplate testRestTemplate, - UserJpaRepository userJpaRepository, + UserService userService, DatabaseCleanUp databaseCleanUp ) { this.testRestTemplate = testRestTemplate; - this.userJpaRepository = userJpaRepository; + this.userService = userService; this.databaseCleanUp = databaseCleanUp; } @@ -50,12 +51,12 @@ class Get { //given BigDecimal JOIN_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userService.join(user); //when HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", userModel.getUserId()); + headers.set("X-USER-ID", savedUser.getId().toString()); String url = "/api/v1/user/point"; ParameterizedTypeReference> resType = new ParameterizedTypeReference<>() { @@ -93,11 +94,11 @@ class Charge { //given BigDecimal CHARGE_POINT = BigDecimal.TEN; - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userJpaRepository.save(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + User savedUser = userService.join(user); HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", userModel.getUserId()); + headers.set("X-USER-ID", savedUser.getId().toString()); //when String url = "/api/v1/user/point/charge"; @@ -116,7 +117,7 @@ class Charge { void 실패_ID없음_400() { //given HttpHeaders headers = new HttpHeaders(); - headers.set("X-USER-ID", "user1"); + headers.set("X-USER-ID", "2"); //when String url = "/api/v1/user/point/charge"; diff --git a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java index 6c5952457..21fcb759e 100644 --- a/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java +++ b/apps/commerce-api/src/test/java/com/loopers/interfaces/api/UserV1ApiE2ETest.java @@ -1,9 +1,8 @@ package com.loopers.interfaces.api; -import com.loopers.domain.user.UserModel; +import com.loopers.domain.user.User; import com.loopers.domain.user.UserService; import com.loopers.interfaces.api.user.UserCreateV1Dto; -import com.loopers.support.error.CoreException; import com.loopers.utils.DatabaseCleanUp; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; @@ -93,11 +92,11 @@ class Get { @Test void 성공_정보조회() { //given - UserModel userModel = UserModel.create("user1", "user1@test.XXX", "1999-01-01", "F"); - userService.join(userModel); + User user = User.create("user1", "user1@test.XXX", "1999-01-01", "F"); + userService.join(user); //when - String url = ENDPOINT_GET.apply(userModel.getUserId()); + String url = ENDPOINT_GET.apply(user.getUserId()); ParameterizedTypeReference> resType = new ParameterizedTypeReference<>() { }; ResponseEntity> res = testRestTemplate.exchange(url, HttpMethod.GET, new HttpEntity<>(null), resType); @@ -105,7 +104,7 @@ class Get { //then assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(res.getBody().data().userId()).isNotNull(); - assertThat(res.getBody().data().userId()).isEqualTo(userModel.getUserId()); + assertThat(res.getBody().data().userId()).isEqualTo(user.getUserId()); } @DisplayName("E2E테스트2") From 9bbe6841f2dd44a4d17da9d33439128ab87d7cb7 Mon Sep 17 00:00:00 2001 From: sieun0322 Date: Fri, 14 Nov 2025 14:16:17 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20OrderFacade=20=EC=A3=BC=EB=AC=B8?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loopers/application/order/OrderFacade.java | 10 ++++++---- .../application/product/ProductFacade.java | 3 +++ .../domain/order/CreateOrderService.java | 1 + .../java/com/loopers/domain/point/Point.java | 2 +- .../loopers/domain/product/ProductService.java | 3 +++ .../domain/product/ProductStockService.java | 1 + .../loopers/domain/user/UserPointService.java | 5 +++-- .../order/OrderFacadeIntegrationTest.java | 18 +++++++++--------- .../product/ProductFacadeIntegrationTest.java | 8 ++++---- .../com/loopers/domain/point/PointTest.java | 2 +- 10 files changed, 32 insertions(+), 21 deletions(-) diff --git a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java index 63fcc98bc..b6e1cd950 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java @@ -10,10 +10,10 @@ import com.loopers.domain.user.User; import com.loopers.domain.user.UserPointService; import com.loopers.domain.user.UserService; -import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; @@ -30,13 +30,15 @@ public class OrderFacade { private final ProductStockService productStockService; private final UserPointService userPointService; + @Transactional(readOnly = true) public Page getOrderList(Long userId, String sortType, int page, int size) { return orderService.getOrders(userId, sortType, page, size); } - + + @Transactional(readOnly = true) public OrderInfo getOrderDetail(Long orderId) { Order order = orderService.getOrder(orderId); return OrderInfo.from(order); @@ -48,11 +50,11 @@ public OrderInfo createOrder(CreateOrderCommand command) { Map quantityMap = command.orderItemInfo(); User user = userService.getActiveUser(command.userId()); List productList = productService.getExistingProducts(quantityMap.keySet()); - Order savedOrder = null; + productStockService.deduct(productList, quantityMap); userPointService.use(user, productList, quantityMap); - savedOrder = createOrderService.save(user, productList, quantityMap); + Order savedOrder = createOrderService.save(user, productList, quantityMap); return OrderInfo.from(savedOrder); } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java index b19585419..2a9a97eff 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Component @@ -13,6 +14,7 @@ public class ProductFacade { private final ProductService productService; private final LikeService likeService; + @Transactional(readOnly = true) public Page getProductList(Long brandId, String sortType, int page, @@ -20,6 +22,7 @@ public Page getProductList(Long brandId, return productService.getProducts(brandId, sortType, page, size); } + @Transactional(readOnly = true) public ProductDetailInfo getProductDetail(long userId, long productId) { Product product = productService.getProduct(productId); boolean isLiked = likeService.isLiked(userId, productId); diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java index 78cd49740..4780d71b3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/order/CreateOrderService.java @@ -6,6 +6,7 @@ import com.loopers.domain.user.User; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.ArrayList; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java index 0264af717..84ecc6696 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/point/Point.java @@ -54,7 +54,7 @@ public void use(BigDecimal amountToChange) { throw new CoreException(ErrorType.BAD_REQUEST, "차감 금액은 0보다 커야 합니다."); } if (this.amount.compareTo(amountToChange) < 0) { - throw new CoreException(ErrorType.INSUFFICIENT_POINT, "포인트이가 부족합니다."); + throw new CoreException(ErrorType.INSUFFICIENT_POINT, "포인트가 부족합니다."); } this.amount = this.amount.subtract(amountToChange); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java index ef8fb2cd7..15a98b75c 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java @@ -22,6 +22,7 @@ public class ProductService { private final ProductRepository productRepository; private final LikeRepository likeRepository; + @Transactional(readOnly = true) public Page getProducts( Long brandId, String sortType, @@ -71,10 +72,12 @@ public List getExistingProducts(Set productIds) { return products; } + @Transactional public Product save(Product product) { return productRepository.save(product); } + @Transactional public List save(List product) { return productRepository.save(product); } diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java index 075b25d6e..800226bbc 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/product/ProductStockService.java @@ -4,6 +4,7 @@ import com.loopers.domain.order.OrderPreparer; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Map; diff --git a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java index 467bbcd63..c29759ccf 100644 --- a/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java +++ b/apps/commerce-api/src/main/java/com/loopers/domain/user/UserPointService.java @@ -2,6 +2,8 @@ package com.loopers.domain.user; import com.loopers.domain.order.OrderPreparer; +import com.loopers.domain.point.Point; +import com.loopers.domain.point.PointRepository; import com.loopers.domain.point.PointService; import com.loopers.domain.product.Product; import lombok.RequiredArgsConstructor; @@ -18,9 +20,8 @@ public class UserPointService { private final PointService pointService; - @Transactional - public void use(User user, List productList, Map quantityMap) { + public void use(User user, List productList, Map quantityMap) { BigDecimal totalAmt = OrderPreparer.getTotalAmt(user.getPoint(), productList, quantityMap); pointService.use(user, totalAmt); } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java index cf9123d78..a2121e2ef 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/order/OrderFacadeIntegrationTest.java @@ -9,7 +9,6 @@ import com.loopers.domain.user.User; import com.loopers.infrastructure.brand.BrandJpaRepository; import com.loopers.infrastructure.order.OrderJpaRepository; -import com.loopers.infrastructure.product.ProductJpaRepository; import com.loopers.infrastructure.user.UserJpaRepository; import com.loopers.interfaces.api.order.OrderCreateV1Dto; import com.loopers.support.error.CoreException; @@ -20,11 +19,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; -import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +29,6 @@ @SpringBootTest -@Transactional class OrderFacadeIntegrationTest { @Autowired private OrderFacade orderFacade; @@ -67,7 +63,7 @@ void setup() { ); savedProducts = productService.save(productList); List orderItems = new ArrayList<>(); - orderItems.add(OrderItem.create(user.getId(), 2L, new BigDecimal(5_000))); + orderItems.add(OrderItem.create(productList.get(0).getId(), 2L, new BigDecimal(5_000))); Order order = Order.create(1, OrderStatus.PENDING, new BigDecimal(10_000), orderItems); savedOrder = orderJpaRepository.save(order); @@ -104,8 +100,8 @@ class Get { // assert assertThat(result.id()).isEqualTo(savedOrder.getId()); - assertThat(result.status().toString()).isEqualTo(savedOrder.getStatus().toString()); - assertThat(result.totalPrice()).isEqualTo(savedOrder.getTotalPrice()); + assertThat(result.status()).isEqualTo(savedOrder.getStatus().toString()); + assertThat(result.totalPrice()).isEqualByComparingTo(savedOrder.getTotalPrice()); } @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") @@ -134,7 +130,7 @@ class Post { OrderInfo savedOrder = orderFacade.createOrder(orderCommand); // assert assertThat(savedOrder).isNotNull(); - assertThat(savedOrder.totalPrice()).isEqualTo(savedProducts.get(0).getPrice()); + assertThat(savedOrder.totalPrice()).isEqualByComparingTo(savedProducts.get(0).getPrice()); assertThat(savedOrder.orderItemInfo()).hasSize(1); } @@ -161,7 +157,11 @@ class Post { CreateOrderCommand orderCommand = CreateOrderCommand.from(savedUser.getId(), request); // act // assert - assertThrows(CoreException.class, () -> orderFacade.createOrder(orderCommand)).getErrorType().equals(ErrorType.INSUFFICIENT_POINT); + CoreException actualException = assertThrows(CoreException.class, + () -> orderFacade.createOrder(orderCommand)); + assertThat(actualException.getErrorType()).isEqualTo(ErrorType.INSUFFICIENT_POINT); + Product deductedProduct = productService.getProduct(productId); + assertThat(deductedProduct.getStock()).isEqualTo(10); } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java index d958c1b6d..f31a3d8f2 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/product/ProductFacadeIntegrationTest.java @@ -2,6 +2,7 @@ import com.loopers.domain.brand.Brand; import com.loopers.domain.product.Product; +import com.loopers.domain.product.ProductService; import com.loopers.domain.user.User; import com.loopers.infrastructure.brand.BrandJpaRepository; import com.loopers.infrastructure.product.ProductJpaRepository; @@ -25,7 +26,6 @@ @SpringBootTest -@Transactional class ProductFacadeIntegrationTest { @Autowired private ProductFacade productFacade; @@ -34,7 +34,7 @@ class ProductFacadeIntegrationTest { @MockitoSpyBean private BrandJpaRepository brandJpaRepository; @MockitoSpyBean - private ProductJpaRepository productJpaRepository; + private ProductService productService; @Autowired private DatabaseCleanUp databaseCleanUp; @@ -55,7 +55,7 @@ void setup() { , Product.create(savedBrandList.get(0), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) , Product.create(savedBrandList.get(1), "Flower Pattern Fleece Jacket", new BigDecimal(178_000), 20) ); - savedProducts = productList.stream().map((product) -> productJpaRepository.save(product)).toList(); + savedProducts = productService.save(productList); } @@ -104,7 +104,7 @@ class Get { ProductDetailInfo result = productFacade.getProductDetail(savedUser.getId(), savedProducts.get(0).getId()); // assert - assertThat(result.brandInfo().name()).isEqualTo(savedProducts.get(0).getBrand().getName()); + assertThat(result.name()).isEqualTo(savedProducts.get(0).getName()); } @DisplayName("존재하지 않는 상품 ID를 주면, 예외가 반환된다.") diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java index 345e7f0e6..29d6b647a 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/point/PointTest.java @@ -60,7 +60,7 @@ class Use { // act, assert assertThatThrownBy(() -> { point.use(BigDecimal.TEN); - }).isInstanceOf(CoreException.class).hasMessageContaining("잔액이 부족합니다."); + }).isInstanceOf(CoreException.class).hasMessageContaining("포인트가 부족합니다."); } @Test