feat(payverse): JPY 결제 지원 추가 및 금액 포맷 규칙 적용

- ChargeService에 JPY 전용 자격 증명 주입(payverse.jpy-*)
  - payverseCharge/payverseWebhook/payverseVerify에 KRW/JPY/USD 3분기 적용
  - JPY 금액 정수화(FLOOR) 처리 및 공통 함수 computePayverseAmount 추가
  - 검증/체크리스트 문서 추가(docs/20260501_payverse-jpy-지원.md)
This commit is contained in:
2026-05-01 14:54:55 +09:00
parent b98cc4b018
commit 343dee1f6c
3 changed files with 79 additions and 40 deletions

View File

@@ -0,0 +1,23 @@
# Payverse JPY 지원 작업 계획
- [x] 요구사항 정리
- JPY 전용 자격 증명 사용
- `payverseCharge`, `payverseWebhook`, `payverseVerify` 모두 일관 분기 추가
- 금액 포맷: JPY는 강제 정수화(소수점 버림)
- 결제수단 표기는 현행 규칙 유지
- [x] 구현 항목
- [x] 환경변수 주입: `payverse.jpy-mid`, `payverse.jpy-client-key`, `payverse.jpy-secret-key`
- [x] `ChargeService.payverseCharge`에 JPY 분기 및 금액 포맷 적용
- [x] `ChargeService.payverseWebhook`에 JPY 분기 및 금액 검증 적용
- [x] `ChargeService.payverseVerify`에 JPY 분기 및 금액 검증 적용
- [x] 공통 금액 포맷 함수 `computePayverseAmount` 추가 (JPY=버림, 그외=4자리 반올림)
- [ ] 검증 항목
- [ ] 단위/통합 테스트 빌드 및 실행 (`./gradlew test`)
- [ ] KRW/JPY/USD 각각에 대해 payload 서명 및 검증 로직 수기 점검
- [ ] JPY에서 `requestAmount`가 항상 정수로 전송되는지 로깅/샘플 요청으로 확인(스테이징)
## 검증 로그
- [ ] 빌드/테스트 결과:
- [ ] 수기 점검 결과:

View File

@@ -85,6 +85,13 @@ class ChargeService(
@Value("\${payverse.usd-secret-key}")
private val payverseUsdSecretKey: String,
@Value("\${payverse.jpy-mid}")
private val payverseJpyMid: String,
@Value("\${payverse.jpy-client-key}")
private val payverseJpyClientKey: String,
@Value("\${payverse.jpy-secret-key}")
private val payverseJpySecretKey: String,
@Value("\${payverse.host}")
private val payverseHost: String,
@@ -106,18 +113,18 @@ class ChargeService(
return when (charge.payment?.status) {
PaymentStatus.REQUEST -> {
// 성공 조건 검증
val mid = if (request.requestCurrency == "KRW") {
payverseMid
} else {
payverseUsdMid
val mid = when (request.requestCurrency) {
"KRW" -> payverseMid
"JPY" -> payverseJpyMid
else -> payverseUsdMid
}
val expectedSign = DigestUtils.sha512Hex(
String.format(
"||%s||%s||%s||%s||%s||",
if (request.requestCurrency == "KRW") {
payverseSecretKey
} else {
payverseUsdSecretKey
when (request.requestCurrency) {
"KRW" -> payverseSecretKey
"JPY" -> payverseJpySecretKey
else -> payverseUsdSecretKey
},
mid,
request.orderId,
@@ -126,9 +133,8 @@ class ChargeService(
)
)
val isAmountMatch = request.requestAmount.compareTo(
charge.payment!!.price
) == 0
val expectedAmount = computePayverseAmount(charge.payment!!.price, request.requestCurrency)
val isAmountMatch = request.requestAmount.compareTo(expectedAmount) == 0
val isSuccess = request.resultStatus == "SUCCESS" &&
request.mid == mid &&
@@ -241,21 +247,20 @@ class ChargeService(
?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
val requestCurrency = can.currency
val isKrw = requestCurrency == "KRW"
val mid = if (isKrw) {
payverseMid
} else {
payverseUsdMid
val mid = when (requestCurrency) {
"KRW" -> payverseMid
"JPY" -> payverseJpyMid
else -> payverseUsdMid
}
val clientKey = if (isKrw) {
payverseClientKey
} else {
payverseUsdClientKey
val clientKey = when (requestCurrency) {
"KRW" -> payverseClientKey
"JPY" -> payverseJpyClientKey
else -> payverseUsdClientKey
}
val secretKey = if (isKrw) {
payverseSecretKey
} else {
payverseUsdSecretKey
val secretKey = when (requestCurrency) {
"KRW" -> payverseSecretKey
"JPY" -> payverseJpySecretKey
else -> payverseUsdSecretKey
}
val charge = Charge(can.can, can.rewardCan)
@@ -270,12 +275,7 @@ class ChargeService(
val savedCharge = chargeRepository.save(charge)
val chargeId = savedCharge.id!!
val amount = BigDecimal(
savedCharge.payment!!.price
.setScale(4, RoundingMode.HALF_UP)
.stripTrailingZeros()
.toPlainString()
)
val amount = computePayverseAmount(savedCharge.payment!!.price, requestCurrency)
val reqDate = savedCharge.createdAt!!.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
val sign = DigestUtils.sha512Hex(
String.format(
@@ -312,16 +312,16 @@ class ChargeService(
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
val isKrw = charge.can?.currency == "KRW"
val mid = if (isKrw) {
payverseMid
} else {
payverseUsdMid
val currency = charge.can?.currency
val mid = when (currency) {
"KRW" -> payverseMid
"JPY" -> payverseJpyMid
else -> payverseUsdMid
}
val clientKey = if (isKrw) {
payverseClientKey
} else {
payverseUsdClientKey
val clientKey = when (currency) {
"KRW" -> payverseClientKey
"JPY" -> payverseJpyClientKey
else -> payverseUsdClientKey
}
// 결제수단 확인
@@ -351,11 +351,12 @@ class ChargeService(
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
val customerId = "${serverEnv}_user_${member.id!!}"
val expectedAmount = computePayverseAmount(charge.can!!.price, charge.can!!.currency)
val isSuccess = verifyResponse.resultStatus == "SUCCESS" &&
verifyResponse.transactionStatus == "SUCCESS" &&
verifyResponse.orderId.toLongOrNull() == charge.id &&
verifyResponse.customerId == customerId &&
verifyResponse.requestAmount.compareTo(charge.can!!.price) == 0
verifyResponse.requestAmount.compareTo(expectedAmount) == 0
if (isSuccess) {
// verify 함수의 232~248 라인과 동일 처리
@@ -737,4 +738,16 @@ class ChargeService(
}
return messageSource.getMessage("can.charge.payment_method.card", langContext.lang)
}
// Payverse 금액 포맷: 통화별 규칙 적용
private fun computePayverseAmount(price: BigDecimal, currency: String): BigDecimal {
val scaled = if (currency == "JPY") {
// JPY: 강제 정수화, 소수점 버림
price.setScale(0, RoundingMode.FLOOR)
} else {
// 그 외: 4자리까지 반올림 후 불필요 0 제거
price.setScale(4, RoundingMode.HALF_UP).stripTrailingZeros()
}
return BigDecimal(scaled.stripTrailingZeros().toPlainString())
}
}

View File

@@ -23,6 +23,9 @@ payverse:
usdMid: ${PAYVERSE_USD_MID}
usdClientKey: ${PAYVERSE_USD_CLIENT_KEY}
usdSecretKey: ${PAYVERSE_USD_SECRET_KEY}
jpyMid: ${PAYVERSE_JPY_MID}
jpyClientKey: ${PAYVERSE_JPY_CLIENT_KEY}
jpySecretKey: ${PAYVERSE_JPY_SECRET_KEY}
bootpay:
applicationId: ${BOOTPAY_APPLICATION_ID}