diff --git a/docs/20260501_payverse-jpy-지원.md b/docs/20260501_payverse-jpy-지원.md new file mode 100644 index 00000000..447a0550 --- /dev/null +++ b/docs/20260501_payverse-jpy-지원.md @@ -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`가 항상 정수로 전송되는지 로깅/샘플 요청으로 확인(스테이징) + +## 검증 로그 +- [ ] 빌드/테스트 결과: +- [ ] 수기 점검 결과: diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 2c631b16..944c9d53 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -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()) + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 85d5f0f4..d9c3eaa8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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}