generated from pricelees/issue-pr-template
168 lines
5.5 KiB
Kotlin
168 lines
5.5 KiB
Kotlin
package roomescape.payment.infrastructure.client
|
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper
|
|
import io.github.oshai.kotlinlogging.KLogger
|
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
import org.springframework.http.HttpMethod
|
|
import org.springframework.http.HttpStatusCode
|
|
import org.springframework.http.MediaType
|
|
import org.springframework.http.client.ClientHttpResponse
|
|
import org.springframework.stereotype.Component
|
|
import org.springframework.web.client.ResponseErrorHandler
|
|
import org.springframework.web.client.RestClient
|
|
import roomescape.payment.exception.PaymentErrorCode
|
|
import roomescape.payment.exception.PaymentException
|
|
import java.net.URI
|
|
|
|
private val log: KLogger = KotlinLogging.logger {}
|
|
|
|
@Component
|
|
class TosspayClient(
|
|
objectMapper: ObjectMapper,
|
|
tosspayClientBuilder: RestClient.Builder
|
|
) {
|
|
private val confirmClient = ConfirmClient(objectMapper, tosspayClientBuilder.build())
|
|
private val cancelClient = CancelClient(objectMapper, tosspayClientBuilder.build())
|
|
|
|
fun confirm(
|
|
paymentKey: String,
|
|
orderId: String,
|
|
amount: Int,
|
|
): PaymentClientConfirmResponse {
|
|
log.info { "[TosspayClient.confirm] 결제 승인 요청: paymentKey=$paymentKey, orderId=$orderId, amount=$amount" }
|
|
|
|
return confirmClient.request(paymentKey, orderId, amount)
|
|
.also {
|
|
log.info { "[TosspayClient.confirm] 결제 승인 완료: response=$it" }
|
|
}
|
|
|
|
}
|
|
|
|
fun cancel(
|
|
paymentKey: String,
|
|
amount: Int,
|
|
cancelReason: String
|
|
): PaymentClientCancelResponse {
|
|
log.info { "[TosspayClient.cancel] 결제 취소 요청: paymentKey=$paymentKey, amount=$amount, cancelReason=$cancelReason" }
|
|
|
|
return cancelClient.request(paymentKey, amount, cancelReason).also {
|
|
log.info { "[TosspayClient.cancel] 결제 취소 완료: response=$it" }
|
|
}
|
|
}
|
|
}
|
|
|
|
private class ConfirmClient(
|
|
private val objectMapper: ObjectMapper,
|
|
private val client: RestClient,
|
|
) {
|
|
companion object {
|
|
private const val CONFIRM_URI: String = "/v1/payments/confirm"
|
|
}
|
|
|
|
private val errorHandler: TosspayErrorHandler = TosspayErrorHandler(objectMapper)
|
|
|
|
fun request(paymentKey: String, orderId: String, amount: Int): PaymentClientConfirmResponse {
|
|
val response = client.post()
|
|
.uri(CONFIRM_URI)
|
|
.contentType(MediaType.APPLICATION_JSON)
|
|
.body(
|
|
mapOf(
|
|
"paymentKey" to paymentKey,
|
|
"orderId" to orderId,
|
|
"amount" to amount
|
|
)
|
|
)
|
|
.retrieve()
|
|
.onStatus(errorHandler)
|
|
.body(String::class.java)
|
|
?: run {
|
|
log.error { "[TosspayClient] 응답 바디 변환 실패" }
|
|
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
|
|
}
|
|
|
|
log.debug { "[TosspayClient.confirm] 응답 수신: json = $response" }
|
|
|
|
return objectMapper.readValue(response, PaymentClientConfirmResponse::class.java)
|
|
}
|
|
}
|
|
|
|
private class CancelClient(
|
|
private val objectMapper: ObjectMapper,
|
|
private val client: RestClient,
|
|
) {
|
|
companion object {
|
|
private const val CANCEL_URI: String = "/v1/payments/{paymentKey}/cancel"
|
|
}
|
|
|
|
private val errorHandler: TosspayErrorHandler = TosspayErrorHandler(objectMapper)
|
|
|
|
fun request(
|
|
paymentKey: String,
|
|
amount: Int,
|
|
cancelReason: String
|
|
): PaymentClientCancelResponse {
|
|
val response = client.post()
|
|
.uri(CANCEL_URI, paymentKey)
|
|
.body(
|
|
mapOf(
|
|
"cancelReason" to cancelReason,
|
|
"cancelAmount" to amount,
|
|
)
|
|
)
|
|
.retrieve()
|
|
.onStatus(errorHandler)
|
|
.body(String::class.java)
|
|
?: run {
|
|
log.error { "[TosspayClient] 응답 바디 변환 실패" }
|
|
throw PaymentException(PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR)
|
|
}
|
|
|
|
log.debug { "[TosspayClient.cancel] 응답 수신: json = $response" }
|
|
return objectMapper.readValue(response, PaymentClientCancelResponse::class.java)
|
|
}
|
|
}
|
|
|
|
private class TosspayErrorHandler(
|
|
private val objectMapper: ObjectMapper
|
|
) : ResponseErrorHandler {
|
|
override fun hasError(response: ClientHttpResponse): Boolean {
|
|
val statusCode: HttpStatusCode = response.statusCode
|
|
|
|
return statusCode.is4xxClientError || statusCode.is5xxServerError
|
|
}
|
|
|
|
override fun handleError(
|
|
url: URI,
|
|
method: HttpMethod,
|
|
response: ClientHttpResponse
|
|
): Nothing {
|
|
val requestType: String = paymentRequestType(url)
|
|
log.warn { "[TosspayClient] $requestType 요청 실패: response: ${parseResponse(response)}" }
|
|
|
|
throw PaymentException(paymentErrorCode(response.statusCode))
|
|
}
|
|
|
|
private fun paymentRequestType(url: URI): String {
|
|
val type = url.path.split("/").last()
|
|
|
|
if (type == "cancel") {
|
|
return "취소"
|
|
}
|
|
return "승인"
|
|
}
|
|
|
|
private fun paymentErrorCode(statusCode: HttpStatusCode) = if (statusCode.is4xxClientError) {
|
|
PaymentErrorCode.PAYMENT_CLIENT_ERROR
|
|
} else {
|
|
PaymentErrorCode.PAYMENT_PROVIDER_ERROR
|
|
}
|
|
|
|
private fun parseResponse(response: ClientHttpResponse): TosspayErrorResponse {
|
|
val body = response.body
|
|
|
|
return objectMapper.readValue(body, TosspayErrorResponse::class.java).also {
|
|
body.close()
|
|
}
|
|
}
|
|
}
|