Skip to content

Commit f311ef8

Browse files
committed
feature: 주문과 결제 연동 및 이벤트 기반 처리 도입
- `OrderStatus` 추가로 주문 상태 관리 강화 - 주문 생성 이벤트(`OrderCreatedEvent`) 및 관련 이벤트 리스너 구현 - 결제 도메인에서 주문 상태 업데이트 로직 추가 - 결제 성공/실패 시 쿠폰 사용, 데이터 플랫폼 연동 등 비동기 이벤트 처리 활성화 - `PaymentProcessor`를 통한 결제 방식 확장 가능성 고려한 설계 - 테스트 및 비즈니스 로직 추가/개선
1 parent b90c603 commit f311ef8

12 files changed

Lines changed: 294 additions & 12 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.loopers.application.coupon;
2+
3+
import com.loopers.application.payment.PaymentEvent.PaymentCompletedEvent;
4+
import com.loopers.domain.coupon.CouponService;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.transaction.annotation.Transactional;
8+
import org.springframework.transaction.event.TransactionPhase;
9+
import org.springframework.transaction.event.TransactionalEventListener;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class CouponUsageEventListener {
14+
private final CouponService couponService;
15+
16+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
17+
@Transactional
18+
public void handlePaymentCompletedEvent(PaymentCompletedEvent event) {
19+
20+
if (event.couponId() != null) {
21+
couponService.confirmCouponUsage(event.couponId());
22+
}
23+
}
24+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.loopers.application.dataplatform;
2+
3+
import com.loopers.application.payment.PaymentEvent.PaymentCompletedEvent;
4+
import com.loopers.domain.dataplatform.DataPlatformGateway;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.scheduling.annotation.Async;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.transaction.event.TransactionPhase;
9+
import org.springframework.transaction.event.TransactionalEventListener;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class DataPlatformEventListener {
14+
15+
private final DataPlatformGateway dataPlatformGateway;
16+
17+
@Async
18+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
19+
public void handleOrderCreatedEvent(PaymentCompletedEvent event) {
20+
dataPlatformGateway.sendPaymentData(event.orderId(), event.paymentId());
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.loopers.application.order;
2+
3+
import com.loopers.domain.payment.PaymentType;
4+
import com.loopers.domain.user.User;
5+
6+
public record OrderCreatedEvent(
7+
Long orderId,
8+
User user,
9+
long finalAmount,
10+
PaymentType paymentType,
11+
String cardType,
12+
String cardNo,
13+
Long couponId
14+
) {}

apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import com.loopers.domain.order.OrderCommand.PlaceOrder;
77
import com.loopers.domain.order.OrderItem;
88
import com.loopers.domain.order.OrderService;
9-
import com.loopers.domain.payment.Payment;
10-
import com.loopers.domain.payment.PaymentProcessor;
119
import com.loopers.domain.product.Product;
1210
import com.loopers.domain.product.ProductService;
1311
import com.loopers.domain.user.User;
@@ -18,6 +16,7 @@
1816
import java.util.Map;
1917
import java.util.stream.Collectors;
2018
import lombok.RequiredArgsConstructor;
19+
import org.springframework.context.ApplicationEventPublisher;
2120
import org.springframework.stereotype.Component;
2221
import org.springframework.transaction.annotation.Transactional;
2322

@@ -30,7 +29,7 @@ public class OrderFacade {
3029
private final UserService userService;
3130
private final OrderService orderService;
3231
private final CouponService couponService;
33-
private final List<PaymentProcessor> paymentProcessors;
32+
private final ApplicationEventPublisher eventPublisher;
3433

3534
@Transactional
3635
public OrderInfo placeOrder(PlaceOrder command) {
@@ -59,22 +58,22 @@ public OrderInfo placeOrder(PlaceOrder command) {
5958
finalPaymentAmount -= disCountAmount;
6059
}
6160

62-
productService.deductStock(products, orderItems);
63-
64-
PaymentProcessor processor = paymentProcessors.stream()
65-
.filter(p -> p.supports(command.paymentType()))
66-
.findFirst()
67-
.orElseThrow(() -> new CoreException(ErrorType.BAD_REQUEST, "지원하지 않는 결제 방식입니다."));
61+
couponService.reserveCoupon(command.couponId());
6862

69-
Map<String, Object> paymentDetails = command.getPaymentDetails();
63+
productService.deductStock(products, orderItems);
7064

71-
Payment paymentResult = processor.process(
65+
OrderCreatedEvent orderEvent = new OrderCreatedEvent(
7266
order.getId(),
7367
user,
7468
finalPaymentAmount,
75-
paymentDetails
69+
command.paymentType(),
70+
command.cardType(),
71+
command.cardNo(),
72+
command.couponId()
7673
);
7774

75+
eventPublisher.publishEvent(orderEvent);
76+
7877
return OrderInfo.from(order);
7978
}
8079

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.loopers.application.payment;
2+
3+
public class PaymentEvent {
4+
public record PaymentRequestedEvent(
5+
Long orderId,
6+
Long paymentId,
7+
Long couponId
8+
) {}
9+
10+
public record PaymentCompletedEvent(
11+
Long orderId,
12+
Long paymentId,
13+
boolean isSuccess,
14+
Long couponId
15+
) {}
16+
17+
public record PaymentRequestFailedEvent(
18+
Long orderId,
19+
Long couponId,
20+
String errorMessage
21+
) {}
22+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.application.order.OrderCreatedEvent;
4+
import com.loopers.application.payment.PaymentEvent.PaymentRequestFailedEvent;
5+
import com.loopers.application.payment.PaymentEvent.PaymentRequestedEvent;
6+
import com.loopers.domain.payment.Payment;
7+
import com.loopers.domain.payment.PaymentProcessor;
8+
import com.loopers.domain.payment.PaymentType;
9+
import com.loopers.domain.user.User;
10+
import com.loopers.domain.user.UserService;
11+
import com.loopers.support.error.CoreException;
12+
import com.loopers.support.error.ErrorType;
13+
import java.util.List;
14+
import java.util.Map;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.context.ApplicationEventPublisher;
17+
import org.springframework.stereotype.Component;
18+
import org.springframework.transaction.annotation.Transactional;
19+
import org.springframework.transaction.event.TransactionPhase;
20+
import org.springframework.transaction.event.TransactionalEventListener;
21+
22+
@Component
23+
@RequiredArgsConstructor
24+
public class PgPaymentEventListener {
25+
26+
private final List<PaymentProcessor> paymentProcessors;
27+
private final UserService userService;
28+
private final ApplicationEventPublisher eventPublisher;
29+
30+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
31+
@Transactional
32+
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
33+
34+
if (event.paymentType() != PaymentType.PG) {
35+
return;
36+
}
37+
38+
User user = userService.findById(event.user().getId())
39+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "유저 정보를 찾을 수 없습니다."));
40+
41+
PaymentProcessor processor = paymentProcessors.stream()
42+
.filter(p -> p.supports(event.paymentType()))
43+
.findFirst()
44+
.orElseThrow(() -> new CoreException(ErrorType.BAD_REQUEST, "PG 결제 프로세서를 찾을 수 없습니다."));
45+
46+
Map<String, Object> paymentDetails = Map.of(
47+
"cardType", event.cardType(),
48+
"cardNo", event.cardNo()
49+
);
50+
51+
try {
52+
Payment payment = processor.process(
53+
event.orderId(),
54+
user,
55+
event.finalAmount(),
56+
paymentDetails
57+
);
58+
59+
eventPublisher.publishEvent(new PaymentRequestedEvent(
60+
event.orderId(), payment.getId(), event.couponId()));
61+
62+
} catch (Exception e) {
63+
eventPublisher.publishEvent(new PaymentRequestFailedEvent(
64+
event.orderId(),
65+
event.couponId(),
66+
e.getMessage()
67+
));
68+
}
69+
}
70+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.loopers.application.point;
2+
3+
import com.loopers.application.order.OrderCreatedEvent;
4+
import com.loopers.application.payment.PaymentEvent.PaymentCompletedEvent;
5+
import com.loopers.application.payment.PaymentEvent.PaymentRequestFailedEvent;
6+
import com.loopers.domain.order.OrderService;
7+
import com.loopers.domain.order.OrderStatus;
8+
import com.loopers.domain.payment.Payment;
9+
import com.loopers.domain.payment.PaymentProcessor;
10+
import com.loopers.domain.payment.PaymentType;
11+
import com.loopers.support.error.CoreException;
12+
import com.loopers.support.error.ErrorType;
13+
import java.util.List;
14+
import java.util.Map;
15+
import lombok.RequiredArgsConstructor;
16+
import org.springframework.context.ApplicationEventPublisher;
17+
import org.springframework.stereotype.Component;
18+
import org.springframework.transaction.annotation.Transactional;
19+
import org.springframework.transaction.event.TransactionPhase;
20+
import org.springframework.transaction.event.TransactionalEventListener;
21+
22+
@Component
23+
@RequiredArgsConstructor
24+
public class PointPaymentEventListener {
25+
private final List<PaymentProcessor> paymentProcessors;
26+
private final OrderService orderService;
27+
private final ApplicationEventPublisher eventPublisher;
28+
29+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
30+
@Transactional
31+
public void handleOrderCreatedEvent(OrderCreatedEvent event) {
32+
33+
if (event.paymentType() != PaymentType.POINT) {
34+
return;
35+
}
36+
37+
PaymentProcessor processor = paymentProcessors.stream()
38+
.filter(p -> p.supports(event.paymentType()))
39+
.findFirst()
40+
.orElseThrow(() -> new CoreException(ErrorType.BAD_REQUEST, "포인트 결제 프로세서를 찾을 수 없습니다."));
41+
42+
try {
43+
Payment payment = processor.process(
44+
event.orderId(),
45+
event.user(),
46+
event.finalAmount(),
47+
Map.of()
48+
);
49+
50+
orderService.updateOrderStatus(event.orderId(), OrderStatus.PAYMENT_COMPLETED);
51+
52+
eventPublisher.publishEvent(new PaymentCompletedEvent(
53+
event.orderId(),
54+
payment.getId(),
55+
true,
56+
event.couponId()
57+
));
58+
59+
} catch (CoreException e) {
60+
orderService.failPayment(event.orderId());
61+
eventPublisher.publishEvent(new PaymentRequestFailedEvent(
62+
event.orderId(),
63+
event.couponId(),
64+
e.getMessage()
65+
));
66+
67+
}
68+
}
69+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.loopers.domain.dataplatform;
2+
3+
public interface DataPlatformGateway {
4+
5+
void sendPaymentData(Long orderId, Long paymentId);
6+
}

apps/commerce-api/src/main/java/com/loopers/domain/order/Order.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import jakarta.persistence.Column;
1111
import jakarta.persistence.Embedded;
1212
import jakarta.persistence.Entity;
13+
import jakarta.persistence.EnumType;
14+
import jakarta.persistence.Enumerated;
1315
import jakarta.persistence.JoinColumn;
1416
import jakarta.persistence.OneToMany;
1517
import jakarta.persistence.Table;
@@ -25,6 +27,10 @@ public class Order extends BaseEntity {
2527
@Column(name = "ref_user_id", nullable = false)
2628
private Long userId;
2729

30+
@Enumerated(EnumType.STRING)
31+
@Column(name = "status", nullable = false)
32+
private OrderStatus status;
33+
2834
@Embedded
2935
@AttributeOverride(name = "value", column = @Column(name = "total_amount"))
3036
private Money totalAmount;
@@ -50,6 +56,14 @@ public Long getUserId() {
5056
return userId;
5157
}
5258

59+
public OrderStatus getStatus() {
60+
return status;
61+
}
62+
63+
public void updateStatus(OrderStatus newStatus) {
64+
this.status = newStatus;
65+
}
66+
5367
public void addOrderItem(Product product, int quantity) {
5468

5569
if (product == null) {

apps/commerce-api/src/main/java/com/loopers/domain/order/OrderService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
import java.util.List;
66
import lombok.RequiredArgsConstructor;
77
import org.springframework.stereotype.Component;
8+
import org.springframework.transaction.annotation.Transactional;
89

910
@RequiredArgsConstructor
1011
@Component
12+
@Transactional
1113
public class OrderService {
1214

1315
private final OrderRepository orderRepository;
@@ -25,4 +27,17 @@ public List<Order> getOrders(Long userId) {
2527
public Order getOrder(Long id) {
2628
return orderRepository.findById(id).orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "주문을 찾을 수 없습니다."));
2729
}
30+
31+
public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
32+
Order order = getOrder(orderId);
33+
order.updateStatus(newStatus);
34+
}
35+
public void failPayment(Long orderId) {
36+
Order order = getOrder(orderId);
37+
38+
if (order.getStatus() != OrderStatus.PAYMENT_COMPLETED) {
39+
order.updateStatus(OrderStatus.PAYMENT_FAILED);
40+
}
41+
}
42+
2843
}

0 commit comments

Comments
 (0)