Skip to content

Commit 648fd71

Browse files
committed
feature: 주문 및 결제 도메인 리팩토링, 자동 결제 복구 스케줄러 추가
- 카드 정보 제거 및 포인트 차감 방식으로 주문 결제 변경(주문로직과 결제 로직 분리를 위해) - Payment 관련 도메인 책임 분리 (Facade, DTO, Command 추가) - 스케줄러 추가로 정체된 결제 자동 복구 처리 - PG 관련 엔드포인트 및 DTO 확장 - 테스트 및 기타 불필요 로직 제거/수정
1 parent 8c8ffc7 commit 648fd71

21 files changed

Lines changed: 486 additions & 125 deletions

apps/commerce-api/src/main/java/com/loopers/CommerceApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
77
import java.util.TimeZone;
88
import org.springframework.cloud.openfeign.EnableFeignClients;
9+
import org.springframework.scheduling.annotation.EnableScheduling;
910

11+
@EnableScheduling
1012
@EnableFeignClients
1113
@ConfigurationPropertiesScan
1214
@SpringBootApplication

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import com.loopers.domain.order.OrderCommand.PlaceOrder;
66
import com.loopers.domain.order.OrderItem;
77
import com.loopers.domain.order.OrderService;
8-
import com.loopers.domain.payment.Payment;
9-
import com.loopers.domain.payment.PaymentService;
8+
import com.loopers.domain.point.PointService;
109
import com.loopers.domain.product.Product;
1110
import com.loopers.domain.product.ProductService;
1211
import com.loopers.domain.user.User;
@@ -28,7 +27,7 @@ public class OrderFacade {
2827
private final ProductService productService;
2928
private final UserService userService;
3029
private final OrderService orderService;
31-
private final PaymentService paymentService;
30+
private final PointService pointService;
3231

3332
@Transactional
3433
public OrderInfo placeOrder(PlaceOrder command) {
@@ -44,16 +43,12 @@ public OrderInfo placeOrder(PlaceOrder command) {
4443

4544
List<OrderItem> orderItems = buildOrderItems(products, command.items());
4645
Order order = orderService.createOrder(user.getId(), orderItems);
46+
long totalAmount = order.getTotalAmount().getValue();
4747

4848
productService.deductStock(products, orderItems);
49+
pointService.deductPoint(user, totalAmount);
4950

50-
Payment payment = paymentService.processPayment(
51-
user,
52-
order,
53-
command.cardType(),
54-
command.cardNo()
55-
);
56-
return OrderInfo.from(order, payment);
51+
return OrderInfo.from(order);
5752
}
5853

5954
private List<OrderItem> buildOrderItems(List<Product> products, List<Item> items) {

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,15 @@ public record OrderInfo(
1010
Long orderId,
1111
Long userId,
1212
List<OrderItemInfo> items,
13-
long totalAmount,
14-
String transactionId,
15-
String paymentStatus
13+
long totalAmount
1614
) {
1715

18-
public static OrderInfo from(Order order, Payment payment) {
16+
public static OrderInfo from(Order order) {
1917
return new OrderInfo(
2018
order.getId(),
2119
order.getUserId(),
2220
order.getOrderItems().stream().map(OrderItemInfo::from).toList(),
23-
order.getTotalAmount().getValue(),
24-
payment.getTransactionId(),
25-
payment.getStatus().name()
21+
order.getTotalAmount().getValue()
2622
);
2723
}
2824

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.money.Money;
4+
import com.loopers.domain.payment.Payment;
5+
import com.loopers.domain.payment.PaymentCommand;
6+
import com.loopers.domain.payment.PaymentExecutor;
7+
import com.loopers.domain.payment.PaymentService;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
@RequiredArgsConstructor
13+
public class PaymentFacade {
14+
15+
private final PaymentService paymentService;
16+
private final PaymentExecutor paymentExecutor;
17+
18+
public Payment processPaymentRequest(PaymentCommand.CreatePayment command) {
19+
20+
return paymentService.findValidPayment(command.orderId())
21+
.orElseGet(() -> {
22+
Payment newPayment = paymentService.createPendingPayment(command.userId(),
23+
command.orderId(),
24+
new Money(command.amount()),
25+
command.cardType(),
26+
command.cardNo());
27+
28+
try {
29+
String pgTxnId = paymentExecutor.execute(newPayment);
30+
31+
paymentService.registerPgToken(newPayment, pgTxnId);
32+
33+
return newPayment;
34+
} catch (Exception e) {
35+
paymentService.failPayment(newPayment);
36+
throw e;
37+
}
38+
});
39+
}
40+
41+
42+
public void handlePaymentCallback(String pgTxnId, boolean isSuccess) {
43+
Payment payment = paymentService.getPaymentByPgTxnId(pgTxnId);
44+
45+
if (isSuccess) {
46+
paymentService.completePayment(payment);
47+
} else {
48+
paymentService.failPayment(payment);
49+
}
50+
}
51+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.CardType;
4+
import com.loopers.domain.payment.Payment;
5+
import com.loopers.domain.payment.PaymentStatus;
6+
7+
public record PaymentInfo(
8+
String paymentId,
9+
Long orderId,
10+
CardType cardType,
11+
String cardNo,
12+
Long amount,
13+
PaymentStatus status,
14+
String transactionId) {
15+
16+
public static PaymentInfo from(Payment payment) {
17+
return new PaymentInfo(
18+
payment.getId().toString(),
19+
payment.getOrderId(),
20+
payment.getCardType(),
21+
payment.getCardNo(),
22+
payment.getAmount().getValue(),
23+
payment.getStatus(),
24+
payment.getTransactionId()
25+
);
26+
}
27+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.loopers.application.payment;
2+
3+
import com.loopers.domain.payment.Payment;
4+
import com.loopers.domain.payment.PaymentRepository;
5+
import com.loopers.domain.payment.PaymentStatus;
6+
import com.loopers.infrastructure.pg.PgClient;
7+
import com.loopers.infrastructure.pg.PgV1Dto.PgDetail;
8+
import com.loopers.infrastructure.pg.PgV1Dto.PgOrderResponse;
9+
import jakarta.transaction.Transactional;
10+
import java.time.LocalDateTime;
11+
import java.util.List;
12+
import lombok.RequiredArgsConstructor;
13+
import org.springframework.scheduling.annotation.Scheduled;
14+
import org.springframework.stereotype.Component;
15+
16+
@Component
17+
@RequiredArgsConstructor
18+
public class PaymentRecoveryScheduler {
19+
20+
private final PaymentRepository paymentRepository;
21+
private final PgClient pgClient;
22+
23+
@Scheduled(fixedDelay = 60000)
24+
@Transactional
25+
public void recover() {
26+
LocalDateTime timeLimit = LocalDateTime.now().minusMinutes(5);
27+
List<Payment> stuckPayments = paymentRepository.findAllByStatusAndCreatedAtBefore(
28+
PaymentStatus.READY, timeLimit
29+
);
30+
31+
for (Payment payment : stuckPayments) {
32+
try {
33+
PgOrderResponse response = pgClient.getTransactionsByOrder(
34+
payment.getUserId(), String.valueOf(payment.getOrderId())
35+
);
36+
37+
if (response.transactions() != null && !response.transactions().isEmpty()) {
38+
PgDetail detail = response.transactions().get(0);
39+
40+
if ("SUCCESS".equals(detail.status())) {
41+
payment.completePayment();
42+
} else if ("FAIL".equals(detail.status())) {
43+
payment.failPayment();
44+
}
45+
} else {
46+
payment.failPayment();
47+
}
48+
} catch (Exception e) {
49+
}
50+
}
51+
}
52+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ public class OrderCommand {
66

77
public record PlaceOrder(
88
Long userId,
9-
List<Item> items,
10-
String cardType,
11-
String cardNo
9+
List<Item> items
1210
) {
1311

1412
}

apps/commerce-api/src/main/java/com/loopers/domain/payment/Payment.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,25 @@ private void validateConstructor(Long orderId, Long userId, Money amount, String
7878
}
7979
}
8080

81-
public void completePayment(String pgTxnId) {
81+
public void completePayment() {
8282
if (this.status == PaymentStatus.PAID || this.status == PaymentStatus.CANCELLED) {
8383
return;
8484
}
85-
this.pgTxnId = pgTxnId;
8685
this.status = PaymentStatus.PAID;
8786
}
8887

8988
public void failPayment() {
89+
if (this.status == PaymentStatus.PAID) {
90+
throw new CoreException(ErrorType.BAD_REQUEST, "이미 성공한 결제는 실패 처리할 수 없습니다.");
91+
}
9092
this.status = PaymentStatus.FAILED;
9193
}
9294

95+
public boolean isProcessingOrCompleted() {
96+
return this.status == PaymentStatus.PAID || this.status == PaymentStatus.READY;
97+
}
98+
99+
public void setPgTxnId(String pgTxnId) {
100+
this.pgTxnId = pgTxnId;
101+
}
93102
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.loopers.domain.payment;
2+
3+
import com.loopers.interfaces.api.payment.PaymentV1Dto.PaymentRequest;
4+
5+
public class PaymentCommand {
6+
7+
public record CreatePayment(
8+
Long userId,
9+
Long orderId,
10+
Long amount,
11+
CardType cardType,
12+
String cardNo
13+
) {
14+
15+
public static CreatePayment from(Long userId, PaymentRequest request) {
16+
return new CreatePayment(
17+
userId,
18+
request.orderId(),
19+
request.amount(),
20+
request.cardType(),
21+
request.cardNo()
22+
);
23+
}
24+
}
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
package com.loopers.domain.payment;
22

3+
import java.time.LocalDateTime;
4+
import java.util.List;
5+
import java.util.Optional;
6+
37
public interface PaymentRepository {
48

59
Payment save(Payment payment);
10+
11+
Optional<Payment> findByOrderId(Long id);
12+
13+
List<Payment> findAllByStatusAndCreatedAtBefore(PaymentStatus paymentStatus, LocalDateTime timeLimit);
14+
15+
Optional<Payment> findByPgTxnId(String pgTxnId);
616
}

0 commit comments

Comments
 (0)