Skip to content

Commit 1087ea2

Browse files
committed
feat(order, point): 주문 생성 유스케이스 및 포인트 차감 도메인 구현
- Order / OrderItem 엔티티 구현 - 상품 재고 차감, 포인트 차감 도메인 로직 추가 - Order 도메인 서비스 + Facade 조합 구현 - OrderRepository, PointRepository 인터페이스 정의 - 정상 주문/재고부족 포인트/부족 단위 / 통합 테스트 작성
1 parent 7e0ac82 commit 1087ea2

21 files changed

Lines changed: 969 additions & 24 deletions
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.loopers.application.order;
2+
3+
import java.util.List;
4+
5+
/**
6+
* packageName : com.loopers.application.order
7+
* fileName : CreateOrderCommand
8+
* author : byeonsungmun
9+
* date : 2025. 11. 14.
10+
* description :
11+
* ===========================================
12+
* DATE AUTHOR NOTE
13+
* -------------------------------------------
14+
* 2025. 11. 14. byeonsungmun 최초 생성
15+
*/
16+
public record CreateOrderCommand(
17+
String userId,
18+
List<OrderItemCommand> items
19+
) {}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.loopers.application.order;
2+
3+
import com.loopers.domain.order.Order;
4+
import com.loopers.domain.order.OrderItem;
5+
import com.loopers.domain.order.OrderService;
6+
import com.loopers.domain.order.OrderStatus;
7+
import com.loopers.domain.point.Point;
8+
import com.loopers.domain.point.PointService;
9+
import com.loopers.domain.product.Product;
10+
import com.loopers.domain.product.ProductService;
11+
import com.loopers.support.error.CoreException;
12+
import com.loopers.support.error.ErrorType;
13+
import lombok.RequiredArgsConstructor;
14+
import lombok.extern.slf4j.Slf4j;
15+
import org.springframework.stereotype.Component;
16+
import org.springframework.transaction.annotation.Transactional;
17+
18+
/**
19+
* packageName : com.loopers.application.order
20+
* fileName : OrderFacade
21+
* author : byeonsungmun
22+
* date : 2025. 11. 13.
23+
* description :
24+
* ===========================================
25+
* DATE AUTHOR NOTE
26+
* -------------------------------------------
27+
* 2025. 11. 13. byeonsungmun 최초 생성
28+
*/
29+
30+
@Slf4j
31+
@Component
32+
@RequiredArgsConstructor
33+
public class OrderFacade {
34+
35+
private final OrderService orderService;
36+
private final ProductService productService;
37+
private final PointService pointService;
38+
39+
@Transactional
40+
public OrderInfo createOrder(CreateOrderCommand command) {
41+
42+
if (command == null || command.items() == null || command.items().isEmpty()) {
43+
throw new CoreException(ErrorType.NOT_FOUND, "상품 정보가 비어있습니다");
44+
}
45+
46+
Order order = Order.create(command.userId());
47+
48+
for (OrderItemCommand itemCommand : command.items()) {
49+
50+
//상품가져오고
51+
Product product = productService.getProduct(itemCommand.productId());
52+
53+
// 재고감소
54+
product.decreaseStock(itemCommand.quantity());
55+
56+
// OrderItem생성
57+
OrderItem orderItem = OrderItem.create(
58+
product.getId(),
59+
product.getName(),
60+
itemCommand.quantity(),
61+
product.getPrice());
62+
63+
order.addOrderItem(orderItem);
64+
orderItem.setOrder(order);
65+
}
66+
67+
//총 가격구하고
68+
long totalAmount = order.getOrderItems().stream()
69+
.mapToLong(OrderItem::getAmount)
70+
.sum();
71+
72+
order.updateTotalAmount(totalAmount);
73+
74+
Point point = pointService.findPointByUserId(command.userId());
75+
point.use(totalAmount);
76+
77+
//저장
78+
Order saved = orderService.createOrder(order);
79+
saved.updateStatus(OrderStatus.COMPLETE);
80+
81+
return OrderInfo.from(saved);
82+
}
83+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.loopers.application.order;
2+
3+
import com.loopers.domain.order.Order;
4+
import com.loopers.domain.order.OrderStatus;
5+
6+
import java.time.LocalDateTime;
7+
import java.util.List;
8+
9+
/**
10+
* packageName : com.loopers.application.order
11+
* fileName : OrderInfo
12+
* author : byeonsungmun
13+
* date : 2025. 11. 14.
14+
* description :
15+
* ===========================================
16+
* DATE AUTHOR NOTE
17+
* -------------------------------------------
18+
* 2025. 11. 14. byeonsungmun 최초 생성
19+
*/
20+
public record OrderInfo(
21+
Long orderId,
22+
String userId,
23+
Long totalAmount,
24+
OrderStatus status,
25+
LocalDateTime createdAt,
26+
List<OrderItemInfo> items
27+
) {
28+
public static OrderInfo from(Order order) {
29+
List<OrderItemInfo> itemInfos = order.getOrderItems().stream()
30+
.map(OrderItemInfo::from)
31+
.toList();
32+
33+
return new OrderInfo(
34+
order.getId(),
35+
order.getUserId(),
36+
order.getTotalAmount(),
37+
order.getStatus(),
38+
order.getCreatedAt(),
39+
itemInfos
40+
);
41+
}
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.loopers.application.order;
2+
3+
/**
4+
* packageName : com.loopers.application.order
5+
* fileName : OrderItemCommand
6+
* author : byeonsungmun
7+
* date : 2025. 11. 14.
8+
* description :
9+
* ===========================================
10+
* DATE AUTHOR NOTE
11+
* -------------------------------------------
12+
* 2025. 11. 14. byeonsungmun 최초 생성
13+
*/
14+
public record OrderItemCommand(
15+
Long productId,
16+
Long quantity
17+
) {}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.loopers.application.order;
2+
3+
import com.loopers.domain.order.OrderItem;
4+
5+
/**
6+
* packageName : com.loopers.application.order
7+
* fileName : OrderInfo
8+
* author : byeonsungmun
9+
* date : 2025. 11. 13.
10+
* description :
11+
* ===========================================
12+
* DATE AUTHOR NOTE
13+
* -------------------------------------------
14+
* 2025. 11. 13. byeonsungmun 최초 생성
15+
*/
16+
public record OrderItemInfo(
17+
Long productId,
18+
String productName,
19+
Long quantity,
20+
Long price,
21+
Long amount
22+
) {
23+
public static OrderItemInfo from(OrderItem item) {
24+
return new OrderItemInfo(
25+
item.getProductId(),
26+
item.getProductName(),
27+
item.getQuantity(),
28+
item.getPrice(),
29+
item.getAmount()
30+
);
31+
}
32+
}

apps/commerce-api/src/main/java/com/loopers/application/point/PointInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public record PointInfo(String userId, Long amount) {
66
public static PointInfo from(Point info) {
77
return new PointInfo(
88
info.getUserId(),
9-
info.getAmount()
9+
info.getBalance()
1010
);
1111
}
1212

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.loopers.domain.order;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import jakarta.persistence.*;
6+
import lombok.Getter;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
12+
/**
13+
* packageName : com.loopers.domain.order
14+
* fileName : Order
15+
* author : byeonsungmun
16+
* date : 2025. 11. 11.
17+
* description :
18+
* ===========================================
19+
* DATE AUTHOR NOTE
20+
* -------------------------------------------
21+
* 2025. 11. 11. byeonsungmun 최초 생성
22+
*/
23+
@Entity
24+
@Table(name = "orders")
25+
@Getter
26+
public class Order {
27+
28+
@Id
29+
@GeneratedValue(strategy = GenerationType.IDENTITY)
30+
private Long id;
31+
32+
@Column(name = "ref_user_id", nullable = false)
33+
private String userId;
34+
35+
@Column(nullable = false)
36+
private Long totalAmount;
37+
38+
@Enumerated(EnumType.STRING)
39+
private OrderStatus status;
40+
41+
@Column(nullable = false)
42+
private LocalDateTime createdAt;
43+
44+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
45+
private List<OrderItem> orderItems = new ArrayList<>();
46+
47+
protected Order() {}
48+
49+
private Order(String userId, OrderStatus status) {
50+
this.userId = requiredValidUserId(userId);
51+
this.totalAmount = 0L;
52+
this.status = requiredValidStatus(status);
53+
this.createdAt = LocalDateTime.now();
54+
}
55+
56+
public static Order create(String userId) {
57+
return new Order(userId, OrderStatus.PENDING);
58+
}
59+
60+
public void addOrderItem(OrderItem orderItem) {
61+
this.orderItems.add(orderItem);
62+
}
63+
64+
private OrderStatus requiredValidStatus(OrderStatus status) {
65+
if (status == null) {
66+
throw new CoreException(ErrorType.BAD_REQUEST, "주문 상태는 필수 입니다.");
67+
}
68+
return status;
69+
}
70+
71+
private String requiredValidUserId(String userId) {
72+
if (userId == null || userId.isEmpty()) {
73+
throw new CoreException(ErrorType.BAD_REQUEST, "사용자 ID는 필수 입니다.");
74+
}
75+
return userId;
76+
}
77+
78+
public void updateTotalAmount(long totalAmount) {
79+
this.totalAmount = totalAmount;
80+
}
81+
82+
public void updateStatus(OrderStatus status) {
83+
this.status = status;
84+
}
85+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.loopers.domain.order;
2+
3+
import com.loopers.support.error.CoreException;
4+
import com.loopers.support.error.ErrorType;
5+
import jakarta.persistence.*;
6+
import lombok.Getter;
7+
import lombok.Setter;
8+
9+
/**
10+
* packageName : com.loopers.domain.order
11+
* fileName : OrderItem
12+
* author : byeonsungmun
13+
* date : 2025. 11. 11.
14+
* description :
15+
* ===========================================
16+
* DATE AUTHOR NOTE
17+
* -------------------------------------------
18+
* 2025. 11. 11. byeonsungmun 최초 생성
19+
*/
20+
21+
@Entity
22+
@Table(name = "order_item")
23+
@Getter
24+
public class OrderItem {
25+
26+
@Id
27+
@GeneratedValue(strategy = GenerationType.IDENTITY)
28+
private Long id;
29+
30+
@Setter
31+
@ManyToOne(fetch = FetchType.LAZY)
32+
@JoinColumn(name = "order_id", nullable = false)
33+
private Order order;
34+
35+
@Column(name = "ref_product_id", nullable = false)
36+
private Long productId;
37+
38+
@Column(name = "ref_product_name", nullable = false)
39+
private String productName;
40+
41+
@Column(nullable = false)
42+
private Long quantity;
43+
44+
@Column(nullable = false)
45+
private Long price;
46+
47+
protected OrderItem() {}
48+
49+
private OrderItem(Long productId, String productName, Long quantity, Long price) {
50+
this.productId = requiredValidProductId(productId);
51+
this.productName = requiredValidProductName(productName);
52+
this.quantity = requiredQuantity(quantity);
53+
this.price = requiredPrice(price);
54+
}
55+
56+
public static OrderItem create(Long productId, String productName, Long quantity, Long price) {
57+
return new OrderItem(productId, productName, quantity, price);
58+
}
59+
60+
public Long getAmount() {
61+
return quantity * price;
62+
}
63+
64+
private Long requiredValidProductId(Long productId) {
65+
if (productId == null || productId <= 0) {
66+
throw new CoreException(ErrorType.BAD_REQUEST, "상품 ID는 필수입니다.");
67+
}
68+
return productId;
69+
}
70+
71+
private String requiredValidProductName(String productName) {
72+
if (productName == null || productName.isEmpty()) {
73+
throw new CoreException(ErrorType.BAD_REQUEST, "상품명은 필수입니다.");
74+
}
75+
return productName;
76+
}
77+
78+
private Long requiredQuantity(Long quantity) {
79+
if (quantity == null || quantity <= 0) {
80+
throw new CoreException(ErrorType.BAD_REQUEST, "수량은 1개 이상이어야 합니다.");
81+
}
82+
return quantity;
83+
}
84+
85+
private Long requiredPrice(Long price) {
86+
if (price == null || price < 0) {
87+
throw new CoreException(ErrorType.BAD_REQUEST, "가격은 0원 이상이어야 합니다.");
88+
}
89+
return price;
90+
}
91+
}

0 commit comments

Comments
 (0)