diff --git a/apps/commerce-api/src/main/java/com/loopers/application/coupon/ApplyCouponCommand.java b/apps/commerce-api/src/main/java/com/loopers/application/coupon/ApplyCouponCommand.java new file mode 100644 index 000000000..9ebb38172 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/application/coupon/ApplyCouponCommand.java @@ -0,0 +1,30 @@ +package com.loopers.application.coupon; + +/** + * 쿠폰 적용 명령. + *
+ * 쿠폰 적용을 위한 명령 객체입니다. + *
+ * + * @param userId 사용자 ID + * @param couponCode 쿠폰 코드 + * @param subtotal 주문 소계 금액 + */ +public record ApplyCouponCommand( + Long userId, + String couponCode, + Integer subtotal +) { + public ApplyCouponCommand { + if (userId == null) { + throw new IllegalArgumentException("userId는 필수입니다."); + } + if (couponCode == null || couponCode.isBlank()) { + throw new IllegalArgumentException("couponCode는 필수입니다."); + } + if (subtotal == null || subtotal < 0) { + throw new IllegalArgumentException("subtotal은 0 이상이어야 합니다."); + } + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponEventHandler.java index 28a720e2f..92f6b8ba3 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponEventHandler.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponEventHandler.java @@ -3,8 +3,10 @@ import com.loopers.domain.coupon.CouponEvent; import com.loopers.domain.coupon.CouponEventPublisher; import com.loopers.domain.order.OrderEvent; +import com.loopers.support.error.CoreException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -50,24 +52,65 @@ public void handleOrderCreated(OrderEvent.OrderCreated event) { return; } - // 쿠폰 사용 처리 (쿠폰 사용 마킹 및 할인 금액 계산) - Integer discountAmount = couponService.applyCoupon( - event.userId(), - event.couponCode(), - event.subtotal() - ); + try { + // ✅ OrderEvent.OrderCreated를 구독하여 쿠폰 적용 Command 실행 + // 쿠폰 사용 처리 (쿠폰 사용 마킹 및 할인 금액 계산) + Integer discountAmount = couponService.applyCoupon( + new ApplyCouponCommand( + event.userId(), + event.couponCode(), + event.subtotal() + ) + ); - // ✅ 도메인 이벤트 발행: 쿠폰이 적용되었음 (과거 사실) - // 주문 도메인이 이 이벤트를 구독하여 자신의 상태를 업데이트함 - couponEventPublisher.publish(CouponEvent.CouponApplied.of( - event.orderId(), - event.userId(), - event.couponCode(), - discountAmount - )); + // ✅ 도메인 이벤트 발행: 쿠폰이 적용되었음 (과거 사실) + // 주문 도메인이 이 이벤트를 구독하여 자신의 상태를 업데이트함 + couponEventPublisher.publish(CouponEvent.CouponApplied.of( + event.orderId(), + event.userId(), + event.couponCode(), + discountAmount + )); - log.info("쿠폰 사용 처리 완료. (orderId: {}, couponCode: {}, discountAmount: {})", - event.orderId(), event.couponCode(), discountAmount); + log.info("쿠폰 사용 처리 완료. (orderId: {}, couponCode: {}, discountAmount: {})", + event.orderId(), event.couponCode(), discountAmount); + } catch (CoreException e) { + // 비즈니스 예외 발생 시 실패 이벤트 발행 + String failureReason = e.getMessage() != null ? e.getMessage() : "쿠폰 적용 실패"; + log.error("쿠폰 적용 실패. (orderId: {}, couponCode: {})", + event.orderId(), event.couponCode(), e); + couponEventPublisher.publish(CouponEvent.CouponApplicationFailed.of( + event.orderId(), + event.userId(), + event.couponCode(), + failureReason + )); + throw e; + } catch (ObjectOptimisticLockingFailureException e) { + // 낙관적 락 충돌: 다른 트랜잭션이 먼저 쿠폰을 사용함 + String failureReason = "쿠폰이 이미 사용되었습니다. (동시성 충돌)"; + log.warn("쿠폰 사용 중 낙관적 락 충돌 발생. (orderId: {}, couponCode: {})", + event.orderId(), event.couponCode()); + couponEventPublisher.publish(CouponEvent.CouponApplicationFailed.of( + event.orderId(), + event.userId(), + event.couponCode(), + failureReason + )); + throw e; + } catch (Exception e) { + // 예상치 못한 오류 발생 시 실패 이벤트 발행 + String failureReason = e.getMessage() != null ? e.getMessage() : "쿠폰 적용 처리 중 오류 발생"; + log.error("쿠폰 적용 처리 중 오류 발생. (orderId: {}, couponCode: {})", + event.orderId(), event.couponCode(), e); + couponEventPublisher.publish(CouponEvent.CouponApplicationFailed.of( + event.orderId(), + event.userId(), + event.couponCode(), + failureReason + )); + throw e; + } } } diff --git a/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java index 77473ac4b..9a83d9df7 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/coupon/CouponService.java @@ -29,6 +29,27 @@ public class CouponService { private final UserCouponRepository userCouponRepository; private final CouponDiscountStrategyFactory couponDiscountStrategyFactory; + /** + * 쿠폰을 적용하여 할인 금액을 계산하고 쿠폰을 사용 처리합니다. + *+ * 동시성 제어 전략: + *
@@ -48,7 +69,7 @@ public class CouponService {
* @throws CoreException 쿠폰을 찾을 수 없거나 사용 불가능한 경우, 동시 사용으로 인한 충돌 시
*/
@Transactional
- public Integer applyCoupon(Long userId, String couponCode, Integer subtotal) {
+ private Integer applyCoupon(Long userId, String couponCode, Integer subtotal) {
// 쿠폰 존재 여부 확인
Coupon coupon = couponRepository.findByCode(couponCode)
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND,
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java
index f32386477..29de9db22 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/payment/PaymentEventHandler.java
@@ -3,6 +3,7 @@
import com.loopers.domain.payment.CardType;
import com.loopers.domain.payment.Payment;
import com.loopers.domain.payment.PaymentEvent;
+import com.loopers.domain.payment.PaymentGateway;
import com.loopers.domain.payment.PaymentRequestResult;
import com.loopers.support.error.CoreException;
import com.loopers.support.error.ErrorType;
@@ -37,6 +38,7 @@
public class PaymentEventHandler {
private final PaymentService paymentService;
+ private final PaymentGateway paymentGateway;
/**
* 결제 요청 이벤트를 처리하여 Payment를 생성하고 PG 결제를 요청합니다.
@@ -93,16 +95,20 @@ public void afterCommit() {
// Payment 생성 시점의 totalAmount를 사용
Long paidAmount = event.totalAmount() - event.usedPointAmount();
- // PG 결제 요청
- PaymentRequestResult result = paymentService.requestPayment(
- event.orderId(),
- event.userId(),
- event.userEntityId(),
- event.cardType(),
- event.cardNo(),
- paidAmount
+ // ✅ PaymentEvent.PaymentRequested를 구독하여 결제 요청 Command 실행
+ String callbackUrl = generateCallbackUrl(event.orderId());
+ PaymentRequestCommand command = new PaymentRequestCommand(
+ event.userId(),
+ event.orderId(),
+ event.cardType(),
+ event.cardNo(),
+ paidAmount,
+ callbackUrl
);
+ // PG 결제 요청
+ PaymentRequestResult result = paymentGateway.requestPayment(command);
+
if (result instanceof PaymentRequestResult.Success success) {
// 결제 성공: PaymentService.toSuccess가 PaymentCompleted 이벤트를 발행하고,
// OrderEventHandler가 이를 받아 주문 상태를 COMPLETED로 변경
@@ -156,5 +162,15 @@ private CardType convertCardType(String cardType) {
String.format("잘못된 카드 타입입니다. (cardType: %s)", cardType));
}
}
+
+ /**
+ * 콜백 URL을 생성합니다.
+ *
+ * @param orderId 주문 ID
+ * @return 콜백 URL
+ */
+ private String generateCallbackUrl(Long orderId) {
+ return String.format("/api/v1/payments/callback?orderId=%d", orderId);
+ }
}
diff --git a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
index df2917eda..4955a1fef 100644
--- a/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
+++ b/apps/commerce-api/src/main/java/com/loopers/application/purchasing/PurchasingFacade.java
@@ -5,8 +5,6 @@
import com.loopers.application.order.OrderService;
import com.loopers.domain.product.Product;
import com.loopers.application.product.ProductService;
-import com.loopers.domain.user.PointEvent;
-import com.loopers.domain.user.PointEventPublisher;
import com.loopers.domain.user.User;
import com.loopers.application.user.UserService;
import com.loopers.infrastructure.payment.PaymentGatewayDto;
@@ -56,7 +54,6 @@ public class PurchasingFacade {
private final ProductService productService; // 상품 조회용으로만 사용 (재고 검증은 이벤트 핸들러에서)
private final OrderService orderService;
private final PaymentService paymentService; // Payment 조회용으로만 사용
- private final PointEventPublisher pointEventPublisher; // PointEvent 발행용
private final PaymentEventPublisher paymentEventPublisher; // PaymentEvent 발행용
/**
@@ -66,8 +63,7 @@ public class PurchasingFacade {
* 2. 상품 조회 (재고 검증은 이벤트 핸들러에서 처리)
* 3. 쿠폰 할인 적용
* 4. 주문 저장 및 OrderEvent.OrderCreated 이벤트 발행
- * 5. 포인트 사용 시 PointEvent.PointUsed 이벤트 발행
- * 6. 결제 요청 시 PaymentEvent.PaymentRequested 이벤트 발행
+ * 5. 결제 요청 시 PaymentEvent.PaymentRequested 이벤트 발행
*
* 결제 방식: @@ -81,7 +77,7 @@ public class PurchasingFacade { * EDA 원칙: *
+ * 포인트 차감을 위한 명령 객체입니다. + *
+ * + * @param userId 사용자 ID + * @param usedPointAmount 사용할 포인트 금액 + */ +public record DeductPointCommand( + Long userId, + Long usedPointAmount +) { + public DeductPointCommand { + if (userId == null) { + throw new IllegalArgumentException("userId는 필수입니다."); + } + } +} + diff --git a/apps/commerce-api/src/main/java/com/loopers/application/user/PointEventHandler.java b/apps/commerce-api/src/main/java/com/loopers/application/user/PointEventHandler.java index d4181de9f..ed227556f 100644 --- a/apps/commerce-api/src/main/java/com/loopers/application/user/PointEventHandler.java +++ b/apps/commerce-api/src/main/java/com/loopers/application/user/PointEventHandler.java @@ -38,12 +38,21 @@ public class PointEventHandler { private final PointEventPublisher pointEventPublisher; /** - * 포인트 사용 이벤트를 처리하여 포인트를 차감합니다. + * 주문 생성 이벤트를 처리하여 포인트를 차감합니다. + *+ * OrderEvent.OrderCreated를 구독하여 포인트 차감 Command를 실행합니다. + *
* - * @param event 포인트 사용 이벤트 + * @param event 주문 생성 이벤트 */ @Transactional - public void handlePointUsed(PointEvent.PointUsed event) { + public void handleOrderCreated(OrderEvent.OrderCreated event) { + // 포인트 사용량이 없는 경우 처리하지 않음 + if (event.usedPointAmount() == null || event.usedPointAmount() == 0) { + log.debug("포인트 사용량이 없어 포인트 차감 처리를 건너뜁니다. (orderId: {})", event.orderId()); + return; + } + try { // 사용자 조회 (비관적 락 사용) User user = userService.getUserById(event.userId()); @@ -67,8 +76,9 @@ public void handlePointUsed(PointEvent.PointUsed event) { throw new CoreException(ErrorType.BAD_REQUEST, failureReason); } - // 포인트 차감 - user.deductPoint(Point.of(event.usedPointAmount())); + // ✅ OrderEvent.OrderCreated를 구독하여 포인트 차감 Command 실행 + DeductPointCommand command = new DeductPointCommand(event.userId(), event.usedPointAmount()); + user.deductPoint(Point.of(command.usedPointAmount())); userService.save(user); log.info("포인트 차감 처리 완료. (orderId: {}, userId: {}, usedPointAmount: {})", diff --git a/apps/commerce-api/src/main/java/com/loopers/interfaces/event/user/PointEventListener.java b/apps/commerce-api/src/main/java/com/loopers/interfaces/event/user/PointEventListener.java index 92681d058..c221c01b6 100644 --- a/apps/commerce-api/src/main/java/com/loopers/interfaces/event/user/PointEventListener.java +++ b/apps/commerce-api/src/main/java/com/loopers/interfaces/event/user/PointEventListener.java @@ -2,7 +2,6 @@ import com.loopers.application.user.PointEventHandler; import com.loopers.domain.order.OrderEvent; -import com.loopers.domain.user.PointEvent; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; @@ -34,20 +33,20 @@ public class PointEventListener { private final PointEventHandler pointEventHandler; /** - * 포인트 사용 이벤트를 처리합니다. + * 주문 생성 이벤트를 처리합니다. ** 트랜잭션 커밋 후 비동기로 실행되어 포인트 사용 처리를 수행합니다. *
* - * @param event 포인트 사용 이벤트 + * @param event 주문 생성 이벤트 */ @Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void handlePointUsed(PointEvent.PointUsed event) { + public void handleOrderCreated(OrderEvent.OrderCreated event) { try { - pointEventHandler.handlePointUsed(event); + pointEventHandler.handleOrderCreated(event); } catch (Exception e) { - log.error("포인트 사용 이벤트 처리 중 오류 발생. (orderId: {})", event.orderId(), e); + log.error("주문 생성 이벤트 처리 중 오류 발생. (orderId: {})", event.orderId(), e); // 이벤트 처리 실패는 다른 리스너에 영향을 주지 않도록 예외를 삼킴 } } diff --git a/apps/commerce-api/src/test/java/com/loopers/application/coupon/CouponEventHandlerTest.java b/apps/commerce-api/src/test/java/com/loopers/application/coupon/CouponEventHandlerTest.java index 6807542cc..cc7867966 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/coupon/CouponEventHandlerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/coupon/CouponEventHandlerTest.java @@ -22,12 +22,7 @@ import org.springframework.test.context.event.RecordApplicationEvents; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +32,7 @@ class CouponEventHandlerTest { @Autowired - private com.loopers.interfaces.event.coupon.CouponEventListener couponEventListener; + private CouponEventHandler couponEventHandler; @Autowired private UserRepository userRepository; @@ -96,7 +91,8 @@ void handleOrderCreated_skips_whenNoCouponCode() { ); // act - couponEventListener.handleOrderCreated(event); + // CouponEventHandler를 직접 호출하여 테스트 + couponEventHandler.handleOrderCreated(event); // assert // 예외 없이 처리되어야 함 @@ -121,13 +117,8 @@ void handleOrderCreated_appliesFixedAmountCoupon_success() throws InterruptedExc ); // act - // CouponEventListener를 통해 호출하여 실제 운영 환경과 동일한 방식으로 테스트 - // CouponEventListener는 @Async와 @TransactionalEventListener(phase = AFTER_COMMIT)로 설정되어 있지만, - // 테스트에서는 동기적으로 실행되도록 설정되어 있을 수 있음 - couponEventListener.handleOrderCreated(event); - - // 비동기 처리 대기 - Thread.sleep(100); + // CouponEventHandler를 직접 호출하여 테스트 + couponEventHandler.handleOrderCreated(event); // assert // 쿠폰 적용 성공 이벤트가 발행되었는지 확인 @@ -163,13 +154,8 @@ void handleOrderCreated_appliesPercentageCoupon_success() throws InterruptedExce ); // act - // CouponEventListener를 통해 호출하여 실제 운영 환경과 동일한 방식으로 테스트 - // CouponEventListener는 @Async와 @TransactionalEventListener(phase = AFTER_COMMIT)로 설정되어 있지만, - // 테스트에서는 동기적으로 실행되도록 설정되어 있을 수 있음 - couponEventListener.handleOrderCreated(event); - - // 비동기 처리 대기 - Thread.sleep(100); + // CouponEventHandler를 직접 호출하여 테스트 + couponEventHandler.handleOrderCreated(event); // assert // 쿠폰 적용 성공 이벤트가 발행되었는지 확인 @@ -207,13 +193,12 @@ void handleOrderCreated_publishesFailedEvent_whenCouponNotFound() throws Interru ); // act - // CouponEventListener를 통해 호출하여 실제 운영 환경과 동일한 방식으로 테스트 - // CouponEventListener는 @Async와 @TransactionalEventListener(phase = AFTER_COMMIT)로 설정되어 있지만, - // 테스트에서는 동기적으로 실행되도록 설정되어 있을 수 있음 - couponEventListener.handleOrderCreated(event); - - // 비동기 처리 대기 - Thread.sleep(100); + // CouponEventHandler를 직접 호출하여 테스트 + try { + couponEventHandler.handleOrderCreated(event); + } catch (Exception e) { + // 예외는 예상된 동작 + } // assert // 쿠폰 적용 실패 이벤트가 발행되었는지 확인 @@ -249,13 +234,12 @@ void handleOrderCreated_publishesFailedEvent_whenCouponNotOwnedByUser() throws I ); // act - // CouponEventListener를 통해 호출하여 실제 운영 환경과 동일한 방식으로 테스트 - // CouponEventListener는 @Async와 @TransactionalEventListener(phase = AFTER_COMMIT)로 설정되어 있지만, - // 테스트에서는 동기적으로 실행되도록 설정되어 있을 수 있음 - couponEventListener.handleOrderCreated(event); - - // 비동기 처리 대기 - Thread.sleep(100); + // CouponEventHandler를 직접 호출하여 테스트 + try { + couponEventHandler.handleOrderCreated(event); + } catch (Exception e) { + // 예외는 예상된 동작 + } // assert // 쿠폰 적용 실패 이벤트가 발행되었는지 확인 @@ -293,13 +277,12 @@ void handleOrderCreated_publishesFailedEvent_whenCouponAlreadyUsed() throws Inte ); // act - // CouponEventListener를 통해 호출하여 실제 운영 환경과 동일한 방식으로 테스트 - // CouponEventListener는 @Async와 @TransactionalEventListener(phase = AFTER_COMMIT)로 설정되어 있지만, - // 테스트에서는 동기적으로 실행되도록 설정되어 있을 수 있음 - couponEventListener.handleOrderCreated(event); - - // 비동기 처리 대기 - Thread.sleep(100); + // CouponEventHandler를 직접 호출하여 테스트 + try { + couponEventHandler.handleOrderCreated(event); + } catch (Exception e) { + // 예외는 예상된 동작 + } // assert // 쿠폰 적용 실패 이벤트가 발행되었는지 확인 diff --git a/apps/commerce-api/src/test/java/com/loopers/application/user/PointEventHandlerTest.java b/apps/commerce-api/src/test/java/com/loopers/application/user/PointEventHandlerTest.java index 3bd381a61..409e767c3 100644 --- a/apps/commerce-api/src/test/java/com/loopers/application/user/PointEventHandlerTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/application/user/PointEventHandlerTest.java @@ -1,5 +1,6 @@ package com.loopers.application.user; +import com.loopers.domain.order.OrderEvent; import com.loopers.domain.user.Gender; import com.loopers.domain.user.Point; import com.loopers.domain.user.PointEvent; @@ -14,13 +15,9 @@ import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.event.RecordApplicationEvents; -import java.util.ArrayList; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicInteger; import static org.assertj.core.api.Assertions.assertThat; @@ -32,9 +29,6 @@ class PointEventHandlerTest { @Autowired private PointEventHandler pointEventHandler; - @Autowired - private com.loopers.interfaces.event.user.PointEventListener pointEventListener; - @Autowired private UserRepository userRepository; @@ -57,13 +51,21 @@ private User createAndSaveUser(String userId, String email, long point) { @Test @DisplayName("포인트를 정상적으로 사용할 수 있다") - void handlePointUsed_success() { + void handleOrderCreated_success() { // arrange User user = createAndSaveUser("testuser", "test@example.com", 50_000L); - PointEvent.PointUsed event = PointEvent.PointUsed.of(1L, user.getId(), 10_000L); + OrderEvent.OrderCreated event = new OrderEvent.OrderCreated( + 1L, + user.getId(), + null, // couponCode + 10_000, // subtotal + 10_000L, // usedPointAmount + List.of(), // orderItems + LocalDateTime.now() + ); // act - pointEventHandler.handlePointUsed(event); + pointEventHandler.handleOrderCreated(event); // assert // 포인트 사용 실패 이벤트는 발행되지 않아야 함 @@ -77,14 +79,22 @@ void handlePointUsed_success() { @Test @DisplayName("포인트 잔액이 부족하면 포인트 사용 실패 이벤트가 발행된다") - void handlePointUsed_publishesFailedEvent_whenInsufficientBalance() { + void handleOrderCreated_publishesFailedEvent_whenInsufficientBalance() { // arrange User user = createAndSaveUser("testuser", "test@example.com", 5_000L); - PointEvent.PointUsed event = PointEvent.PointUsed.of(1L, user.getId(), 10_000L); + OrderEvent.OrderCreated event = new OrderEvent.OrderCreated( + 1L, + user.getId(), + null, // couponCode + 10_000, // subtotal + 10_000L, // usedPointAmount + List.of(), // orderItems + LocalDateTime.now() + ); // act try { - pointEventHandler.handlePointUsed(event); + pointEventHandler.handleOrderCreated(event); } catch (Exception e) { // 예외는 예상된 동작 } @@ -108,13 +118,21 @@ void handlePointUsed_publishesFailedEvent_whenInsufficientBalance() { @Test @DisplayName("포인트 잔액이 정확히 사용 요청 금액과 같으면 정상적으로 사용할 수 있다") - void handlePointUsed_success_whenBalanceEqualsUsedAmount() { + void handleOrderCreated_success_whenBalanceEqualsUsedAmount() { // arrange User user = createAndSaveUser("testuser", "test@example.com", 10_000L); - PointEvent.PointUsed event = PointEvent.PointUsed.of(1L, user.getId(), 10_000L); + OrderEvent.OrderCreated event = new OrderEvent.OrderCreated( + 1L, + user.getId(), + null, // couponCode + 10_000, // subtotal + 10_000L, // usedPointAmount + List.of(), // orderItems + LocalDateTime.now() + ); // act - pointEventHandler.handlePointUsed(event); + pointEventHandler.handleOrderCreated(event); // assert // 포인트 사용 실패 이벤트는 발행되지 않아야 함 @@ -128,13 +146,21 @@ void handlePointUsed_success_whenBalanceEqualsUsedAmount() { @Test @DisplayName("포인트 사용량이 0이면 정상적으로 처리된다") - void handlePointUsed_success_whenUsedAmountIsZero() { + void handleOrderCreated_success_whenUsedAmountIsZero() { // arrange User user = createAndSaveUser("testuser", "test@example.com", 50_000L); - PointEvent.PointUsed event = PointEvent.PointUsed.of(1L, user.getId(), 0L); + OrderEvent.OrderCreated event = new OrderEvent.OrderCreated( + 1L, + user.getId(), + null, // couponCode + 10_000, // subtotal + 0L, // usedPointAmount + List.of(), // orderItems + LocalDateTime.now() + ); // act - pointEventHandler.handlePointUsed(event); + pointEventHandler.handleOrderCreated(event); // assert // 포인트 사용 실패 이벤트는 발행되지 않아야 함 diff --git a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java index e0c867d89..4f1230c27 100644 --- a/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java +++ b/apps/commerce-api/src/test/java/com/loopers/domain/coupon/CouponServiceTest.java @@ -1,5 +1,6 @@ package com.loopers.domain.coupon; +import com.loopers.application.coupon.ApplyCouponCommand; import com.loopers.application.coupon.CouponService; import com.loopers.domain.coupon.discount.CouponDiscountStrategy; import com.loopers.domain.coupon.discount.CouponDiscountStrategyFactory; @@ -69,7 +70,7 @@ void appliesCouponAndCalculatesDiscount() { when(userCouponRepository.save(any(UserCoupon.class))).thenReturn(userCoupon); // act - Integer result = couponService.applyCoupon(userId, couponCode, subtotal); + Integer result = couponService.applyCoupon(new ApplyCouponCommand(userId, couponCode, subtotal)); // assert assertThat(result).isEqualTo(expectedDiscount); @@ -91,7 +92,7 @@ void throwsException_whenCouponNotFound() { // act CoreException result = assertThrows(CoreException.class, () -> { - couponService.applyCoupon(userId, couponCode, subtotal); + couponService.applyCoupon(new ApplyCouponCommand(userId, couponCode, subtotal)); }); // assert @@ -117,7 +118,7 @@ void throwsException_whenUserCouponNotFound() { // act CoreException result = assertThrows(CoreException.class, () -> { - couponService.applyCoupon(userId, couponCode, subtotal); + couponService.applyCoupon(new ApplyCouponCommand(userId, couponCode, subtotal)); }); // assert @@ -145,7 +146,7 @@ void throwsException_whenCouponAlreadyUsed() { // act CoreException result = assertThrows(CoreException.class, () -> { - couponService.applyCoupon(userId, couponCode, subtotal); + couponService.applyCoupon(new ApplyCouponCommand(userId, couponCode, subtotal)); }); // assert @@ -180,7 +181,7 @@ void throwsException_whenOptimisticLockConflict() { // act CoreException result = assertThrows(CoreException.class, () -> { - couponService.applyCoupon(userId, couponCode, subtotal); + couponService.applyCoupon(new ApplyCouponCommand(userId, couponCode, subtotal)); }); // assert