feat(charge): payverse pg - webhook API 추가

This commit is contained in:
2025-09-25 21:18:45 +09:00
parent bc6c05b3ea
commit 03149a637d
4 changed files with 110 additions and 1 deletions

View File

@@ -6,18 +6,25 @@ import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
import kr.co.vividnext.sodalive.marketing.AdTrackingService
import kr.co.vividnext.sodalive.member.Member
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
import java.time.LocalDateTime
import javax.servlet.http.HttpServletRequest
@RestController
@RequestMapping("/charge")
class ChargeController(
private val service: ChargeService,
private val trackingService: AdTrackingService
private val trackingService: AdTrackingService,
@Value("\${payverse.inbound-ip}")
private val payverseInboundIp: String
) {
@PostMapping("/payverse")
@@ -46,6 +53,24 @@ class ChargeController(
ApiResponse.ok(Unit)
}
// Payverse Webhook 엔드포인트 (payverseVerify 아래)
@PostMapping("/payverse/webhook")
fun payverseWebhook(
@RequestBody request: PayverseWebhookRequest,
servletRequest: HttpServletRequest
): PayverseWebhookResponse {
val remoteIp = servletRequest.remoteAddr ?: ""
if (remoteIp != payverseInboundIp) {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
val success = service.payverseWebhook(request)
if (!success) {
throw ResponseStatusException(HttpStatus.NOT_FOUND)
}
return PayverseWebhookResponse(receiveResult = "SUCCESS")
}
@PostMapping
fun charge(
@RequestBody chargeRequest: ChargeRequest,

View File

@@ -72,3 +72,19 @@ data class PayverseVerifyResponse(
val processingCurrency: String,
val processingAmount: BigDecimal
)
data class PayverseWebhookRequest(
val type: String,
val mid: String,
val tid: String,
val schemeCode: String,
val orderId: String,
val productName: String,
val requestCurrency: String,
val requestAmount: BigDecimal,
val resultStatus: String,
val approvalDay: String,
val sign: String
)
data class PayverseWebhookResponse(val receiveResult: String)

View File

@@ -81,6 +81,73 @@ class ChargeService(
private val serverEnv: String
) {
@Transactional
fun payverseWebhook(request: PayverseWebhookRequest): Boolean {
val chargeId = request.orderId.toLongOrNull() ?: return false
val charge = chargeRepository.findByIdOrNull(chargeId) ?: return false
// 결제수단 확인
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
return false
}
// 결제 상태 분기 처리
return when (charge.payment?.status) {
PaymentStatus.REQUEST -> {
// 성공 조건 검증
val expectedSign = DigestUtils.sha512Hex(
String.format(
"||%s||%s||%s||%s||%s||",
payverseSecretKey,
payverseMid,
request.orderId,
request.requestAmount,
request.approvalDay
)
)
val isAmountMatch = request.requestAmount.compareTo(
BigDecimal.valueOf(charge.payment!!.price)
) == 0
val isSuccess = request.resultStatus == "SUCCESS" &&
request.mid == payverseMid &&
charge.title == request.productName &&
isAmountMatch &&
request.sign == expectedSign
if (isSuccess) {
// payverseVerify의 226~246 라인과 동일 처리
charge.payment?.receiptId = request.tid
charge.payment?.method = request.schemeCode
charge.payment?.status = PaymentStatus.COMPLETE
charge.payment?.locale = request.requestCurrency
val member = charge.member!!
member.charge(charge.chargeCan, charge.rewardCan, "pg")
applicationEventPublisher.publishEvent(
ChargeSpringEvent(
chargeId = charge.id!!,
memberId = member.id!!
)
)
true
} else {
false
}
}
PaymentStatus.COMPLETE -> {
// 이미 결제가 완료된 경우 성공 처리(idempotent)
true
}
else -> {
// 그 외 상태는 404
false
}
}
}
@Transactional
fun chargeByCoupon(couponNumber: String, member: Member): String {
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)

View File

@@ -18,6 +18,7 @@ payverse:
clientKey: ${PAYVERSE_CLIENT_KEY}
secretKey: ${PAYVERSE_SECRET_KEY}
host: ${PAYVERSE_HOST}
inboundIp: ${PAYVERSE_INBOUND_IP}
bootpay:
applicationId: ${BOOTPAY_APPLICATION_ID}