Skip to content

Commit dfff473

Browse files
committed
feature: 카드 결제 기능 추가 (PG 연동 및 결제 상태 관리 포함)
1 parent 5fdd536 commit dfff473

15 files changed

Lines changed: 325 additions & 95 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
77
import java.util.TimeZone;
8+
import org.springframework.cloud.openfeign.EnableFeignClients;
89

10+
@EnableFeignClients
911
@ConfigurationPropertiesScan
1012
@SpringBootApplication
1113
public class CommerceApiApplication {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
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.point.PointService;
8+
import com.loopers.domain.payment.Payment;
9+
import com.loopers.domain.payment.PaymentService;
910
import com.loopers.domain.product.Product;
1011
import com.loopers.domain.product.ProductService;
1112
import com.loopers.domain.user.User;
@@ -27,7 +28,7 @@ public class OrderFacade {
2728
private final ProductService productService;
2829
private final UserService userService;
2930
private final OrderService orderService;
30-
private final PointService pointService;
31+
private final PaymentService paymentService;
3132

3233
@Transactional
3334
public OrderInfo placeOrder(PlaceOrder command) {
@@ -43,12 +44,16 @@ public OrderInfo placeOrder(PlaceOrder command) {
4344

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

4848
productService.deductStock(products, orderItems);
49-
pointService.deductPoint(user, totalAmount);
5049

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

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

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@
33
import com.loopers.domain.money.Money;
44
import com.loopers.domain.order.Order;
55
import com.loopers.domain.order.OrderItem;
6+
import com.loopers.domain.payment.Payment;
67
import java.util.List;
78

89
public record OrderInfo(
910
Long orderId,
1011
Long userId,
1112
List<OrderItemInfo> items,
12-
long totalAmount
13+
long totalAmount,
14+
String transactionId,
15+
String paymentStatus
1316
) {
1417

15-
public static OrderInfo from(Order order) {
18+
public static OrderInfo from(Order order, Payment payment) {
1619
return new OrderInfo(
1720
order.getId(),
1821
order.getUserId(),
1922
order.getOrderItems().stream().map(OrderItemInfo::from).toList(),
20-
order.getTotalAmount().getValue()
23+
order.getTotalAmount().getValue(),
24+
payment.getTransactionId(),
25+
payment.getStatus().name()
2126
);
2227
}
2328

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

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

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

1214
}

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
import jakarta.persistence.Table;
1414
import java.util.UUID;
1515
import lombok.AccessLevel;
16+
import lombok.Getter;
1617
import lombok.NoArgsConstructor;
1718
import org.springframework.util.StringUtils;
1819

20+
@Getter
1921
@Entity
2022
@Table(name = "payment")
2123
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -30,8 +32,9 @@ public class Payment extends BaseEntity {
3032
@Column(name = "transaction_id", nullable = false, unique = true)
3133
private String transactionId;//멱등키
3234

35+
@Enumerated(EnumType.STRING)
3336
@Column(name = "card_type", nullable = false)
34-
private String cardType;
37+
private CardType cardType;
3538

3639
@Column(name = "card_no", nullable = false)
3740
private String cardNo;
@@ -47,8 +50,8 @@ public class Payment extends BaseEntity {
4750
@Column(name = "pg_txn_id")
4851
private String pgTxnId;
4952

50-
public Payment(Long orderId, Long userId, Money amount, String cardType, String cardNo) {
51-
validateConstructor(orderId, userId, amount, cardType, cardNo);
53+
public Payment(Long orderId, Long userId, Money amount, CardType cardType, String cardNo) {
54+
validateConstructor(orderId, userId, amount, cardNo);
5255

5356
this.orderId = orderId;
5457
this.userId = userId;
@@ -60,7 +63,7 @@ public Payment(Long orderId, Long userId, Money amount, String cardType, String
6063
this.transactionId = UUID.randomUUID().toString();
6164
}
6265

63-
private void validateConstructor(Long orderId, Long userId, Money amount, String cardType, String cardNo) {
66+
private void validateConstructor(Long orderId, Long userId, Money amount, String cardNo) {
6467
if (orderId == null) {
6568
throw new CoreException(ErrorType.BAD_REQUEST, "주문 정보는 필수입니다.");
6669
}
@@ -70,11 +73,18 @@ private void validateConstructor(Long orderId, Long userId, Money amount, String
7073
if (amount == null) {
7174
throw new CoreException(ErrorType.BAD_REQUEST, "결제 금액은 필수입니다.");
7275
}
73-
if (!StringUtils.hasText(cardType)) {
74-
throw new CoreException(ErrorType.BAD_REQUEST, "카드 종류는 필수입니다.");
75-
}
7676
if (!StringUtils.hasText(cardNo)) {
7777
throw new CoreException(ErrorType.BAD_REQUEST, "카드 번호는 필수입니다.");
7878
}
7979
}
80+
81+
public void completePayment(String pgTxnId) {
82+
this.pgTxnId = pgTxnId;
83+
this.status = PaymentStatus.PAID;
84+
}
85+
86+
public void failPayment() {
87+
this.status = PaymentStatus.FAILED;
88+
}
89+
8090
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.loopers.domain.payment;
2+
3+
public interface PaymentExecutor {
4+
5+
String execute(Payment payment);
6+
}

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,29 @@
1111
public class PaymentService {
1212

1313
private final PaymentRepository paymentRepository;
14+
private final PaymentExecutor paymentExecutor;
1415

1516
@Transactional
16-
public Payment createPendingPayment(User user, Order order, String cardType, String cardNo) {
17+
public Payment processPayment(User user, Order order, String cardType, String cardNo) {
1718
Payment payment = new Payment(
1819
order.getId(),
1920
user.getId(),
2021
order.getTotalAmount(),
21-
cardType,
22+
CardType.valueOf(cardType),
2223
cardNo
2324
);
2425

25-
return paymentRepository.save(payment);
26+
paymentRepository.save(payment);
27+
try {
28+
String pgTxnId = paymentExecutor.execute(payment);
29+
30+
payment.completePayment(pgTxnId);
31+
32+
} catch (Exception e) {
33+
payment.failPayment();
34+
throw e;
35+
}
36+
37+
return payment;
2638
}
2739
}

apps/commerce-api/src/main/java/com/loopers/domain/product/Product.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ public int getLikeCount() {
9191
}
9292

9393
public void deductStock(int quantity) {
94+
if (quantity <= 0) {
95+
throw new CoreException(ErrorType.BAD_REQUEST, "차감할 재고 수량은 0보다 커야 합니다.");
96+
}
97+
if (this.stock - quantity < 0) {
98+
throw new CoreException(ErrorType.BAD_REQUEST, "재고가 부족합니다.");
99+
}
94100
this.stock -= quantity;
95101
}
96102

@@ -100,7 +106,7 @@ public int increaseLikeCount() {
100106
}
101107

102108
public int decreaseLikeCount() {
103-
if (this.likeCount < 0) {
109+
if (this.likeCount <= 0) {
104110
throw new CoreException(ErrorType.BAD_REQUEST, "좋아요수는 0보다 작을 수 없습니다.");
105111
}
106112
this.likeCount--;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.loopers.infrastructure.pg;
2+
3+
import com.loopers.domain.payment.Payment;
4+
import com.loopers.domain.payment.PaymentExecutor;
5+
import com.loopers.infrastructure.pg.PgV1Dto.PgApiResponse;
6+
import com.loopers.infrastructure.pg.PgV1Dto.PgApproveRequest;
7+
import com.loopers.infrastructure.pg.PgV1Dto.PgApproveResponse;
8+
import com.loopers.support.error.CoreException;
9+
import com.loopers.support.error.ErrorType;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.stereotype.Component;
12+
13+
@Component
14+
@RequiredArgsConstructor
15+
public class LoopersPgExecutor implements PaymentExecutor {
16+
17+
private final PgClient pgClient;
18+
19+
@Override
20+
public String execute(Payment payment) {
21+
try {
22+
PgApproveRequest request = new PgApproveRequest(
23+
payment.getTransactionId(),
24+
payment.getCardType().name(),
25+
payment.getCardNo(),
26+
payment.getAmount().getValue(),
27+
"http://localhost:8080/api/v1/callback"
28+
);
29+
30+
PgApiResponse<PgApproveResponse> responseWrapper =
31+
pgClient.requestPayment(payment.getUserId(), request);
32+
33+
if ("SUCCESS".equals(responseWrapper.result()) && responseWrapper.data() != null) {
34+
return responseWrapper.data().transactionKey();
35+
} else {
36+
throw new CoreException(ErrorType.BAD_REQUEST, "결제에 실패했습니다.");
37+
}
38+
39+
} catch (CoreException e) {
40+
throw e;
41+
} catch (Exception e) {
42+
throw new CoreException(ErrorType.BAD_REQUEST, "PG 연동 오류: " + e.getMessage());
43+
}
44+
}
45+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.loopers.infrastructure.pg;
2+
import com.loopers.infrastructure.pg.PgV1Dto.PgApiResponse;
3+
import com.loopers.infrastructure.pg.PgV1Dto.PgApproveRequest;
4+
import com.loopers.infrastructure.pg.PgV1Dto.PgApproveResponse;
5+
import org.springframework.cloud.openfeign.FeignClient;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RequestBody;
8+
import org.springframework.web.bind.annotation.RequestHeader;
9+
10+
@FeignClient(name = "pg-client", url = "http://localhost:8082")
11+
public interface PgClient {
12+
@PostMapping("/api/v1/payments")
13+
PgApiResponse<PgApproveResponse> requestPayment(
14+
@RequestHeader("X-USER-ID") Long userId,
15+
@RequestBody PgApproveRequest request
16+
);
17+
}

0 commit comments

Comments
 (0)