Compare commits
3 Commits
d736ec4368
...
343dee1f6c
| Author | SHA1 | Date | |
|---|---|---|---|
| 343dee1f6c | |||
| b98cc4b018 | |||
| dc11f44a32 |
23
docs/20260501_payverse-jpy-지원.md
Normal file
23
docs/20260501_payverse-jpy-지원.md
Normal 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`가 항상 정수로 전송되는지 로깅/샘플 요청으로 확인(스테이징)
|
||||||
|
|
||||||
|
## 검증 로그
|
||||||
|
- [ ] 빌드/테스트 결과:
|
||||||
|
- [ ] 수기 점검 결과:
|
||||||
@@ -17,8 +17,12 @@ class CanController(private val service: CanService) {
|
|||||||
fun getCans(
|
fun getCans(
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
): ApiResponse<List<CanResponse>> {
|
): ApiResponse<List<CanResponse>> {
|
||||||
val isNotSelectedCurrency = member != null && member.id == 2L
|
val forcedCurrency = if (member != null && (member.id == 2L || member.id == 4L || member.id == 44144L)) {
|
||||||
return ApiResponse.ok(service.getCans(isNotSelectedCurrency = isNotSelectedCurrency))
|
"JPY"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(service.getCans(forcedCurrency = forcedCurrency))
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
|
|||||||
@@ -14,15 +14,11 @@ class CanService(
|
|||||||
private val repository: CanRepository,
|
private val repository: CanRepository,
|
||||||
private val countryContext: CountryContext
|
private val countryContext: CountryContext
|
||||||
) {
|
) {
|
||||||
fun getCans(isNotSelectedCurrency: Boolean): List<CanResponse> {
|
fun getCans(forcedCurrency: String? = null): List<CanResponse> {
|
||||||
val currency = if (isNotSelectedCurrency) {
|
val currency = forcedCurrency ?: when (countryContext.countryCode) {
|
||||||
null
|
|
||||||
} else {
|
|
||||||
when (countryContext.countryCode) {
|
|
||||||
"KR" -> "KRW"
|
"KR" -> "KRW"
|
||||||
else -> "USD"
|
else -> "USD"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return repository.findAllByStatusAndCurrency(status = CanStatus.SALE, currency = currency)
|
return repository.findAllByStatusAndCurrency(status = CanStatus.SALE, currency = currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,13 @@ class ChargeService(
|
|||||||
@Value("\${payverse.usd-secret-key}")
|
@Value("\${payverse.usd-secret-key}")
|
||||||
private val payverseUsdSecretKey: String,
|
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}")
|
@Value("\${payverse.host}")
|
||||||
private val payverseHost: String,
|
private val payverseHost: String,
|
||||||
|
|
||||||
@@ -106,18 +113,18 @@ class ChargeService(
|
|||||||
return when (charge.payment?.status) {
|
return when (charge.payment?.status) {
|
||||||
PaymentStatus.REQUEST -> {
|
PaymentStatus.REQUEST -> {
|
||||||
// 성공 조건 검증
|
// 성공 조건 검증
|
||||||
val mid = if (request.requestCurrency == "KRW") {
|
val mid = when (request.requestCurrency) {
|
||||||
payverseMid
|
"KRW" -> payverseMid
|
||||||
} else {
|
"JPY" -> payverseJpyMid
|
||||||
payverseUsdMid
|
else -> payverseUsdMid
|
||||||
}
|
}
|
||||||
val expectedSign = DigestUtils.sha512Hex(
|
val expectedSign = DigestUtils.sha512Hex(
|
||||||
String.format(
|
String.format(
|
||||||
"||%s||%s||%s||%s||%s||",
|
"||%s||%s||%s||%s||%s||",
|
||||||
if (request.requestCurrency == "KRW") {
|
when (request.requestCurrency) {
|
||||||
payverseSecretKey
|
"KRW" -> payverseSecretKey
|
||||||
} else {
|
"JPY" -> payverseJpySecretKey
|
||||||
payverseUsdSecretKey
|
else -> payverseUsdSecretKey
|
||||||
},
|
},
|
||||||
mid,
|
mid,
|
||||||
request.orderId,
|
request.orderId,
|
||||||
@@ -126,9 +133,8 @@ class ChargeService(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val isAmountMatch = request.requestAmount.compareTo(
|
val expectedAmount = computePayverseAmount(charge.payment!!.price, request.requestCurrency)
|
||||||
charge.payment!!.price
|
val isAmountMatch = request.requestAmount.compareTo(expectedAmount) == 0
|
||||||
) == 0
|
|
||||||
|
|
||||||
val isSuccess = request.resultStatus == "SUCCESS" &&
|
val isSuccess = request.resultStatus == "SUCCESS" &&
|
||||||
request.mid == mid &&
|
request.mid == mid &&
|
||||||
@@ -241,21 +247,20 @@ class ChargeService(
|
|||||||
?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
|
?: throw SodaException(messageKey = "can.charge.invalid_request_restart")
|
||||||
|
|
||||||
val requestCurrency = can.currency
|
val requestCurrency = can.currency
|
||||||
val isKrw = requestCurrency == "KRW"
|
val mid = when (requestCurrency) {
|
||||||
val mid = if (isKrw) {
|
"KRW" -> payverseMid
|
||||||
payverseMid
|
"JPY" -> payverseJpyMid
|
||||||
} else {
|
else -> payverseUsdMid
|
||||||
payverseUsdMid
|
|
||||||
}
|
}
|
||||||
val clientKey = if (isKrw) {
|
val clientKey = when (requestCurrency) {
|
||||||
payverseClientKey
|
"KRW" -> payverseClientKey
|
||||||
} else {
|
"JPY" -> payverseJpyClientKey
|
||||||
payverseUsdClientKey
|
else -> payverseUsdClientKey
|
||||||
}
|
}
|
||||||
val secretKey = if (isKrw) {
|
val secretKey = when (requestCurrency) {
|
||||||
payverseSecretKey
|
"KRW" -> payverseSecretKey
|
||||||
} else {
|
"JPY" -> payverseJpySecretKey
|
||||||
payverseUsdSecretKey
|
else -> payverseUsdSecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
val charge = Charge(can.can, can.rewardCan)
|
val charge = Charge(can.can, can.rewardCan)
|
||||||
@@ -270,12 +275,7 @@ class ChargeService(
|
|||||||
val savedCharge = chargeRepository.save(charge)
|
val savedCharge = chargeRepository.save(charge)
|
||||||
|
|
||||||
val chargeId = savedCharge.id!!
|
val chargeId = savedCharge.id!!
|
||||||
val amount = BigDecimal(
|
val amount = computePayverseAmount(savedCharge.payment!!.price, requestCurrency)
|
||||||
savedCharge.payment!!.price
|
|
||||||
.setScale(4, RoundingMode.HALF_UP)
|
|
||||||
.stripTrailingZeros()
|
|
||||||
.toPlainString()
|
|
||||||
)
|
|
||||||
val reqDate = savedCharge.createdAt!!.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
val reqDate = savedCharge.createdAt!!.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
||||||
val sign = DigestUtils.sha512Hex(
|
val sign = DigestUtils.sha512Hex(
|
||||||
String.format(
|
String.format(
|
||||||
@@ -312,16 +312,16 @@ class ChargeService(
|
|||||||
val member = memberRepository.findByIdOrNull(memberId)
|
val member = memberRepository.findByIdOrNull(memberId)
|
||||||
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
?: throw SodaException(messageKey = "common.error.bad_credentials")
|
||||||
|
|
||||||
val isKrw = charge.can?.currency == "KRW"
|
val currency = charge.can?.currency
|
||||||
val mid = if (isKrw) {
|
val mid = when (currency) {
|
||||||
payverseMid
|
"KRW" -> payverseMid
|
||||||
} else {
|
"JPY" -> payverseJpyMid
|
||||||
payverseUsdMid
|
else -> payverseUsdMid
|
||||||
}
|
}
|
||||||
val clientKey = if (isKrw) {
|
val clientKey = when (currency) {
|
||||||
payverseClientKey
|
"KRW" -> payverseClientKey
|
||||||
} else {
|
"JPY" -> payverseJpyClientKey
|
||||||
payverseUsdClientKey
|
else -> payverseUsdClientKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// 결제수단 확인
|
// 결제수단 확인
|
||||||
@@ -351,11 +351,12 @@ class ChargeService(
|
|||||||
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
|
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
|
||||||
|
|
||||||
val customerId = "${serverEnv}_user_${member.id!!}"
|
val customerId = "${serverEnv}_user_${member.id!!}"
|
||||||
|
val expectedAmount = computePayverseAmount(charge.can!!.price, charge.can!!.currency)
|
||||||
val isSuccess = verifyResponse.resultStatus == "SUCCESS" &&
|
val isSuccess = verifyResponse.resultStatus == "SUCCESS" &&
|
||||||
verifyResponse.transactionStatus == "SUCCESS" &&
|
verifyResponse.transactionStatus == "SUCCESS" &&
|
||||||
verifyResponse.orderId.toLongOrNull() == charge.id &&
|
verifyResponse.orderId.toLongOrNull() == charge.id &&
|
||||||
verifyResponse.customerId == customerId &&
|
verifyResponse.customerId == customerId &&
|
||||||
verifyResponse.requestAmount.compareTo(charge.can!!.price) == 0
|
verifyResponse.requestAmount.compareTo(expectedAmount) == 0
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
// verify 함수의 232~248 라인과 동일 처리
|
// verify 함수의 232~248 라인과 동일 처리
|
||||||
@@ -737,4 +738,16 @@ class ChargeService(
|
|||||||
}
|
}
|
||||||
return messageSource.getMessage("can.charge.payment_method.card", langContext.lang)
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package kr.co.vividnext.sodalive.member.contentpreference
|
|||||||
|
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
|
||||||
private val FORCED_KR_MEMBER_IDS = setOf(16L, 17L, 17958L)
|
private val FORCED_KR_MEMBER_IDS = setOf(16L, 17L, 17958L, 44144L)
|
||||||
private val FORCED_JP_MEMBER_IDS = setOf(2L, 29721L, 32050L, 37543L, 40850L)
|
private val FORCED_JP_MEMBER_IDS = setOf(2L, 29721L, 32050L, 37543L, 40850L)
|
||||||
|
|
||||||
fun resolveCountryCodeWithForcedMapping(member: Member, requestCountryCode: String?): String {
|
fun resolveCountryCodeWithForcedMapping(member: Member, requestCountryCode: String?): String {
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ payverse:
|
|||||||
usdMid: ${PAYVERSE_USD_MID}
|
usdMid: ${PAYVERSE_USD_MID}
|
||||||
usdClientKey: ${PAYVERSE_USD_CLIENT_KEY}
|
usdClientKey: ${PAYVERSE_USD_CLIENT_KEY}
|
||||||
usdSecretKey: ${PAYVERSE_USD_SECRET_KEY}
|
usdSecretKey: ${PAYVERSE_USD_SECRET_KEY}
|
||||||
|
jpyMid: ${PAYVERSE_JPY_MID}
|
||||||
|
jpyClientKey: ${PAYVERSE_JPY_CLIENT_KEY}
|
||||||
|
jpySecretKey: ${PAYVERSE_JPY_SECRET_KEY}
|
||||||
|
|
||||||
bootpay:
|
bootpay:
|
||||||
applicationId: ${BOOTPAY_APPLICATION_ID}
|
applicationId: ${BOOTPAY_APPLICATION_ID}
|
||||||
|
|||||||
Reference in New Issue
Block a user