diff --git a/src/main/kotlin/roomescape/payment/business/PaymentService.kt b/src/main/kotlin/roomescape/payment/business/PaymentService.kt index a7343ef8..4f2d9c83 100644 --- a/src/main/kotlin/roomescape/payment/business/PaymentService.kt +++ b/src/main/kotlin/roomescape/payment/business/PaymentService.kt @@ -1,10 +1,9 @@ package roomescape.payment.business -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.exception.ErrorType -import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository @@ -24,44 +23,45 @@ class PaymentService( ) { @Transactional fun createPayment( - paymentResponse: PaymentApproveResponse, + approveResponse: PaymentApproveResponse, reservation: ReservationEntity - ): PaymentCreateResponse = PaymentEntity( - orderId = paymentResponse.orderId, - paymentKey = paymentResponse.paymentKey, - totalAmount = paymentResponse.totalAmount, - reservation = reservation, - approvedAt = paymentResponse.approvedAt - ).also { - paymentRepository.save(it) - }.toCreateResponse() + ): PaymentCreateResponse { + val payment = PaymentEntity( + orderId = approveResponse.orderId, + paymentKey = approveResponse.paymentKey, + totalAmount = approveResponse.totalAmount, + reservation = reservation, + approvedAt = approveResponse.approvedAt + ) + + return paymentRepository.save(payment).toCreateResponse() + } @Transactional(readOnly = true) - fun isReservationPaid( - reservationId: Long - ): Boolean = paymentRepository.existsByReservationId(reservationId) + fun isReservationPaid(reservationId: Long): Boolean = paymentRepository.existsByReservationId(reservationId) @Transactional fun createCanceledPayment( cancelInfo: PaymentCancelResponse, approvedAt: OffsetDateTime, paymentKey: String - ): CanceledPaymentEntity = CanceledPaymentEntity( - paymentKey = paymentKey, - cancelReason = cancelInfo.cancelReason, - cancelAmount = cancelInfo.cancelAmount, - approvedAt = approvedAt, - canceledAt = cancelInfo.canceledAt - ).also { canceledPaymentRepository.save(it) } + ): CanceledPaymentEntity { + val canceledPayment = CanceledPaymentEntity( + paymentKey = paymentKey, + cancelReason = cancelInfo.cancelReason, + cancelAmount = cancelInfo.cancelAmount, + approvedAt = approvedAt, + canceledAt = cancelInfo.canceledAt + ) + + return canceledPaymentRepository.save(canceledPayment) + } @Transactional fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest { val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId) - ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[reservationId: $reservationId]", - HttpStatus.NOT_FOUND - ) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) + // 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다. val canceled: CanceledPaymentEntity = cancelPayment(paymentKey) @@ -73,23 +73,19 @@ class PaymentService( cancelReason: String = "고객 요청", canceledAt: OffsetDateTime = OffsetDateTime.now() ): CanceledPaymentEntity { - val paymentEntity: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey) + val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey) ?.also { paymentRepository.delete(it) } - ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[paymentKey: $paymentKey]", - HttpStatus.NOT_FOUND - ) + ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - return CanceledPaymentEntity( + val canceledPayment = CanceledPaymentEntity( paymentKey = paymentKey, cancelReason = cancelReason, - cancelAmount = paymentEntity.totalAmount, - approvedAt = paymentEntity.approvedAt, + cancelAmount = payment.totalAmount, + approvedAt = payment.approvedAt, canceledAt = canceledAt - ).also { - canceledPaymentRepository.save(it) - } + ) + + return canceledPaymentRepository.save(canceledPayment) } @Transactional @@ -97,12 +93,8 @@ class PaymentService( paymentKey: String, canceledAt: OffsetDateTime ) { - canceledPaymentRepository.findByPaymentKey(paymentKey)?.let { - it.canceledAt = canceledAt - } ?: throw RoomescapeException( - ErrorType.PAYMENT_NOT_FOUND, - "[paymentKey: $paymentKey]", - HttpStatus.NOT_FOUND - ) + canceledPaymentRepository.findByPaymentKey(paymentKey) + ?.apply { this.canceledAt = canceledAt } + ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) } } diff --git a/src/main/kotlin/roomescape/payment/exception/PaymentErrorCode.kt b/src/main/kotlin/roomescape/payment/exception/PaymentErrorCode.kt new file mode 100644 index 00000000..1cad1ba4 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/exception/PaymentErrorCode.kt @@ -0,0 +1,16 @@ +package roomescape.payment.exception + +import org.springframework.http.HttpStatus +import roomescape.common.exception.ErrorCode + +enum class PaymentErrorCode( + override val httpStatus: HttpStatus, + override val errorCode: String, + override val message: String +) : ErrorCode { + PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P001", "결제 정보를 찾을 수 없어요."), + CANCELED_PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "P002", "취소된 결제 정보를 찾을 수 없어요."), + PAYMENT_CLIENT_ERROR(HttpStatus.BAD_REQUEST, "P003", "결제에 실패했어요. 결제 수단을 확인한 후 다시 시도해주세요."), + + PAYMENT_PROVIDER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "P999", "시스템에 일시적인 오류가 발생했어요. 잠시 후 다시 시도해주세요.") +} diff --git a/src/main/kotlin/roomescape/payment/exception/PaymentException.kt b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt new file mode 100644 index 00000000..44c59be0 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/exception/PaymentException.kt @@ -0,0 +1,8 @@ +package roomescape.payment.exception + +import roomescape.common.exception.RoomescapeExceptionV2 + +class PaymentException( + override val errorCode: PaymentErrorCode, + override val message: String = errorCode.message +) : RoomescapeExceptionV2(errorCode, message) diff --git a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt index a6ceaab2..d8dd1b7d 100644 --- a/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt +++ b/src/test/kotlin/roomescape/payment/business/PaymentServiceTest.kt @@ -11,6 +11,8 @@ import io.mockk.runs import org.springframework.http.HttpStatus import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.web.PaymentCancelRequest @@ -23,19 +25,15 @@ class PaymentServiceTest : FunSpec({ val paymentService = PaymentService(paymentRepository, canceledPaymentRepository) - context("cancelPaymentByAdmin") { + context("createCanceledPaymentByReservationId") { val reservationId = 1L test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") { every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.createCanceledPaymentByReservationId(reservationId) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } context("reservationId로 paymentKey를 찾고난 후") { @@ -50,14 +48,10 @@ class PaymentServiceTest : FunSpec({ paymentRepository.findByPaymentKey(paymentKey) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.createCanceledPaymentByReservationId(reservationId) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") { @@ -76,6 +70,7 @@ class PaymentServiceTest : FunSpec({ } returns PaymentFixture.createCanceled( id = 1L, paymentKey = paymentKey, + cancelReason = "Test", cancelAmount = paymentEntity.totalAmount, ) @@ -84,7 +79,7 @@ class PaymentServiceTest : FunSpec({ assertSoftly(result) { this.paymentKey shouldBe paymentKey this.amount shouldBe paymentEntity.totalAmount - this.cancelReason shouldBe "고객 요청" + this.cancelReason shouldBe "Test" } } } @@ -99,14 +94,10 @@ class PaymentServiceTest : FunSpec({ canceledPaymentRepository.findByPaymentKey(paymentKey) } returns null - val exception = shouldThrow { + val exception = shouldThrow { paymentService.updateCanceledTime(paymentKey, canceledAt) } - - assertSoftly(exception) { - this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND - this.httpStatus shouldBe HttpStatus.NOT_FOUND - } + exception.errorCode shouldBe PaymentErrorCode.PAYMENT_NOT_FOUND } test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {