Skip to content

Commit d5a2599

Browse files
committed
Merge branch '9round' into 10round
# Conflicts: # apps/commerce-api/src/main/java/com/loopers/application/order/OrderFacade.java # apps/commerce-api/src/main/java/com/loopers/application/product/ProductFacade.java # apps/commerce-api/src/main/java/com/loopers/domain/brand/Brand.java # apps/commerce-api/src/main/java/com/loopers/domain/product/ProductService.java # settings.gradle.kts
2 parents e882552 + 1bc3858 commit d5a2599

139 files changed

Lines changed: 4494 additions & 132 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/commerce-api/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ dependencies {
22
// add-ons
33
implementation(project(":modules:jpa"))
44
implementation(project(":modules:redis"))
5+
implementation(project(":modules:kafka"))
56
implementation(project(":supports:jackson"))
67
implementation(project(":supports:logging"))
78
implementation(project(":supports:monitoring"))
@@ -11,6 +12,12 @@ dependencies {
1112
implementation("org.springframework.boot:spring-boot-starter-actuator")
1213
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}")
1314

15+
// resilience4j
16+
implementation("io.github.resilience4j:resilience4j-spring-boot3")
17+
18+
// feign
19+
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
20+
1421
// querydsl
1522
annotationProcessor("com.querydsl:querydsl-apt::jakarta")
1623
annotationProcessor("jakarta.persistence:jakarta.persistence-api")

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@
44
import org.springframework.boot.SpringApplication;
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
7+
import org.springframework.cloud.openfeign.EnableFeignClients;
8+
import org.springframework.scheduling.annotation.EnableAsync;
9+
import org.springframework.scheduling.annotation.EnableScheduling;
10+
711
import java.util.TimeZone;
812

913
@ConfigurationPropertiesScan
14+
@EnableFeignClients
15+
@EnableScheduling
16+
@EnableAsync
1017
@SpringBootApplication
1118
public class CommerceApiApplication {
1219

apps/commerce-api/src/main/java/com/loopers/application/like/LikeFacade.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ public class LikeFacade {
2121

2222
private final LikeService likeService;
2323

24-
public void createLike(String userId, Long productId) {
24+
public void like(String userId, Long productId) {
2525
likeService.like(userId, productId);
2626
}
2727

28-
public void deleteLike(String userId, Long productId) {
28+
public void unlike(String userId, Long productId) {
2929
likeService.unlike(userId, productId);
3030
}
3131
}
32-

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515
*/
1616
public record CreateOrderCommand(
1717
String userId,
18-
List<OrderItemCommand> items
18+
List<OrderItemCommand> items,
19+
OrderPaymentCommand payment
1920
) {}

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

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,33 @@
33
import com.loopers.domain.order.Order;
44
import com.loopers.domain.order.OrderItem;
55
import com.loopers.domain.order.OrderService;
6-
import com.loopers.domain.order.OrderStatus;
6+
import com.loopers.domain.order.event.OrderEvent;
7+
import com.loopers.domain.order.event.OrderEventPublisher;
8+
import com.loopers.domain.payment.Payment;
9+
import com.loopers.domain.payment.PaymentService;
10+
import com.loopers.domain.point.Point;
711
import com.loopers.domain.point.PointService;
812
import com.loopers.domain.product.Product;
913
import com.loopers.domain.product.ProductService;
1014
import com.loopers.support.error.CoreException;
1115
import com.loopers.support.error.ErrorType;
1216
import lombok.RequiredArgsConstructor;
13-
import lombok.extern.slf4j.Slf4j;
17+
import org.springframework.beans.factory.annotation.Value;
1418
import org.springframework.stereotype.Component;
1519
import org.springframework.transaction.annotation.Transactional;
1620

17-
/**
18-
* packageName : com.loopers.application.order
19-
* fileName : OrderFacade
20-
* author : byeonsungmun
21-
* date : 2025. 11. 13.
22-
* description :
23-
* ===========================================
24-
* DATE AUTHOR NOTE
25-
* -------------------------------------------
26-
* 2025. 11. 13. byeonsungmun 최초 생성
27-
*/
28-
29-
@Slf4j
3021
@Component
3122
@RequiredArgsConstructor
3223
public class OrderFacade {
3324

3425
private final OrderService orderService;
3526
private final ProductService productService;
3627
private final PointService pointService;
28+
private final PaymentService paymentService;
29+
private final OrderEventPublisher orderEventPublisher;
30+
@Value("${app.callback.base-url}")
31+
private String callbackBaseUrl;
32+
3733

3834
@Transactional
3935
public OrderInfo createOrder(CreateOrderCommand command) {
@@ -46,13 +42,10 @@ public OrderInfo createOrder(CreateOrderCommand command) {
4642

4743
for (OrderItemCommand itemCommand : command.items()) {
4844

49-
//상품가져오고
5045
Product product = productService.getProduct(itemCommand.productId());
5146

52-
// 재고감소
5347
product.decreaseStock(itemCommand.quantity());
5448

55-
// OrderItem생성
5649
OrderItem orderItem = OrderItem.create(
5750
product.getId(),
5851
product.getName(),
@@ -63,19 +56,55 @@ public OrderInfo createOrder(CreateOrderCommand command) {
6356
orderItem.setOrder(order);
6457
}
6558

66-
//총 가격구하고
6759
long totalAmount = order.getOrderItems().stream()
6860
.mapToLong(OrderItem::getAmount)
6961
.sum();
7062

7163
order.updateTotalAmount(totalAmount);
7264

73-
pointService.usePoint(command.userId(), totalAmount);
65+
Point point = pointService.findPointByUserId(command.userId());
66+
point.use(totalAmount);
7467

75-
//저장
7668
Order saved = orderService.createOrder(order);
77-
saved.updateStatus(OrderStatus.COMPLETE);
69+
70+
OrderPaymentCommand paymentCommand = command.payment();
71+
String cardType = paymentCommand.cardType();
72+
String orderReference = createOrderReference(saved.getId());
73+
String callbackUrl = callbackBaseUrl + "/api/v1/orders/" + orderReference + "/callback";
74+
75+
Payment payment = Payment.pending(
76+
saved.getId(),
77+
command.userId(),
78+
orderReference,
79+
cardType,
80+
paymentCommand.cardNo(),
81+
saved.getTotalAmount()
82+
);
83+
84+
paymentService.save(payment);
85+
orderEventPublisher.publish(
86+
OrderEvent.PaymentRequested.of(
87+
saved.getId(),
88+
command.userId(),
89+
orderReference,
90+
cardType,
91+
maskCardNumber(paymentCommand.cardNo()),
92+
saved.getTotalAmount(),
93+
callbackUrl
94+
)
95+
);
7896

7997
return OrderInfo.from(saved);
8098
}
99+
100+
private String maskCardNumber(String cardNo) {
101+
if (cardNo == null || cardNo.length() < 4) {
102+
return "****";
103+
}
104+
return "*".repeat(cardNo.length() - 4) + cardNo.substring(cardNo.length() - 4);
105+
}
106+
107+
private String createOrderReference(Long orderId) {
108+
return "order-" + orderId;
109+
}
81110
}

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

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,13 @@
33
import com.loopers.domain.order.Order;
44
import com.loopers.domain.order.OrderStatus;
55

6-
import java.time.LocalDateTime;
76
import java.util.List;
87

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-
*/
208
public record OrderInfo(
219
Long orderId,
2210
String userId,
2311
Long totalAmount,
2412
OrderStatus status,
25-
LocalDateTime createdAt,
2613
List<OrderItemInfo> items
2714
) {
2815
public static OrderInfo from(Order order) {
@@ -35,7 +22,6 @@ public static OrderInfo from(Order order) {
3522
order.getUserId(),
3623
order.getTotalAmount(),
3724
order.getStatus(),
38-
order.getCreatedAt(),
3925
itemInfos
4026
);
4127
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.loopers.application.order;
2+
3+
public record OrderPaymentCommand(
4+
String cardType,
5+
String cardNo
6+
) {
7+
@Override
8+
public String toString() {
9+
return "OrderPaymentCommand[cardType=" + cardType +
10+
", cardNo=****" + (cardNo != null && cardNo.length() > 4 ? cardNo.substring(cardNo.length() - 4) : "") + "]";
11+
}
12+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.payment.Payment;
8+
import com.loopers.domain.payment.PaymentService;
9+
import com.loopers.domain.point.Point;
10+
import com.loopers.domain.point.PointService;
11+
import com.loopers.domain.product.Product;
12+
import com.loopers.domain.product.ProductService;
13+
import com.loopers.infrastructure.dataplatform.OrderDataPlatformClient;
14+
import com.loopers.infrastructure.payment.PgPaymentClient;
15+
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto;
16+
import com.loopers.interfaces.api.ApiResponse;
17+
import com.loopers.support.error.CoreException;
18+
import com.loopers.support.error.ErrorType;
19+
import lombok.RequiredArgsConstructor;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.stereotype.Component;
23+
import org.springframework.transaction.annotation.Transactional;
24+
25+
@Component
26+
@RequiredArgsConstructor
27+
public class OrderPaymentProcessor {
28+
29+
private static final Logger log = LoggerFactory.getLogger(OrderPaymentProcessor.class);
30+
31+
private final PaymentService paymentService;
32+
private final OrderService orderService;
33+
private final ProductService productService;
34+
private final PointService pointService;
35+
private final PgPaymentClient pgPaymentClient;
36+
private final OrderDataPlatformClient orderDataPlatformClient;
37+
38+
@Transactional
39+
public void handlePaymentCallback(String orderReference, PgPaymentV1Dto.TransactionStatus status, String transactionKey, String reason) {
40+
Payment payment = paymentService.findByOrderReference(orderReference)
41+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보를 찾을 수 없습니다."));
42+
43+
if (payment.getTransactionKey() != null && payment.getTransactionKey().equals(transactionKey)) {
44+
return;
45+
}
46+
47+
Order order = orderService.findById(payment.getOrderId());
48+
applyPaymentResult(order, payment, status, transactionKey, reason);
49+
}
50+
51+
@Transactional
52+
public void syncPayment(Long orderId) {
53+
Payment payment = paymentService.findByOrderId(orderId)
54+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보를 찾을 수 없습니다."));
55+
Order order = orderService.findById(orderId);
56+
ApiResponse<PgPaymentV1Dto.OrderResponse> response = pgPaymentClient.getPayments(payment.getUserId(), payment.getOrderReference());
57+
PgPaymentV1Dto.OrderResponse data = response != null ? response.data() : null;
58+
59+
if (data == null || data.transactions() == null || data.transactions().isEmpty()) {
60+
log.warn("결제 내역을 찾을 수 없습니다. orderId={}, orderReference={}", orderId, payment.getOrderReference());
61+
return;
62+
}
63+
64+
PgPaymentV1Dto.TransactionRecord record = data.transactions().get(data.transactions().size() - 1);
65+
66+
if (payment.getTransactionKey() != null && payment.getTransactionKey().equals(record.transactionKey())) {
67+
log.info("이미 처리된 결제입니다. orderId={}, transactionKey={}", orderId, record.transactionKey());
68+
return;
69+
}
70+
applyPaymentResult(order, payment, record.status(), record.transactionKey(), record.reason());
71+
}
72+
73+
@Transactional
74+
public void handlePaymentResult(Long orderId, PgPaymentV1Dto.TransactionStatus status, String transactionKey, String reason) {
75+
Payment payment = paymentService.findByOrderId(orderId)
76+
.orElseThrow(() -> new CoreException(ErrorType.NOT_FOUND, "결제 정보를 찾을 수 없습니다."));
77+
Order order = orderService.findById(orderId);
78+
applyPaymentResult(order, payment, status, transactionKey, reason);
79+
}
80+
81+
private void applyPaymentResult(
82+
Order order,
83+
Payment payment,
84+
PgPaymentV1Dto.TransactionStatus status,
85+
String transactionKey,
86+
String reason
87+
) {
88+
OrderStatus newStatus = OrderPaymentSupport.mapOrderStatus(status);
89+
payment.updateStatus(OrderPaymentSupport.mapPaymentStatus(status), transactionKey, reason);
90+
paymentService.save(payment);
91+
92+
if (newStatus == OrderStatus.COMPLETE) {
93+
order.updateStatus(OrderStatus.COMPLETE);
94+
} else if (newStatus == OrderStatus.FAIL) {
95+
revertOrder(order, payment.getUserId());
96+
} else {
97+
order.updateStatus(OrderStatus.PENDING);
98+
}
99+
100+
orderDataPlatformClient.send(order, payment);
101+
}
102+
103+
private void revertOrder(Order order, String userId) {
104+
for (OrderItem item : order.getOrderItems()) {
105+
Product product = productService.getProduct(item.getProductId());
106+
product.increaseStock(item.getQuantity());
107+
}
108+
Point point = pointService.findPointByUserId(userId);
109+
if (point != null) {
110+
point.refund(order.getTotalAmount());
111+
}
112+
order.updateStatus(OrderStatus.FAIL);
113+
}
114+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.loopers.application.order;
2+
3+
import com.loopers.domain.order.OrderStatus;
4+
import com.loopers.domain.payment.PaymentStatus;
5+
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.Response;
6+
import com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.TransactionStatus;
7+
import com.loopers.interfaces.api.ApiResponse;
8+
import com.loopers.support.error.CoreException;
9+
import com.loopers.support.error.ErrorType;
10+
11+
import static com.loopers.infrastructure.payment.dto.PgPaymentV1Dto.TransactionStatus.SUCCESS;
12+
13+
public final class OrderPaymentSupport {
14+
15+
private OrderPaymentSupport() {
16+
}
17+
18+
public static OrderStatus mapOrderStatus(TransactionStatus status) {
19+
if (status == SUCCESS) {
20+
return OrderStatus.COMPLETE;
21+
}
22+
if (status == TransactionStatus.FAILED) {
23+
return OrderStatus.FAIL;
24+
}
25+
return OrderStatus.PENDING;
26+
}
27+
28+
public static PaymentStatus mapPaymentStatus(TransactionStatus status) {
29+
if (status == SUCCESS) {
30+
return PaymentStatus.SUCCESS;
31+
}
32+
if (status == TransactionStatus.FAILED) {
33+
return PaymentStatus.FAIL;
34+
}
35+
return PaymentStatus.PENDING;
36+
}
37+
38+
public static Response requirePgResponse(ApiResponse<Response> response) {
39+
Response data = response != null ? response.data() : null;
40+
if (data == null) {
41+
throw new CoreException(ErrorType.INTERNAL_ERROR, "PG 응답을 확인할 수 없습니다.");
42+
}
43+
return data;
44+
}
45+
}

0 commit comments

Comments
 (0)