diff --git a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt index 99794588..0f5ff705 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/payment/business/PaymentService.kt @@ -8,9 +8,11 @@ import com.sangdol.roomescape.payment.exception.PaymentErrorCode import com.sangdol.roomescape.payment.exception.PaymentException import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient import com.sangdol.roomescape.payment.infrastructure.persistence.* +import com.sangdol.roomescape.payment.mapper.toEvent import com.sangdol.roomescape.payment.mapper.toResponse import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -24,12 +26,14 @@ class PaymentService( private val canceledPaymentRepository: CanceledPaymentRepository, private val paymentWriter: PaymentWriter, private val transactionExecutionUtil: TransactionExecutionUtil, + private val eventPublisher: ApplicationEventPublisher ) { - fun requestConfirm(request: PaymentConfirmRequest): PaymentGatewayResponse { + fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse { log.info { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" } try { return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also { - log.info { "[requestConfirm] 결제 완료: paymentKey=${request.paymentKey}" } + eventPublisher.publishEvent(it.toEvent(reservationId)) + log.info { "[requestConfirm] 결제 및 이벤트 발행 완료: paymentKey=${request.paymentKey}" } } } catch (e: Exception) { when(e) { diff --git a/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentServiceTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentServiceTest.kt new file mode 100644 index 00000000..c1a20307 --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/payment/PaymentServiceTest.kt @@ -0,0 +1,125 @@ +package com.sangdol.roomescape.payment + +import com.ninjasquad.springmockk.MockkBean +import com.sangdol.roomescape.payment.business.PaymentService +import com.sangdol.roomescape.payment.business.domain.PaymentMethod +import com.sangdol.roomescape.payment.business.domain.UserFacingPaymentErrorCode +import com.sangdol.roomescape.payment.business.event.PaymentEvent +import com.sangdol.roomescape.payment.business.event.PaymentEventListener +import com.sangdol.roomescape.payment.exception.ExternalPaymentException +import com.sangdol.roomescape.payment.exception.PaymentErrorCode +import com.sangdol.roomescape.payment.exception.PaymentException +import com.sangdol.roomescape.payment.infrastructure.client.TosspayClient +import com.sangdol.roomescape.supports.FunSpecSpringbootTest +import com.sangdol.roomescape.supports.PaymentFixture +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import io.mockk.* +import org.junit.jupiter.api.assertThrows + +class PaymentServiceTest( + private val paymentService: PaymentService, + @MockkBean(relaxed = true) private val paymentEventListener: PaymentEventListener, + @MockkBean(relaxed = true) private val tosspayClient: TosspayClient +) : FunSpecSpringbootTest() { + + init { + afterTest { + clearAllMocks() + } + + context("결제를 승인한다.") { + val request = PaymentFixture.confirmRequest + + test("결제 정상 승인 및 이벤트 발행 확인") { + val tosspayAPIResponse = PaymentFixture.confirmResponse( + paymentKey = request.paymentKey, + amount = request.amount, + orderId = request.orderId, + method = PaymentMethod.CARD + ) + + val paymentEventSlot = slot() + + every { + tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) + } returns tosspayAPIResponse + + every { + paymentEventListener.handlePaymentEvent(capture(paymentEventSlot)) + } just runs + + paymentService.requestConfirm(12345L, request) + + assertSoftly(paymentEventSlot.captured) { + this.paymentKey shouldBe request.paymentKey + this.orderId shouldBe request.orderId + this.totalAmount shouldBe request.amount + this.method shouldBe PaymentMethod.CARD + } + } + + context("외부 API 요청 과정에서 예외가 발생하면, 예외를 PaymentException으로 변환한 뒤 던진다.") { + + test("외부 API가 4xx 응답을 보내면 ${PaymentErrorCode.PAYMENT_CLIENT_ERROR}로 변환하여 예외를 던진다.") { + val expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR + val exception = ExternalPaymentException(400, "INVALID_REQUEST", "잘못된 요청입니다.") + every { + tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) + } throws exception + + assertThrows { + paymentService.requestConfirm(12345L, request) + }.also { + it.errorCode shouldBe expectedErrorCode + it.message shouldBe expectedErrorCode.message + } + } + + test("외부 API가 5xx 응답을 보내면 ${PaymentErrorCode.PAYMENT_PROVIDER_ERROR}로 변환하여 예외를 던진다.") { + val expectedErrorCode = PaymentErrorCode.PAYMENT_PROVIDER_ERROR + val exception = ExternalPaymentException(500, "UNKNOWN_PAYMENT_ERROR", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요.") + every { + tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) + } throws exception + + assertThrows { + paymentService.requestConfirm(12345L, request) + }.also { + it.errorCode shouldBe expectedErrorCode + it.message shouldBe expectedErrorCode.message + } + } + + test("외부 API의 에러코드가 ${UserFacingPaymentErrorCode::class.simpleName}에 있으면 해당 예외 메시지를 담아 던진다.") { + val expectedErrorCode = PaymentErrorCode.PAYMENT_CLIENT_ERROR + val exception = ExternalPaymentException(400, "EXCEED_MAX_CARD_INSTALLMENT_PLAN", "설정 가능한 최대 할부 개월 수를 초과했습니다.") + every { + tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) + } throws exception + + assertThrows { + paymentService.requestConfirm(12345L, request) + }.also { + it.errorCode shouldBe expectedErrorCode + it.message shouldBe "${expectedErrorCode.message}(${exception.message})" + } + } + + test("외부 API에서 예상치 못한 예외가 발생한 경우 ${PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR}로 변환한다.") { + val expectedErrorCode = PaymentErrorCode.PAYMENT_UNEXPECTED_ERROR + every { + tosspayClient.confirm(request.paymentKey, request.orderId, request.amount) + } throws Exception("unexpected") + + assertThrows { + paymentService.requestConfirm(12345L, request) + }.also { + it.errorCode shouldBe expectedErrorCode + it.message shouldBe expectedErrorCode.message + } + } + } + } + } +}