refactor: PaymentService에서의 예외 처리 방법 변경에 따른 별도의 ExceptionHandler 추가 및 테스트 보완

This commit is contained in:
이상진 2025-10-07 22:17:46 +09:00
parent 979623a670
commit c4cd168175
2 changed files with 405 additions and 378 deletions

View File

@ -0,0 +1,26 @@
package com.sangdol.roomescape.order.infrastructure.persistence
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
interface PaymentAttemptRepository: JpaRepository<PaymentAttemptEntity, Long> {
fun countByReservationId(reservationId: Long): Long
@Query(
"""
SELECT
CASE
WHEN COUNT(pa) > 0
THEN TRUE
ELSE FALSE
END
FROM
PaymentAttemptEntity pa
WHERE
pa.reservationId = :reservationId
AND pa.result = com.sangdol.roomescape.order.infrastructure.persistence.AttemptResult.SUCCESS
"""
)
fun isSuccessAttemptExists(reservationId: Long): Boolean
}

View File

@ -1,378 +1,379 @@
//package com.sangdol.roomescape.payment package com.sangdol.roomescape.payment
//
//import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.MockkBean
//import com.sangdol.common.types.web.HttpStatus import com.sangdol.common.types.web.HttpStatus
//import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
//import com.sangdol.roomescape.payment.business.PaymentService import com.sangdol.roomescape.payment.business.PaymentService
//import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.business.domain.*
//import com.sangdol.roomescape.payment.infrastructure.client.CardDetail import com.sangdol.roomescape.payment.dto.*
//import com.sangdol.roomescape.payment.infrastructure.client.EasyPayDetail import com.sangdol.roomescape.payment.exception.ExternalPaymentException
//import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.exception.PaymentErrorCode
//import com.sangdol.roomescape.payment.infrastructure.client.TransferDetail import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient
//import com.sangdol.roomescape.payment.infrastructure.common.* import com.sangdol.roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
//import com.sangdol.roomescape.payment.infrastructure.persistence.* import com.sangdol.roomescape.payment.infrastructure.persistence.PaymentRepository
//import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest import com.sangdol.roomescape.supports.FunSpecSpringbootTest
//import com.sangdol.roomescape.payment.web.PaymentCreateResponse import com.sangdol.roomescape.supports.PaymentFixture
//import com.sangdol.roomescape.supports.* import com.sangdol.roomescape.supports.runExceptionTest
//import io.kotest.matchers.shouldBe import com.sangdol.roomescape.supports.runTest
//import io.mockk.every import io.kotest.matchers.shouldBe
//import org.springframework.data.repository.findByIdOrNull import io.mockk.clearMocks
//import org.springframework.http.HttpMethod import io.mockk.every
// import org.hamcrest.CoreMatchers.containsString
//class PaymentAPITest( import org.hamcrest.CoreMatchers.equalTo
// @MockkBean import org.springframework.data.repository.findByIdOrNull
// private val tosspayClient: TosspayClient, import org.springframework.http.HttpMethod
// private val paymentService: PaymentService,
// private val paymentRepository: PaymentRepository, class PaymentAPITest(
// private val paymentDetailRepository: PaymentDetailRepository, @MockkBean
// private val canceledPaymentRepository: CanceledPaymentRepository private val tosspayClient: TosspayClient,
//) : FunSpecSpringbootTest() { private val paymentService: PaymentService,
// init { private val paymentRepository: PaymentRepository,
// context("결제를 승인한다.") { private val canceledPaymentRepository: CanceledPaymentRepository
// context("권한이 없으면 접근할 수 없다.") { ) : FunSpecSpringbootTest() {
// val endpoint = "/payments?reservationId=$INVALID_PK" init {
// context("결제를 승인한다.") {
// test("비회원") { context("권한이 없으면 접근할 수 없다.") {
// runExceptionTest( val endpoint = "/payments/confirm"
// method = HttpMethod.POST,
// endpoint = endpoint, test("비회원") {
// expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND runExceptionTest(
// ) method = HttpMethod.POST,
// } endpoint = endpoint,
// expectedErrorCode = AuthErrorCode.TOKEN_NOT_FOUND
// test("관리자") { )
// runExceptionTest( }
// token = testAuthUtil.defaultHqAdminLogin().second,
// method = HttpMethod.POST, test("관리자") {
// endpoint = endpoint, runExceptionTest(
// expectedErrorCode = AuthErrorCode.ACCESS_DENIED token = testAuthUtil.defaultHqAdminLogin().second,
// ) method = HttpMethod.POST,
// } endpoint = endpoint,
// } expectedErrorCode = AuthErrorCode.ACCESS_DENIED
// )
// val amount = 100_000 }
// context("간편결제 + 카드로 ${amount}원을 결제한다.") { }
// context("일시불") {
// test("토스페이 + 토스뱅크카드(신용)") { val amount = 100_000
// runConfirmTest( context("간편결제 + 카드로 ${amount}원을 결제한다.") {
// amount = amount, context("일시불") {
// cardDetail = PaymentFixture.cardDetail( test("토스페이 + 토스뱅크카드(신용)") {
// amount = amount, runConfirmTest(
// issuerCode = CardIssuerCode.TOSS_BANK, amount = amount,
// cardType = CardType.CREDIT, cardDetail = PaymentFixture.cardDetail(
// ), amount = amount,
// easyPayDetail = PaymentFixture.easypayDetail( issuerCode = CardIssuerCode.TOSS_BANK,
// amount = 0, cardType = CardType.CREDIT,
// provider = EasyPayCompanyCode.TOSSPAY ),
// ) easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = 0,
// } provider = EasyPayCompanyCode.TOSSPAY
// )
// test("삼성페이 + 삼성카드(법인)") { )
// runConfirmTest( }
// amount = amount,
// cardDetail = PaymentFixture.cardDetail( test("삼성페이 + 삼성카드(법인)") {
// amount = amount, runConfirmTest(
// issuerCode = CardIssuerCode.SAMSUNG, amount = amount,
// cardType = CardType.CREDIT, cardDetail = PaymentFixture.cardDetail(
// ownerType = CardOwnerType.CORPORATE amount = amount,
// ), issuerCode = CardIssuerCode.SAMSUNG,
// easyPayDetail = PaymentFixture.easypayDetail( cardType = CardType.CREDIT,
// amount = 0, ownerType = CardOwnerType.CORPORATE
// provider = EasyPayCompanyCode.SAMSUNGPAY ),
// ) easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = 0,
// } provider = EasyPayCompanyCode.SAMSUNGPAY
// } )
// )
// context("할부") { }
// val installmentPlanMonths = 12 }
// test("네이버페이 + 신한카드 / 12개월") {
// runConfirmTest( context("할부") {
// amount = amount, val installmentPlanMonths = 12
// cardDetail = PaymentFixture.cardDetail( test("네이버페이 + 신한카드 / 12개월") {
// amount = amount, runConfirmTest(
// issuerCode = CardIssuerCode.SHINHAN, amount = amount,
// installmentPlanMonths = installmentPlanMonths cardDetail = PaymentFixture.cardDetail(
// ), amount = amount,
// easyPayDetail = PaymentFixture.easypayDetail( issuerCode = CardIssuerCode.SHINHAN,
// amount = 0, installmentPlanMonths = installmentPlanMonths
// provider = EasyPayCompanyCode.NAVERPAY ),
// ) easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = 0,
// } provider = EasyPayCompanyCode.NAVERPAY
// } )
// )
// context("간편결제사 포인트 일부 사용") { }
// val point = (amount * 0.1).toInt() }
// test("토스페이 + 국민카드(체크) / 일시불 / $point 포인트 사용") {
// runConfirmTest( context("간편결제사 포인트 일부 사용") {
// amount = amount, val point = (amount * 0.1).toInt()
// cardDetail = PaymentFixture.cardDetail( test("토스페이 + 국민카드(체크) / 일시불 / $point 포인트 사용") {
// amount = (amount - point), runConfirmTest(
// issuerCode = CardIssuerCode.KOOKMIN, amount = amount,
// cardType = CardType.CHECK cardDetail = PaymentFixture.cardDetail(
// ), amount = (amount - point),
// easyPayDetail = PaymentFixture.easypayDetail( issuerCode = CardIssuerCode.KOOKMIN,
// amount = 0, cardType = CardType.CHECK
// provider = EasyPayCompanyCode.TOSSPAY, ),
// discountAmount = point easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = 0,
// ) provider = EasyPayCompanyCode.TOSSPAY,
// } discountAmount = point
// } )
// } )
// }
// context("간편결제사의 선불 충전금액으로 ${amount}원을 결제한다.") { }
// test("토스페이 + 토스페이머니 / 전액") { }
// runConfirmTest(
// easyPayDetail = PaymentFixture.easypayDetail( context("간편결제사의 선불 충전금액으로 ${amount}원을 결제한다.") {
// amount = amount, test("토스페이 + 토스페이머니 / 전액") {
// provider = EasyPayCompanyCode.TOSSPAY runConfirmTest(
// ) easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = amount,
// } provider = EasyPayCompanyCode.TOSSPAY
// )
// val point = (amount * 0.05).toInt() )
// }
// test("카카오페이 + 카카오페이머니 / $point 사용") {
// runConfirmTest( val point = (amount * 0.05).toInt()
// easyPayDetail = PaymentFixture.easypayDetail(
// amount = (amount - point), test("카카오페이 + 카카오페이머니 / $point 사용") {
// provider = EasyPayCompanyCode.KAKAOPAY, runConfirmTest(
// discountAmount = point easyPayDetail = PaymentFixture.easypayDetail(
// ) amount = (amount - point),
// ) provider = EasyPayCompanyCode.KAKAOPAY,
// } discountAmount = point
// } )
// )
// context("계좌이체로 결제한다.") { }
// test("토스뱅크") { }
// runConfirmTest(
// transferDetail = PaymentFixture.transferDetail(bankCode = BankCode.TOSS_BANK) context("계좌이체로 결제한다.") {
// ) test("토스뱅크") {
// } runConfirmTest(
// } transferDetail = PaymentFixture.transferDetail(bankCode = BankCode.TOSS_BANK)
// )
// context("지원하지 않는 결제수단으로 요청시 실패한다.") { }
// val supportedMethod = listOf( }
// PaymentMethod.CARD,
// PaymentMethod.EASY_PAY, context("결제 처리중 오류가 발생한다.") {
// PaymentMethod.TRANSFER, lateinit var token: String
// ) val commonRequest = PaymentFixture.confirmRequest
//
// PaymentMethod.entries.filter { it !in supportedMethod }.forEach { beforeTest {
// test("결제 수단: ${it.koreanName}") { token = testAuthUtil.defaultUserLogin().second
// val (user, token) = testAuthUtil.defaultUserLogin() }
// val reservation = dummyInitializer.createConfirmReservation(user = user)
// afterTest {
// val request = PaymentFixture.confirmRequest clearMocks(tosspayClient)
// }
// every {
// tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) test("예외 코드가 UserFacingPaymentErrorCode에 있으면 결제 실패 메시지를 같이 담는다.") {
// } returns PaymentFixture.confirmResponse( val statusCode = HttpStatus.BAD_REQUEST.value()
// paymentKey = request.paymentKey, val message = "거래금액 한도를 초과했습니다."
// amount = request.amount,
// method = it, every {
// cardDetail = null, tosspayClient.confirm(commonRequest.paymentKey, commonRequest.orderId, commonRequest.amount)
// easyPayDetail = null, } throws ExternalPaymentException(
// transferDetail = null, httpStatusCode = statusCode,
// ) errorCode = UserFacingPaymentErrorCode.EXCEED_MAX_AMOUNT.name,
// message = message
// runExceptionTest( )
// token = token,
// method = HttpMethod.POST, runTest(
// endpoint = "/payments?reservationId=${reservation.id}", token = token,
// requestBody = PaymentFixture.confirmRequest, using = {
// expectedErrorCode = PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE body(commonRequest)
// ) },
// } on = {
// } post("/payments/confirm")
// } },
// } expect = {
// statusCode(statusCode)
// context("결제를 취소한다.") { body("code", equalTo(PaymentErrorCode.PAYMENT_CLIENT_ERROR.errorCode))
// context("권한이 없으면 접근할 수 없다.") { body("message", containsString(message))
// val endpoint = "/payments/cancel" }
// )
// test("비회원") { }
// runExceptionTest(
// method = HttpMethod.POST, context("예외 코드가 UserFacingPaymentErrorCode에 없으면 Client의 상태 코드에 따라 다르게 처리한다.") {
// endpoint = endpoint, mapOf(
// requestBody = PaymentFixture.cancelRequest, HttpStatus.BAD_REQUEST.value() to PaymentErrorCode.PAYMENT_CLIENT_ERROR,
// expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND HttpStatus.INTERNAL_SERVER_ERROR.value() to PaymentErrorCode.PAYMENT_PROVIDER_ERROR
// ) ).forEach { (statusCode, expectedErrorCode) ->
// } test("statusCode=${statusCode}") {
// val message = "잘못된 시크릿키 연동 정보 입니다."
// test("관리자") {
// runExceptionTest( every {
// token = testAuthUtil.defaultHqAdminLogin().second, tosspayClient.confirm(commonRequest.paymentKey, commonRequest.orderId, commonRequest.amount)
// method = HttpMethod.POST, } throws ExternalPaymentException(
// endpoint = endpoint, httpStatusCode = statusCode,
// requestBody = PaymentFixture.cancelRequest, errorCode = "INVALID_API_KEY",
// expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND message = message
// ) )
// }
// } runTest(
// token = token,
// test("정상 취소") { using = {
// val (user, token) = testAuthUtil.defaultUserLogin() body(commonRequest)
// val reservation = dummyInitializer.createConfirmReservation(user = user) },
// val confirmRequest = PaymentFixture.confirmRequest on = {
// post("/payments/confirm")
// val paymentCreateResponse = createPayment( },
// request = confirmRequest, expect = {
// reservationId = reservation.id statusCode(statusCode)
// ) body("code", equalTo(expectedErrorCode.errorCode))
// body("message", equalTo(expectedErrorCode.message))
// every { }
// tosspayClient.cancel( )
// confirmRequest.paymentKey, }
// confirmRequest.amount, }
// cancelReason = "cancelReason" }
// ) }
// } returns PaymentFixture.cancelResponse(confirmRequest.amount) }
//
// val requestBody = PaymentFixture.cancelRequest.copy(reservationId = reservation.id) context("결제를 취소한다.") {
// context("권한이 없으면 접근할 수 없다.") {
// runTest( val endpoint = "/payments/cancel"
// token = token,
// using = { test("비회원") {
// body(requestBody) runExceptionTest(
// }, method = HttpMethod.POST,
// on = { endpoint = endpoint,
// post("/payments/cancel") requestBody = PaymentFixture.cancelRequest,
// }, expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
// expect = { )
// statusCode(HttpStatus.OK.value()) }
// }
// ).also { test("관리자") {
// val payment = paymentRepository.findByIdOrNull(paymentCreateResponse.paymentId) runExceptionTest(
// ?: throw AssertionError("Unexpected Exception Occurred.") token = testAuthUtil.defaultHqAdminLogin().second,
// val canceledPayment = method = HttpMethod.POST,
// canceledPaymentRepository.findByPaymentId(paymentCreateResponse.paymentId) endpoint = endpoint,
// ?: throw AssertionError("Unexpected Exception Occurred.") requestBody = PaymentFixture.cancelRequest,
// expectedErrorCode = AuthErrorCode.MEMBER_NOT_FOUND
// payment.status shouldBe PaymentStatus.CANCELED )
// canceledPayment.paymentId shouldBe payment.id }
// canceledPayment.cancelAmount shouldBe payment.totalAmount }
// }
// } test("정상 취소") {
// val (user, token) = testAuthUtil.defaultUserLogin()
// test("예약에 대한 결제 정보가 없으면 실패한다.") { val reservation = dummyInitializer.createConfirmReservation(user = user)
// val (user, token) = testAuthUtil.defaultUserLogin() val confirmRequest = PaymentFixture.confirmRequest
// val reservation = dummyInitializer.createConfirmReservation(user = user)
// val paymentCreateResponse = createPayment(
// runExceptionTest( request = confirmRequest,
// token = token, reservationId = reservation.id
// method = HttpMethod.POST, )
// endpoint = "/payments/cancel",
// requestBody = PaymentFixture.cancelRequest.copy(reservationId = reservation.id), every {
// expectedErrorCode = PaymentErrorCode.PAYMENT_NOT_FOUND tosspayClient.cancel(
// ) confirmRequest.paymentKey,
// } confirmRequest.amount,
// } cancelReason = "cancelReason"
// } )
// } returns PaymentFixture.cancelResponse(confirmRequest.amount)
// private fun createPayment(
// request: PaymentConfirmRequest, val requestBody = PaymentFixture.cancelRequest.copy(reservationId = reservation.id)
// reservationId: Long,
// ): PaymentCreateResponse { runTest(
// every { token = token,
// tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) using = {
// } returns PaymentFixture.confirmResponse( body(requestBody)
// request.paymentKey, },
// request.amount, on = {
// method = PaymentMethod.CARD, post("/payments/cancel")
// cardDetail = PaymentFixture.cardDetail(request.amount), },
// easyPayDetail = null, expect = {
// transferDetail = null, statusCode(HttpStatus.OK.value())
// ) }
// ).also {
// return paymentService.confirm(reservationId, request) val payment = paymentRepository.findByIdOrNull(paymentCreateResponse.paymentId)
// } ?: throw AssertionError("Unexpected Exception Occurred.")
// val canceledPayment =
// fun runConfirmTest( canceledPaymentRepository.findByPaymentId(paymentCreateResponse.paymentId)
// cardDetail: CardDetail? = null, ?: throw AssertionError("Unexpected Exception Occurred.")
// easyPayDetail: EasyPayDetail? = null,
// transferDetail: TransferDetail? = null, payment.status shouldBe PaymentStatus.CANCELED
// paymentKey: String = "paymentKey", canceledPayment.paymentId shouldBe payment.id
// amount: Int = 10000, canceledPayment.cancelAmount shouldBe payment.totalAmount
// ) { }
// val (user, token) = testAuthUtil.defaultUserLogin() }
// val reservation = dummyInitializer.createConfirmReservation(user = user)
// val request = PaymentFixture.confirmRequest.copy(paymentKey = paymentKey, amount = amount) test("예약에 대한 결제 정보가 없으면 실패한다.") {
// val (user, token) = testAuthUtil.defaultUserLogin()
// val method = if (easyPayDetail != null) { val reservation = dummyInitializer.createConfirmReservation(user = user)
// PaymentMethod.EASY_PAY
// } else if (cardDetail != null) { runExceptionTest(
// PaymentMethod.CARD token = token,
// } else if (transferDetail != null) { method = HttpMethod.POST,
// PaymentMethod.TRANSFER endpoint = "/payments/cancel",
// } else { requestBody = PaymentFixture.cancelRequest.copy(reservationId = reservation.id),
// throw AssertionError("결제타입 확인 필요.") expectedErrorCode = PaymentErrorCode.PAYMENT_NOT_FOUND
// } )
// }
// val clientResponse = PaymentFixture.confirmResponse( }
// paymentKey, amount, method, cardDetail, easyPayDetail, transferDetail }
// )
// private fun createPayment(
// every { request: PaymentConfirmRequest,
// tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) reservationId: Long,
// } returns clientResponse ): PaymentCreateResponse {
// every {
// runTest( tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
// token = token, } returns PaymentFixture.confirmResponse(
// using = { request.paymentKey,
// body(request) request.amount,
// }, method = PaymentMethod.CARD,
// on = { cardDetail = PaymentFixture.cardDetail(request.amount),
// post("/payments?reservationId=${reservation.id}") easyPayDetail = null,
// }, transferDetail = null,
// expect = { )
// statusCode(HttpStatus.OK.value())
// } val paymentResponse = paymentService.requestConfirm(request)
// ).also { return paymentService.savePayment(reservationId, paymentResponse)
// val createdPayment = paymentRepository.findByIdOrNull(it.extract().path("data.paymentId")) }
// ?: throw AssertionError("Unexpected Exception Occurred.")
// val createdPaymentDetail = fun runConfirmTest(
// paymentDetailRepository.findByIdOrNull(it.extract().path("data.detailId")) cardDetail: CardDetailResponse? = null,
// ?: throw AssertionError("Unexpected Exception Occurred.") easyPayDetail: EasyPayDetailResponse? = null,
// transferDetail: TransferDetailResponse? = null,
// createdPayment.status shouldBe clientResponse.status paymentKey: String = "paymentKey",
// createdPayment.method shouldBe clientResponse.method amount: Int = 10000,
// createdPayment.reservationId shouldBe reservation.id ) {
// val token = testAuthUtil.defaultUserLogin().second
// when (createdPaymentDetail) { val request = PaymentFixture.confirmRequest.copy(paymentKey = paymentKey, amount = amount)
// is PaymentCardDetailEntity -> {
// createdPaymentDetail.issuerCode shouldBe clientResponse.card!!.issuerCode val method = if (easyPayDetail != null) {
// createdPaymentDetail.cardType shouldBe clientResponse.card.cardType PaymentMethod.EASY_PAY
// createdPaymentDetail.cardNumber shouldBe clientResponse.card.number } else if (cardDetail != null) {
// createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount PaymentMethod.CARD
// createdPaymentDetail.vat shouldBe clientResponse.vat } else if (transferDetail != null) {
// createdPaymentDetail.amount shouldBe (clientResponse.totalAmount - (clientResponse.easyPay?.discountAmount PaymentMethod.TRANSFER
// ?: 0)) } else {
// clientResponse.easyPay?.let { easypay -> throw AssertionError("결제타입 확인 필요.")
// createdPaymentDetail.easypayProviderCode shouldBe easypay.provider }
// createdPaymentDetail.easypayDiscountAmount shouldBe easypay.discountAmount
// } val clientResponse = PaymentFixture.confirmResponse(
// } paymentKey, amount, method, cardDetail, easyPayDetail, transferDetail
// )
// is PaymentBankTransferDetailEntity -> {
// createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount every {
// createdPaymentDetail.vat shouldBe clientResponse.vat tosspayClient.confirm(request.paymentKey, request.orderId, request.amount)
// createdPaymentDetail.bankCode shouldBe clientResponse.transfer!!.bankCode } returns clientResponse
// createdPaymentDetail.settlementStatus shouldBe clientResponse.transfer.settlementStatus
// } runTest(
// token = token,
// is PaymentEasypayPrepaidDetailEntity -> { using = {
// createdPaymentDetail.suppliedAmount shouldBe clientResponse.suppliedAmount body(request)
// createdPaymentDetail.vat shouldBe clientResponse.vat },
// createdPaymentDetail.easypayProviderCode shouldBe clientResponse.easyPay!!.provider on = {
// createdPaymentDetail.amount shouldBe clientResponse.easyPay.amount post("/payments/confirm")
// createdPaymentDetail.discountAmount shouldBe clientResponse.easyPay.discountAmount },
// } expect = {
// } statusCode(HttpStatus.OK.value())
// } }
// } )
//} }
}