33import com .loopers .domain .coupon .Coupon ;
44import com .loopers .domain .coupon .CouponService ;
55import com .loopers .domain .order .Order ;
6+ import com .loopers .domain .order .OrderCreatedEvent ;
67import com .loopers .domain .order .OrderService ;
8+ import com .loopers .domain .order .OrderStatus ;
79import com .loopers .domain .product .Product ;
810import com .loopers .domain .product .ProductService ;
911import com .loopers .domain .user .User ;
1214import com .loopers .support .error .ErrorType ;
1315import lombok .RequiredArgsConstructor ;
1416import lombok .extern .slf4j .Slf4j ;
17+ import org .springframework .context .ApplicationEventPublisher ;
1518import org .springframework .stereotype .Component ;
1619import org .springframework .transaction .annotation .Transactional ;
1720
@@ -29,47 +32,83 @@ public class OrderFacade {
2932 private final ProductService productService ;
3033 private final CouponService couponService ;
3134 private final OrderService orderService ;
35+ private final ApplicationEventPublisher eventPublisher ;
3236
37+ /**
38+ * 주문 생성
39+ * - 핵심 트랜잭션: 재고 차감, 주문 저장
40+ * - 후속 처리(쿠폰 사용, 데이터 플랫폼 전송): 이벤트로 분리
41+ */
3342 @ Transactional
3443 public OrderInfo createOrder (OrderPlaceCommand command ) {
35- log .info ("주문 생성 시작: userBusinessId={}, items={}" ,
36- command .userId (), command .items ().size ());
37-
3844 // 1. 사용자 조회
39- User user = userService .getUserByUserId (command .userId ());
45+ User user = userService .getUserByLoginId (command .loginId ());
4046
41- // 2. 상품 조회 (데드락 방지를 위한 정렬 + 비관적 락 )
47+ // 2. 상품 조회 (데드락 방지를 위해 ID 정렬 후 조회 )
4248 List <Long > productIds = extractAndSortProductIds (command .items ());
4349 List <Product > products = productService .getProductsByIdsWithPessimisticLock (productIds );
4450 Map <Long , Product > productMap = toProductMap (products );
4551
46- // 3. 재고 검증 및 차감 (도메인 로직은 Product가 처리)
52+ // 3. 재고 검증 및 차감
4753 validateAndDecreaseStock (command .items (), productMap );
4854
49- // 4. 주문 생성 (도메인 서비스 위임)
55+ // 4. 쿠폰 검증 (사용은 이벤트로 분리)
56+ Long discountAmount = 0L ;
57+ if (command .couponId () != null ) {
58+ Coupon coupon = couponService .getCouponWithOptimisticLock (command .couponId ());
59+ couponService .validateCouponUsable (coupon , user );
60+ discountAmount = coupon .calculateDiscount (calculateTotalAmount (command .items (), productMap ));
61+ }
62+
63+ // 5. 주문 생성
5064 List <OrderService .OrderItemRequest > itemRequests = command .items ().stream ()
5165 .map (item -> OrderService .OrderItemRequest .of (item .productId (), item .quantity ()))
5266 .toList ();
5367 Order order = orderService .createOrderWithItems (user , itemRequests , productMap );
5468
55- // 5. 쿠폰 적용 (선택)
56- Long discountAmount = applyCouponIfExists (command .couponId (), user , order );
69+ // 6. 쿠폰 ID 저장 (실제 사용은 이벤트로)
70+ if (command .couponId () != null ) {
71+ order .applyCoupon (command .couponId ());
72+ }
5773
58- // 6 . 주문 저장
74+ // 7 . 주문 저장
5975 Order savedOrder = orderService .save (order );
6076
61- log .info ("주문 생성 완료: orderId={}, totalAmount={}, discountAmount={}" ,
62- savedOrder .getId (), savedOrder .getTotalAmountValue (), discountAmount );
77+ // 8. 이벤트 발행 → 쿠폰 사용, 데이터 플랫폼 전송, 유저 행동 로깅
78+ eventPublisher .publishEvent (OrderCreatedEvent .from (savedOrder , discountAmount ));
79+
80+ log .info ("주문 생성 완료: orderId={}, userId={}, couponId={}" ,
81+ savedOrder .getId (), user .getId (), command .couponId ());
6382
6483 return OrderInfo .from (savedOrder , discountAmount );
6584 }
6685
86+ /**
87+ * 쿠폰 사용 처리 - OrderEventListener에서 호출
88+ */
89+ @ Transactional
90+ public void useCoupon (Long couponId ) {
91+ log .info ("쿠폰 사용 처리: couponId={}" , couponId );
92+ Coupon coupon = couponService .getCouponWithOptimisticLock (couponId );
93+ coupon .use ();
94+ couponService .save (coupon );
95+ }
96+
97+ /**
98+ * 주문 취소 - PaymentEventListener에서 호출 (보상 트랜잭션)
99+ */
67100 @ Transactional
68101 public void cancelOrder (Long orderId , Long couponId ) {
69102 log .info ("주문 취소 시작: orderId={}" , orderId );
70103
71104 Order order = orderService .getOrderById (orderId );
72105
106+ // 이미 취소된 주문은 스킵 (멱등성)
107+ if (order .getStatus () == OrderStatus .CANCELLED ) {
108+ log .info ("이미 취소된 주문입니다: orderId={}" , orderId );
109+ return ;
110+ }
111+
73112 // 1. 재고 복구
74113 List <Long > productIds = order .getOrderItems ().stream ()
75114 .map (item -> item .getProductId ())
@@ -93,20 +132,39 @@ public void cancelOrder(Long orderId, Long couponId) {
93132 log .info ("주문 취소 완료: orderId={}" , orderId );
94133 }
95134
135+ /**
136+ * 주문 완료 처리 - PaymentEventListener에서 호출
137+ */
96138 @ Transactional
97139 public void completeOrder (Long orderId ) {
98140 log .info ("주문 완료 처리: orderId={}" , orderId );
99141
100142 Order order = orderService .getOrderById (orderId );
101- order .completePayment ();
143+
144+ // 이미 완료된 주문은 스킵 (멱등성)
145+ if (order .getStatus () == OrderStatus .COMPLETED ) {
146+ log .info ("이미 완료된 주문입니다: orderId={}" , orderId );
147+ return ;
148+ }
149+
150+ order .markAsCompleted ();
102151 orderService .save (order );
103152
104153 log .info ("주문 완료: orderId={}" , orderId );
105154 }
106155
156+ /**
157+ * 주문에서 사용자 ID 조회 (데이터 플랫폼 전송용)
158+ */
107159 @ Transactional (readOnly = true )
108- public List <OrderInfo > getMyOrders (String userId ) {
109- User user = userService .getUserByUserId (userId );
160+ public Long getUserIdByOrderId (Long orderId ) {
161+ Order order = orderService .getOrderById (orderId );
162+ return order .getUser ().getId ();
163+ }
164+
165+ @ Transactional (readOnly = true )
166+ public List <OrderInfo > getMyOrders (String loginId ) {
167+ User user = userService .getUserByLoginId (loginId );
110168 List <Order > orders = orderService .getOrdersByUser (user );
111169
112170 return orders .stream ()
@@ -115,13 +173,22 @@ public List<OrderInfo> getMyOrders(String userId) {
115173 }
116174
117175 @ Transactional (readOnly = true )
118- public OrderInfo getOrderDetail (Long orderId , String userId ) {
119- User user = userService .getUserByUserId ( userId );
176+ public OrderInfo getOrderDetail (Long orderId , String loginId ) {
177+ User user = userService .getUserByLoginId ( loginId );
120178 Order order = orderService .getOrderByIdAndUser (orderId , user );
121179
122180 return OrderInfo .from (order , 0L );
123181 }
124182
183+ private Long calculateTotalAmount (List <OrderPlaceCommand .OrderItemCommand > items , Map <Long , Product > productMap ) {
184+ return items .stream ()
185+ .mapToLong (item -> {
186+ Product product = productMap .get (item .productId ());
187+ return product .getPrice ().getValue () * item .quantity ();
188+ })
189+ .sum ();
190+ }
191+
125192 private void validateAndDecreaseStock (
126193 List <OrderPlaceCommand .OrderItemCommand > items ,
127194 Map <Long , Product > productMap
@@ -143,53 +210,20 @@ private void validateAndDecreaseStock(
143210 }
144211 }
145212
146- /**
147- * 쿠폰 적용
148- */
149- private Long applyCouponIfExists (Long couponId , User user , Order order ) {
150- if (couponId == null ) {
151- return 0L ;
152- }
153-
154- Coupon coupon = couponService .getCouponWithOptimisticLock (couponId );
155- couponService .validateCouponUsable (coupon , user );
156-
157- Long discountAmount = coupon .calculateDiscount (order .getTotalAmountValue ());
158- coupon .use ();
159- couponService .save (coupon );
160-
161- order .applyCoupon (couponId );
162-
163- log .info ("쿠폰 적용 완료: couponId={}, discountAmount={}" , couponId , discountAmount );
164-
165- return discountAmount ;
166- }
167-
168- /**
169- * 쿠폰 복구
170- */
171213 private void restoreCoupon (Long couponId ) {
172214 Coupon coupon = couponService .getCouponWithOptimisticLock (couponId );
173215 coupon .restore ();
174216 couponService .save (coupon );
175217 log .debug ("쿠폰 복구: couponId={}" , couponId );
176218 }
177219
178- /**
179- * 상품 ID 추출 및 정렬
180- */
181- private List <Long > extractAndSortProductIds (
182- List <OrderPlaceCommand .OrderItemCommand > items
183- ) {
220+ private List <Long > extractAndSortProductIds (List <OrderPlaceCommand .OrderItemCommand > items ) {
184221 return items .stream ()
185222 .map (OrderPlaceCommand .OrderItemCommand ::productId )
186223 .sorted ()
187224 .toList ();
188225 }
189226
190- /**
191- * 상품 리스트를 Map으로 변환
192- */
193227 private Map <Long , Product > toProductMap (List <Product > products ) {
194228 return products .stream ()
195229 .collect (Collectors .toMap (Product ::getId , Function .identity ()));
0 commit comments