From b0b0eb6fcf98c460b127babb6228755e41acce90 Mon Sep 17 00:00:00 2001 From: pricelees Date: Mon, 18 Aug 2025 16:43:56 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20PaymentClient=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20Request=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20=EB=B0=8F=20Writer=EC=97=90=EC=84=9C?= =?UTF-8?q?=EC=9D=98=20Client=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/implement/PaymentRequester.kt | 21 ++++++ .../payment/implement/PaymentWriterV2.kt | 71 ++++--------------- .../ReservationWithPaymentServiceV2.kt | 59 +++++++-------- .../implement/ReservationValidator.kt | 22 ++++++ .../implement/ReservationWriter.kt | 20 +++++- 5 files changed, 108 insertions(+), 85 deletions(-) create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentRequester.kt diff --git a/src/main/kotlin/roomescape/payment/implement/PaymentRequester.kt b/src/main/kotlin/roomescape/payment/implement/PaymentRequester.kt new file mode 100644 index 00000000..bb34a34a --- /dev/null +++ b/src/main/kotlin/roomescape/payment/implement/PaymentRequester.kt @@ -0,0 +1,21 @@ +package roomescape.payment.implement + +import org.springframework.stereotype.Component +import roomescape.payment.infrastructure.client.v2.* + +@Component +class PaymentRequester( + private val client: TosspaymentClientV2 +) { + fun requestConfirmPayment(paymentKey: String, orderId: String, amount: Int): PaymentConfirmResponse { + val request = PaymentConfirmRequest(paymentKey, orderId, amount) + + return client.confirm(request) + } + + fun requestCancelPayment(paymentKey: String, amount: Int, cancelReason: String): PaymentCancelResponseV2 { + val request = PaymentCancelRequestV2(paymentKey, amount, cancelReason) + + return client.cancel(request) + } +} diff --git a/src/main/kotlin/roomescape/payment/implement/PaymentWriterV2.kt b/src/main/kotlin/roomescape/payment/implement/PaymentWriterV2.kt index b3aff378..3def90da 100644 --- a/src/main/kotlin/roomescape/payment/implement/PaymentWriterV2.kt +++ b/src/main/kotlin/roomescape/payment/implement/PaymentWriterV2.kt @@ -10,33 +10,18 @@ import roomescape.payment.exception.PaymentException import roomescape.payment.infrastructure.client.v2.* import roomescape.payment.infrastructure.common.PaymentMethod import roomescape.payment.infrastructure.persistence.v2.* -import roomescape.reservation.web.ReservationCancelRequest import roomescape.reservation.web.ReservationPaymentRequest -import roomescape.reservation.web.toPaymentConfirmRequest import java.time.LocalDateTime private val log: KLogger = KotlinLogging.logger {} @Component class PaymentWriterV2( - private val paymentClient: TosspaymentClientV2, private val paymentRepository: PaymentRepositoryV2, private val paymentDetailRepository: PaymentDetailRepository, private val canceledPaymentRepository: CanceledPaymentRepositoryV2, private val tsidFactory: TsidFactory, ) { - - fun requestConfirmPayment( - reservationId: Long, - request: ReservationPaymentRequest - ): PaymentConfirmResponse { - log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 승인 요청 시작: reservationId=${reservationId}, paymentKey=${request.paymentKey}" } - - return paymentClient.confirm(request.toPaymentConfirmRequest()).also { - log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 승인 요청 완료: response=$it" } - } - } - fun createPayment( reservationId: Long, request: ReservationPaymentRequest, @@ -48,12 +33,12 @@ class PaymentWriterV2( id = tsidFactory.next(), reservationId, request.orderId, request.paymentType ).also { paymentRepository.save(it) - saveDetail(paymentConfirmResponse, it.id) + createDetail(paymentConfirmResponse, it.id) log.debug { "[PaymentWriterV2.createPayment] 결제 승인 및 결제 정보 저장 완료: reservationId=${reservationId}, paymentId=${it.id}" } } } - private fun saveDetail( + private fun createDetail( paymentResponse: PaymentConfirmResponse, paymentId: Long, ): PaymentDetailEntity { @@ -72,50 +57,24 @@ class PaymentWriterV2( throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE) } - fun requestCancelPayment( - reservationId: Long, - request: ReservationCancelRequest, - ): PaymentCancelResponseV2 { - log.debug { "[PaymentWriterV2.requestConfirmPayment] 결제 취소 요청 시작: reservationId=$reservationId, request=${request}" } - - val payment: PaymentEntityV2 = paymentRepository.findByReservationId(reservationId) - ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - - val paymentCancelRequest = PaymentCancelRequestV2( - paymentKey = payment.paymentKey, - cancelReason = request.cancelReason, - amount = payment.totalAmount - ) - - return paymentClient.cancel(paymentCancelRequest).also { - log.debug { "[PaymentWriterV2.requestCancelPayment] 결제 취소 요청 완료: reservationId=${reservationId}, paymentKey=${payment.paymentKey}" } - } - } - fun createCanceledPayment( memberId: Long, - reservationId: Long, - request: ReservationCancelRequest, - paymentCancelResponse: PaymentCancelResponseV2, - requestedAt: LocalDateTime + payment: PaymentEntityV2, + requestedAt: LocalDateTime, + cancelResponse: PaymentCancelResponseV2 ) { - val payment: PaymentEntityV2 = paymentRepository.findByReservationId(reservationId) - ?.also { it.cancel() } - ?: throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) + log.debug { "[PaymentWriterV2.cancelPayment] 취소된 결제 정보 저장 시작: paymentId=${payment.id}, paymentKey=${payment.paymentKey}" } - val cancelDetail: CancelDetail = paymentCancelResponse.cancels - - CanceledPaymentEntityV2( + val canceledPayment: CanceledPaymentEntityV2 = cancelResponse.cancels.toEntity( id = tsidFactory.next(), - canceledAt = cancelDetail.canceledAt, - requestedAt = requestedAt, paymentId = payment.id, - canceledBy = memberId, - cancelReason = request.cancelReason, - cancelAmount = cancelDetail.cancelAmount, - cardDiscountAmount = cancelDetail.cardDiscountAmount, - transferDiscountAmount = cancelDetail.transferDiscountAmount, - easypayDiscountAmount = cancelDetail.easyPayDiscountAmount, - ).also { canceledPaymentRepository.save(it) } + cancelRequestedAt = requestedAt, + canceledBy = memberId + ) + + canceledPaymentRepository.save(canceledPayment).also { + payment.cancel() + log.debug { "[PaymentWriterV2.cancelPayment] 취소된 결제 정보 저장 완료: paymentId=${payment.id}, canceledPaymentId=${it.id}, paymentKey=${payment.paymentKey}" } + } } } diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt index 8bdc544d..0c7fef87 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt @@ -5,6 +5,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.transaction.Transactional import org.springframework.stereotype.Service import roomescape.common.util.TransactionExecutionUtil +import roomescape.payment.implement.PaymentFinderV2 +import roomescape.payment.implement.PaymentRequester import roomescape.payment.implement.PaymentWriterV2 import roomescape.payment.infrastructure.client.v2.PaymentConfirmResponse import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2 @@ -13,7 +15,6 @@ import roomescape.reservation.implement.ReservationWriter import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.reservation.web.* -import java.time.LocalDateTime private val log: KLogger = KotlinLogging.logger {} @@ -21,10 +22,11 @@ private val log: KLogger = KotlinLogging.logger {} class ReservationWithPaymentServiceV2( private val reservationWriter: ReservationWriter, private val reservationFinder: ReservationFinder, + private val paymentRequester: PaymentRequester, + private val paymentFinder: PaymentFinderV2, private val paymentWriter: PaymentWriterV2, - private val transactionExecutionUtil: TransactionExecutionUtil + private val transactionExecutionUtil: TransactionExecutionUtil, ) { - @Transactional fun createPendingReservation(memberId: Long, request: ReservationCreateRequest): ReservationCreateResponseV2 { log.info { @@ -59,29 +61,20 @@ class ReservationWithPaymentServiceV2( "예약 결제 시작: memberId=$memberId, reservationId=$reservationId, request=$request" } - val paymentConfirmResponse = paymentWriter.requestConfirmPayment(reservationId, request) - - return transactionExecutionUtil.withNewTransaction(isReadOnly = false) { - savePayment(memberId, reservationId, request, paymentConfirmResponse) - } - } - - private fun savePayment( - memberId: Long, - reservationId: Long, - request: ReservationPaymentRequest, - paymentConfirmResponse: PaymentConfirmResponse - ): ReservationPaymentResponse { - val reservation = - reservationFinder.findPendingReservation(reservationId, memberId).also { it.confirm() } - val payment: PaymentEntityV2 = paymentWriter.createPayment( - reservationId = reservationId, - request = request, - paymentConfirmResponse = paymentConfirmResponse + val paymentConfirmResponse: PaymentConfirmResponse = paymentRequester.requestConfirmPayment( + paymentKey = request.paymentKey, + orderId = request.orderId, + amount = request.amount ) - return ReservationPaymentResponse(reservationId, reservation.status, payment.id, payment.status) - .also { log.info { "[ReservationWithPaymentServiceV2.payReservation] 예약 결제 완료: response=${it}" } } + return transactionExecutionUtil.withNewTransaction(isReadOnly = false) { + val payment: PaymentEntityV2 = paymentWriter.createPayment(reservationId, request, paymentConfirmResponse) + val reservation: ReservationEntity = + reservationWriter.modifyStatusFromPendingToConfirmed(reservationId, memberId) + + ReservationPaymentResponse(reservationId, reservation.status, payment.id, payment.status) + .also { log.info { "[ReservationWithPaymentServiceV2.payReservation] 예약 결제 완료: response=${it}" } } + } } fun cancelReservation( @@ -89,17 +82,27 @@ class ReservationWithPaymentServiceV2( reservationId: Long, request: ReservationCancelRequest ) { - val requestedAt: LocalDateTime = LocalDateTime.now() log.info { "[ReservationWithPaymentServiceV2.cancelReservation] " + "예약 취소 시작: memberId=$memberId, reservationId=$reservationId, request=$request" } - val paymentCancelResponse = paymentWriter.requestCancelPayment(reservationId, request) + val reservation: ReservationEntity = reservationFinder.findById(reservationId) + val payment: PaymentEntityV2 = paymentFinder.findPaymentByReservationId(reservationId) + val paymentCancelResponse = paymentRequester.requestCancelPayment( + paymentKey = payment.paymentKey, + amount = payment.totalAmount, + cancelReason = request.cancelReason + ) transactionExecutionUtil.withNewTransaction(isReadOnly = false) { - paymentWriter.createCanceledPayment(memberId, reservationId, request, paymentCancelResponse, requestedAt) - reservationFinder.findById(reservationId).also { reservationWriter.cancelByUser(it, memberId) } + paymentWriter.createCanceledPayment(memberId, payment, request.requestedAt, paymentCancelResponse) + reservationWriter.modifyStatusToCanceledByUser(reservation, memberId) + }.also { + log.info { + "[ReservationWithPaymentServiceV2.cancelReservation] " + + "예약 취소 완료: reservationId=$reservationId, memberId=$memberId, cancelReason=${request.cancelReason}" + } } } } diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt index aef346c8..78285bda 100644 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt @@ -153,4 +153,26 @@ class ReservationValidator( throw ReservationException(ReservationErrorCode.RESERVATION_NOT_PENDING) } } + + fun validateIsPending(reservation: ReservationEntity) { + log.debug { "[ReservationValidator.validateIsPending] 시작: reservationId=${reservation.id}, status=${reservation.status}" } + + if (reservation.status != ReservationStatus.PENDING) { + log.warn { "[ReservationValidator.validateIsPending] 예약 상태가 결제 대기 중이 아님: reservationId=${reservation.id}, status=${reservation.status}" } + throw ReservationException(ReservationErrorCode.RESERVATION_NOT_PENDING) + } + + log.debug { "[ReservationValidator.validateIsPending] 완료: reservationId=${reservation.id}, status=${reservation.status}" } + } + + fun validateModifyAuthority(reservation: ReservationEntity, memberId: Long) { + log.debug { "[ReservationValidator.validateModifyAuthority] 시작: reservationId=${reservation.id}, memberId=$memberId" } + + if (reservation.member.id != memberId) { + log.error { "[ReservationValidator.validateModifyAuthority] 예약자 본인이 아님: reservationId=${reservation.id}, reservation.memberId=${reservation.member.id} memberId=$memberId" } + throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) + } + + log.debug { "[ReservationValidator.validateModifyAuthority] 완료: reservationId=${reservation.id}, memberId=$memberId" } + } } diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt index 6a881242..add04a0d 100644 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt +++ b/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt @@ -3,9 +3,12 @@ package roomescape.reservation.implement import com.github.f4b6a3.tsid.TsidFactory import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Component import roomescape.common.config.next import roomescape.member.implement.MemberFinder +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationRepository import roomescape.reservation.infrastructure.persistence.ReservationStatus @@ -100,7 +103,7 @@ class ReservationWriter( log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" } } - fun cancelByUser(reservation: ReservationEntity, requesterId: Long) { + fun modifyStatusToCanceledByUser(reservation: ReservationEntity, requesterId: Long) { log.debug { "[ReservationWriter.cancel] 예약 취소 시작: reservationId=${reservation.id}, requesterId=$requesterId" } memberFinder.findById(requesterId) @@ -110,4 +113,19 @@ class ReservationWriter( log.debug { "[ReservationWriter.cancel] 예약 취소 완료: reservationId=${reservation.id}" } } } + + fun modifyStatusFromPendingToConfirmed(reservationId: Long, memberId: Long): ReservationEntity { + log.debug { "[ReservationWriter.confirmPendingReservation] 시작: reservationId=$reservationId, memberId=$memberId" } + + return reservationRepository.findByIdOrNull(reservationId)?.also { + reservationValidator.validateIsPending(it) + reservationValidator.validateModifyAuthority(it, memberId) + + it.confirm() + log.debug { "[ReservationWriter.confirmPendingReservation] 완료: reservationId=${it.id}, status=${it.status}" } + } ?: run { + log.warn { "[ReservationWriter.confirmPendingReservation] 예약을 찾을 수 없음: reservationId=$reservationId" } + throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + } + } }