Skip to content

Commit 49ea3b9

Browse files
authored
Fix: 결제 지급 포인트를 서버 가격표 기준으로 검증하도록 수정 (#404)
* Fix: 결제 금액과 포인트 검증 추가 * Fix: 필드명 수정
1 parent 3692f20 commit 49ea3b9

4 files changed

Lines changed: 207 additions & 222 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.roome.domain.payment.entity;
2+
3+
import com.roome.domain.point.entity.PointReason;
4+
import java.util.Arrays;
5+
import java.util.List;
6+
import java.util.Optional;
7+
import lombok.Getter;
8+
import lombok.RequiredArgsConstructor;
9+
10+
// 판매 중인 포인트 상품 카탈로그
11+
// 가격과 지급 포인트의 대응 관계는 서버가 소유하며, 클라이언트가 보낸 값은 이 카탈로그로 검증/파생한다.
12+
@Getter
13+
@RequiredArgsConstructor
14+
public enum PointProduct {
15+
16+
POINT_100(1_000, 100, PointReason.POINT_PURCHASE_100, PointReason.POINT_REFUND_100),
17+
POINT_550(5_000, 550, PointReason.POINT_PURCHASE_550, PointReason.POINT_REFUND_550),
18+
POINT_1200(10_000, 1_200, PointReason.POINT_PURCHASE_1200, PointReason.POINT_REFUND_1200),
19+
POINT_4000(30_000, 4_000, PointReason.POINT_PURCHASE_4000, PointReason.POINT_REFUND_4000);
20+
21+
private final int price; // 결제 금액 (KRW)
22+
private final int points; // 지급 포인트
23+
private final PointReason earnReason; // 적립 사유
24+
private final PointReason refundReason; // 환불 사유
25+
26+
public static Optional<PointProduct> findByPrice(int price) {
27+
return Arrays.stream(values())
28+
.filter(product -> product.price == price)
29+
.findFirst();
30+
}
31+
32+
public static Optional<PointProduct> findByPoints(int points) {
33+
return Arrays.stream(values())
34+
.filter(product -> product.points == points)
35+
.findFirst();
36+
}
37+
38+
public static List<PointReason> purchaseReasons() {
39+
return Arrays.stream(values())
40+
.map(PointProduct::getEarnReason)
41+
.toList();
42+
}
43+
}

roome/src/main/java/com/roome/domain/payment/service/PaymentService.java

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.roome.domain.payment.entity.Payment;
1010
import com.roome.domain.payment.entity.PaymentLog;
1111
import com.roome.domain.payment.entity.PaymentStatus;
12+
import com.roome.domain.payment.entity.PointProduct;
1213
import com.roome.domain.payment.repository.PaymentLogRepository;
1314
import com.roome.domain.payment.repository.PaymentRepository;
1415
import com.roome.domain.point.entity.Point;
@@ -60,11 +61,20 @@ public PaymentResponseDto requestPayment(Long userId, PaymentRequestDto requestD
6061
User user = userRepository.findById(userId)
6162
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
6263

64+
// 금액이 판매 중인 상품 가격인지 검증하고, 지급 포인트는 클라이언트 값이 아닌 카탈로그에서 파생한다.
65+
PointProduct product = PointProduct.findByPrice(requestDto.getAmount())
66+
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT_AMOUNT));
67+
68+
if (product.getPoints() != requestDto.getPurchasedPoints()) {
69+
log.warn("포인트 수량 위변조 의심: userId={}, orderId={}, 요청 포인트={}, 카탈로그 포인트={}",
70+
userId, requestDto.getOrderId(), requestDto.getPurchasedPoints(), product.getPoints());
71+
}
72+
6373
Payment payment = Payment.builder()
6474
.user(user)
6575
.orderId(requestDto.getOrderId())
66-
.amount(requestDto.getAmount())
67-
.purchasedPoints(requestDto.getPurchasedPoints())
76+
.amount(product.getPrice())
77+
.purchasedPoints(product.getPoints())
6878
.status(PaymentStatus.PENDING)
6979
.paymentKey(null) // 결제 성공 후 업데이트 예정
7080
.build();
@@ -151,8 +161,10 @@ public PaymentResponseDto verifyPayment(Long userId, PaymentVerifyDto verifyDto)
151161
paymentRepository.save(payment);
152162

153163
// 사용자 포인트 지급
154-
PointReason pointReason = getPointReasonForAmount(payment.getPurchasedPoints());
155-
pointService.earnPoints(payment.getUser(), pointReason);
164+
// Toss가 승인한 결제 금액(payment.amount)을 기준으로 카탈로그에서 지급 사유를 파생
165+
PointProduct product = PointProduct.findByPrice(payment.getAmount())
166+
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_PAYMENT_AMOUNT));
167+
pointService.earnPoints(payment.getUser(), product.getEarnReason());
156168

157169
// 결제 내역 로그 저장
158170
savePaymentLog(payment, verifyDto.getPaymentKey());
@@ -192,12 +204,7 @@ public PaymentResponseDto cancelPayment(Long userId, String paymentKey, String c
192204
throw new BusinessException(ErrorCode.PAYMENT_ACCESS_DENIED);
193205
}
194206

195-
List<PointReason> purchaseReasons = List.of(
196-
PointReason.POINT_PURCHASE_100,
197-
PointReason.POINT_PURCHASE_550,
198-
PointReason.POINT_PURCHASE_1200,
199-
PointReason.POINT_PURCHASE_4000
200-
);
207+
List<PointReason> purchaseReasons = PointProduct.purchaseReasons();
201208
PageRequest pageRequest = PageRequest.of(0, 1); // 최신 1개만 조회
202209

203210
List<PointHistory> latestPurchases = pointHistoryRepository.findLatestPurchase(userId, purchaseReasons, pageRequest);
@@ -226,7 +233,13 @@ public PaymentResponseDto cancelPayment(Long userId, String paymentKey, String c
226233
throw new BusinessException(ErrorCode.PAYMENT_NOT_CANCELABLE);
227234
}
228235

229-
int refundPoints = getRefundPointsForAmount(cancelAmount);
236+
// 환불 금액이 판매 중인 상품 가격 단위인지 검증하고, 차감 포인트를 카탈로그에서 파생한다.
237+
if (cancelAmount == null) {
238+
throw new BusinessException(ErrorCode.INVALID_REFUND_AMOUNT);
239+
}
240+
PointProduct refundProduct = PointProduct.findByPrice(cancelAmount)
241+
.orElseThrow(() -> new BusinessException(ErrorCode.INVALID_REFUND_AMOUNT));
242+
int refundPoints = refundProduct.getPoints();
230243

231244
// Toss API에 결제 취소 요청
232245
boolean isCanceled = tossPaymentClient.cancelPayment(payment.getPaymentKey(), cancelReason,
@@ -240,7 +253,7 @@ public PaymentResponseDto cancelPayment(Long userId, String paymentKey, String c
240253
paymentRepository.save(payment);
241254

242255
// 사용자 포인트 차감
243-
pointService.usePoints(payment.getUser(), getRefundReasonForAmount(refundPoints));
256+
pointService.usePoints(payment.getUser(), refundProduct.getRefundReason());
244257

245258
saveRefundLog(payment, cancelAmount, paymentKey);
246259

@@ -297,33 +310,4 @@ private void saveRefundLog(Payment payment, int refundAmount, String paymentKey)
297310
}
298311

299312

300-
private PointReason getPointReasonForAmount(int purchasedPoints) {
301-
return switch (purchasedPoints) {
302-
case 100 -> PointReason.POINT_PURCHASE_100;
303-
case 550 -> PointReason.POINT_PURCHASE_550;
304-
case 1200 -> PointReason.POINT_PURCHASE_1200;
305-
case 4000 -> PointReason.POINT_PURCHASE_4000;
306-
default -> throw new BusinessException(ErrorCode.INVALID_PAYMENT_AMOUNT);
307-
};
308-
}
309-
310-
private PointReason getRefundReasonForAmount(int refundPoints) {
311-
return switch (refundPoints) {
312-
case 100 -> PointReason.POINT_REFUND_100;
313-
case 550 -> PointReason.POINT_REFUND_550;
314-
case 1200 -> PointReason.POINT_REFUND_1200;
315-
case 4000 -> PointReason.POINT_REFUND_4000;
316-
default -> throw new BusinessException(ErrorCode.INVALID_REFUND_POINT_AMOUNT);
317-
};
318-
}
319-
320-
private int getRefundPointsForAmount(int cancelAmount) {
321-
return switch (cancelAmount) {
322-
case 1000 -> 100;
323-
case 5000 -> 550;
324-
case 10000 -> 1200;
325-
case 30000 -> 4000;
326-
default -> throw new BusinessException(ErrorCode.INVALID_REFUND_AMOUNT);
327-
};
328-
}
329313
}

0 commit comments

Comments
 (0)