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.HttpRequest import org.springframework.http.HttpStatus 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.RestClient import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.payment.web.PaymentApprove import roomescape.payment.web.PaymentCancel import java.io.IOException import java.util.Map @Component class TossPaymentClient( private val log: KLogger = KotlinLogging.logger {}, private val objectMapper: ObjectMapper, tossPaymentClientBuilder: RestClient.Builder, ) { companion object { private const val CONFIRM_URL: String = "/v1/payments/confirm" private const val CANCEL_URL: String = "/v1/payments/{paymentKey}/cancel" } private val tossPaymentClient: RestClient = tossPaymentClientBuilder.build() fun confirmPayment(paymentRequest: PaymentApprove.Request): PaymentApprove.Response { logPaymentInfo(paymentRequest) return tossPaymentClient.post() .uri(CONFIRM_URL) .contentType(MediaType.APPLICATION_JSON) .body(paymentRequest) .retrieve() .onStatus( { status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError }, { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } ) .body(PaymentApprove.Response::class.java) ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) } fun cancelPayment(cancelRequest: PaymentCancel.Request): PaymentCancel.Response { logPaymentCancelInfo(cancelRequest) val param = Map.of("cancelReason", cancelRequest.cancelReason) return tossPaymentClient.post() .uri(CANCEL_URL, cancelRequest.paymentKey) .contentType(MediaType.APPLICATION_JSON) .body(param) .retrieve() .onStatus( { status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError }, { req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) } ) .body(PaymentCancel.Response::class.java) ?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) } private fun logPaymentInfo(paymentRequest: PaymentApprove.Request) { log.info { "결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " + "amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}" } } private fun logPaymentCancelInfo(cancelRequest: PaymentCancel.Request) { log.info { "결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " + "cancelReason=${cancelRequest.cancelReason}" } } @Throws(IOException::class) private fun handlePaymentError( res: ClientHttpResponse ): Nothing { val statusCode = res.statusCode val errorType = getErrorTypeByStatusCode(statusCode) val errorResponse = getErrorResponse(res) throw RoomescapeException( errorType, "[ErrorCode = ${errorResponse.code}, ErrorMessage = ${errorResponse.message}]", statusCode ) } @Throws(IOException::class) private fun getErrorResponse( res: ClientHttpResponse ): TossPaymentErrorResponse { val body = res.body val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java) body.close() return errorResponse } private fun getErrorTypeByStatusCode( statusCode: HttpStatusCode ): ErrorType { if (statusCode.is4xxClientError) { return ErrorType.PAYMENT_ERROR } return ErrorType.PAYMENT_SERVER_ERROR } }