From b865dc01f67f7467ea88ae6ddd449de586f68ecb Mon Sep 17 00:00:00 2001 From: pricelees Date: Tue, 5 Aug 2025 21:46:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20payment=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=20Finder,=20Writer=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/business/PaymentService.kt | 138 ++++++------------ .../payment/implement/PaymentFinder.kt | 48 ++++++ .../payment/implement/PaymentWriter.kt | 80 ++++++++++ .../persistence/PaymentRepository.kt | 4 +- .../roomescape/payment/web/PaymentDTO.kt | 10 +- .../business/ReservationWithPaymentService.kt | 25 ++-- 6 files changed, 192 insertions(+), 113 deletions(-) create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentFinder.kt create mode 100644 src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt diff --git a/src/main/kotlin/roomescape/payment/business/PaymentService.kt b/src/main/kotlin/roomescape/payment/business/PaymentService.kt index b5458b69..a6b30c41 100644 --- a/src/main/kotlin/roomescape/payment/business/PaymentService.kt +++ b/src/main/kotlin/roomescape/payment/business/PaymentService.kt @@ -1,17 +1,13 @@ package roomescape.payment.business -import com.github.f4b6a3.tsid.TsidFactory import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import roomescape.common.config.next -import roomescape.payment.exception.PaymentErrorCode -import roomescape.payment.exception.PaymentException +import roomescape.payment.implement.PaymentFinder +import roomescape.payment.implement.PaymentWriter import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity -import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository import roomescape.payment.infrastructure.persistence.PaymentEntity -import roomescape.payment.infrastructure.persistence.PaymentRepository import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse import roomescape.payment.web.PaymentCreateResponse @@ -23,108 +19,70 @@ private val log = KotlinLogging.logger {} @Service class PaymentService( - private val tsidFactory: TsidFactory, - private val paymentRepository: PaymentRepository, - private val canceledPaymentRepository: CanceledPaymentRepository, + private val paymentFinder: PaymentFinder, + private val paymentWriter: PaymentWriter ) { @Transactional fun createPayment( - approveResponse: PaymentApproveResponse, + approvedPaymentInfo: PaymentApproveResponse, reservation: ReservationEntity, ): PaymentCreateResponse { - log.debug { "[PaymentService.createPayment] 결제 정보 저장 시작: request=$approveResponse, reservationId=${reservation.id}" } - val payment = PaymentEntity( - _id = tsidFactory.next(), - orderId = approveResponse.orderId, - paymentKey = approveResponse.paymentKey, - totalAmount = approveResponse.totalAmount, - reservation = reservation, - approvedAt = approveResponse.approvedAt + log.info { "[PaymentService.createPayment] 시작: paymentKey=${approvedPaymentInfo.paymentKey}, reservationId=${reservation.id}" } + + val created: PaymentEntity = paymentWriter.create( + paymentKey = approvedPaymentInfo.paymentKey, + orderId = approvedPaymentInfo.orderId, + totalAmount = approvedPaymentInfo.totalAmount, + approvedAt = approvedPaymentInfo.approvedAt, + reservation = reservation ) - return paymentRepository.save(payment) - .toCreateResponse() - .also { log.info { "[PaymentService.createPayment] 결제 정보 저장 완료: paymentId=${it.id}, reservationId=${reservation.id}" } } + return created.toCreateResponse() + .also { log.info { "[PaymentService.createPayment] 완료: paymentKey=${it.paymentKey}, reservationId=${reservation.id}, paymentId=${it.id}" } } } @Transactional(readOnly = true) - fun isReservationPaid(reservationId: Long): Boolean { - log.debug { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 시작: reservationId=$reservationId" } + fun existsByReservationId(reservationId: Long): Boolean { + log.info { "[PaymentService.isReservationPaid] 시작: reservationId=$reservationId" } - return paymentRepository.existsByReservationId(reservationId) - .also { log.info { "[PaymentService.isReservationPaid] 예약 결제 여부 확인 완료: reservationId=$reservationId, isPaid=$it" } } + return paymentFinder.existsPaymentByReservationId(reservationId) + .also { log.info { "[PaymentService.isReservationPaid] 완료: reservationId=$reservationId, isPaid=$it" } } } @Transactional fun createCanceledPayment( - cancelInfo: PaymentCancelResponse, + canceledPaymentInfo: PaymentCancelResponse, approvedAt: OffsetDateTime, paymentKey: String, ): CanceledPaymentEntity { - log.debug { - "[PaymentService.createCanceledPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" + - ", cancelInfo=$cancelInfo" - } - val canceledPayment = CanceledPaymentEntity( - _id = tsidFactory.next(), - paymentKey = paymentKey, - cancelReason = cancelInfo.cancelReason, - cancelAmount = cancelInfo.cancelAmount, + log.info { "[PaymentService.createCanceledPayment] 시작: paymentKey=$paymentKey" } + + val created: CanceledPaymentEntity = paymentWriter.createCanceled( + cancelReason = canceledPaymentInfo.cancelReason, + cancelAmount = canceledPaymentInfo.cancelAmount, + canceledAt = canceledPaymentInfo.canceledAt, approvedAt = approvedAt, - canceledAt = cancelInfo.canceledAt + paymentKey = paymentKey ) - return canceledPaymentRepository.save(canceledPayment) - .also { - log.info { - "[PaymentService.createCanceledPayment] 결제 취소 정보 생성 완료: canceledPaymentId=${it.id}" + - ", paymentKey=${paymentKey}, amount=${cancelInfo.cancelAmount}, canceledAt=${it.canceledAt}" - } - } + return created.also { + log.info { "[PaymentService.createCanceledPayment] 완료: paymentKey=${paymentKey}, canceledPaymentId=${it.id}" } + } } @Transactional - fun createCanceledPaymentByReservationId(reservationId: Long): PaymentCancelRequest { - log.debug { "[PaymentService.createCanceledPaymentByReservationId] 예약 삭제 & 결제 취소 정보 저장 시작: reservationId=$reservationId" } - val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId) - ?: run { - log.warn { "[PaymentService.createCanceledPaymentByReservationId] 예약 조회 실패: reservationId=$reservationId" } - throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - } + fun createCanceledPayment(reservationId: Long): PaymentCancelRequest { + log.info { "[PaymentService.createCanceledPaymentByReservationId] 시작: reservationId=$reservationId" } - val canceled: CanceledPaymentEntity = cancelPayment(paymentKey) - - return PaymentCancelRequest(paymentKey, canceled.cancelAmount, canceled.cancelReason) - .also { log.info { "[PaymentService.createCanceledPaymentByReservationId] 예약 ID로 결제 취소 완료: reservationId=$reservationId" } } - } - - private fun cancelPayment( - paymentKey: String, - cancelReason: String = "고객 요청", - canceledAt: OffsetDateTime = OffsetDateTime.now(), - ): CanceledPaymentEntity { - log.debug { "[PaymentService.cancelPayment] 결제 취소 정보 저장 시작: paymentKey=$paymentKey" } - val payment: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey) - ?.also { - paymentRepository.delete(it) - log.info { "[PaymentService.cancelPayment] 결제 정보 삭제 완료: paymentId=${it.id}, paymentKey=$paymentKey" } - } - ?: run { - log.warn { "[PaymentService.cancelPayment] 결제 정보 조회 실패: paymentKey=$paymentKey" } - throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - } - - val canceledPayment = CanceledPaymentEntity( - _id = tsidFactory.next(), - paymentKey = paymentKey, - cancelReason = cancelReason, - cancelAmount = payment.totalAmount, - approvedAt = payment.approvedAt, - canceledAt = canceledAt + val payment: PaymentEntity = paymentFinder.findByReservationId(reservationId) + val canceled: CanceledPaymentEntity = paymentWriter.createCanceled( + payment = payment, + cancelReason = "예약 취소", + canceledAt = OffsetDateTime.now(), ) - return canceledPaymentRepository.save(canceledPayment) - .also { log.info { "[PaymentService.cancelPayment] 결제 취소 정보 저장 완료: canceledPaymentId=${it.id}" } } + return PaymentCancelRequest(canceled.paymentKey, canceled.cancelAmount, canceled.cancelReason) + .also { log.info { "[PaymentService.createCanceledPaymentByReservationId] 완료: reservationId=$reservationId, paymentKey=${it.paymentKey}" } } } @Transactional @@ -132,18 +90,10 @@ class PaymentService( paymentKey: String, canceledAt: OffsetDateTime, ) { - log.debug { "[PaymentService.updateCanceledTime] 취소 시간 업데이트 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" } - canceledPaymentRepository.findByPaymentKey(paymentKey) - ?.apply { this.canceledAt = canceledAt } - ?.also { - log.info { - "[PaymentService.updateCanceledTime] 취소 시간 업데이트 완료: paymentKey=$paymentKey" + - ", canceledAt=$canceledAt" - } - } - ?: run { - log.warn { "[PaymentService.updateCanceledTime] 결제 정보 조회 실패: paymentKey=$paymentKey" } - throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) - } + log.info { "[PaymentService.updateCanceledTime] 시작: paymentKey=$paymentKey, canceledAt=$canceledAt" } + + paymentFinder.findCanceledByKey(paymentKey).apply { this.canceledAt = canceledAt } + + log.info { "[PaymentService.updateCanceledTime] 완료: paymentKey=$paymentKey, canceledAt=$canceledAt" } } } diff --git a/src/main/kotlin/roomescape/payment/implement/PaymentFinder.kt b/src/main/kotlin/roomescape/payment/implement/PaymentFinder.kt new file mode 100644 index 00000000..84a64d17 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/implement/PaymentFinder.kt @@ -0,0 +1,48 @@ +package roomescape.payment.implement + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException +import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity +import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository +import roomescape.payment.infrastructure.persistence.PaymentEntity +import roomescape.payment.infrastructure.persistence.PaymentRepository + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class PaymentFinder( + private val paymentRepository: PaymentRepository, + private val canceledPaymentRepository: CanceledPaymentRepository, +) { + fun existsPaymentByReservationId(reservationId: Long): Boolean { + log.debug { "[PaymentFinder.existsPaymentByReservationId] 시작: reservationId=$reservationId" } + + return paymentRepository.existsByReservationId(reservationId) + .also { log.debug { "[PaymentFinder.existsPaymentByReservationId] 완료: reservationId=$reservationId, isExist=$it" } } + } + + fun findByReservationId(reservationId: Long): PaymentEntity { + log.debug { "[PaymentFinder.findByReservationId] 시작: reservationId=$reservationId" } + + return paymentRepository.findByReservationId(reservationId) + ?.also { log.debug { "[PaymentFinder.findByReservationId] 완료: reservationId=$reservationId" } } + ?: run { + log.warn { "[PaymentFinder.findByReservationId] 실패: reservationId=$reservationId" } + throw PaymentException(PaymentErrorCode.PAYMENT_NOT_FOUND) + } + } + + fun findCanceledByKey(paymentKey: String): CanceledPaymentEntity { + log.debug { "[PaymentFinder.findCanceledByKey] 시작: paymentKey=$paymentKey" } + + return canceledPaymentRepository.findByPaymentKey(paymentKey) + ?.also { log.debug { "[PaymentFinder.findCanceledByKey] 완료: canceledPaymentId=${it.id}" } } + ?: run { + log.warn { "[PaymentFinder.findCanceledByKey] 실패: paymentKey=$paymentKey" } + throw PaymentException(PaymentErrorCode.CANCELED_PAYMENT_NOT_FOUND) + } + } +} diff --git a/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt b/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt new file mode 100644 index 00000000..43778651 --- /dev/null +++ b/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt @@ -0,0 +1,80 @@ +package roomescape.payment.implement + +import com.github.f4b6a3.tsid.TsidFactory +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Component +import roomescape.common.config.next +import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity +import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository +import roomescape.payment.infrastructure.persistence.PaymentEntity +import roomescape.payment.infrastructure.persistence.PaymentRepository +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import java.time.OffsetDateTime + +private val log: KLogger = KotlinLogging.logger {} + +@Component +class PaymentWriter( + private val paymentRepository: PaymentRepository, + private val canceledPaymentRepository: CanceledPaymentRepository, + private val tsidFactory: TsidFactory, +) { + fun create( + paymentKey: String, + orderId: String, + totalAmount: Long, + approvedAt: OffsetDateTime, + reservation: ReservationEntity + ): PaymentEntity { + log.debug { "[PaymentWriter.create] 시작: paymentKey=${paymentKey}, reservationId=${reservation.id}" } + + val payment = PaymentEntity( + _id = tsidFactory.next(), + orderId = orderId, + paymentKey = paymentKey, + totalAmount = totalAmount, + reservation = reservation, + approvedAt = approvedAt + ) + + return paymentRepository.save(payment) + .also { log.debug { "[PaymentWriter.create] 완료: paymentId=${it.id}, reservationId=${reservation.id}" } } + } + + fun createCanceled( + payment: PaymentEntity, + cancelReason: String, + canceledAt: OffsetDateTime, + ): CanceledPaymentEntity = createCanceled( + cancelReason = cancelReason, + canceledAt = canceledAt, + cancelAmount = payment.totalAmount, + approvedAt = payment.approvedAt, + paymentKey = payment.paymentKey + ) + + fun createCanceled( + cancelReason: String, + cancelAmount: Long, + canceledAt: OffsetDateTime, + approvedAt: OffsetDateTime, + paymentKey: String, + ): CanceledPaymentEntity { + log.debug { "[PaymentWriter.createCanceled] 시작: paymentKey=$paymentKey cancelAmount=$cancelAmount" } + + val canceledPayment = CanceledPaymentEntity( + _id = tsidFactory.next(), + paymentKey = paymentKey, + cancelReason = cancelReason, + cancelAmount = cancelAmount, + approvedAt = approvedAt, + canceledAt = canceledAt + ) + + return canceledPaymentRepository.save(canceledPayment) + .also { + log.debug { "[PaymentWriter.createCanceled] 완료: paymentKey=${paymentKey}, canceledPaymentId=${it.id}" } + } + } +} diff --git a/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt b/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt index bf83f375..79c1e07e 100644 --- a/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt +++ b/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt @@ -1,14 +1,12 @@ package roomescape.payment.infrastructure.persistence import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.data.jpa.repository.Query interface PaymentRepository : JpaRepository { fun existsByReservationId(reservationId: Long): Boolean - @Query("SELECT p.paymentKey FROM PaymentEntity p WHERE p.reservation.id = :reservationId") - fun findPaymentKeyByReservationId(reservationId: Long): String? + fun findByReservationId(reservationId: Long): PaymentEntity? fun findByPaymentKey(paymentKey: String): PaymentEntity? } diff --git a/src/main/kotlin/roomescape/payment/web/PaymentDTO.kt b/src/main/kotlin/roomescape/payment/web/PaymentDTO.kt index 10a885fe..44c069e9 100644 --- a/src/main/kotlin/roomescape/payment/web/PaymentDTO.kt +++ b/src/main/kotlin/roomescape/payment/web/PaymentDTO.kt @@ -3,8 +3,6 @@ package roomescape.payment.web import com.fasterxml.jackson.databind.annotation.JsonDeserialize import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer import roomescape.payment.infrastructure.persistence.PaymentEntity -import roomescape.reservation.web.ReservationRetrieveResponse -import roomescape.reservation.web.toRetrieveResponse import java.time.OffsetDateTime data class PaymentCancelRequest( @@ -26,15 +24,15 @@ data class PaymentCreateResponse( val orderId: String, val paymentKey: String, val totalAmount: Long, - val reservation: ReservationRetrieveResponse, + val reservationId: Long, val approvedAt: OffsetDateTime ) -fun PaymentEntity.toCreateResponse(): PaymentCreateResponse = PaymentCreateResponse( +fun PaymentEntity.toCreateResponse() = PaymentCreateResponse( id = this.id!!, orderId = this.orderId, paymentKey = this.paymentKey, totalAmount = this.totalAmount, - reservation = this.reservation.toRetrieveResponse(), + reservationId = this.reservation.id!!, approvedAt = this.approvedAt -) \ No newline at end of file +) diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt index 03151e88..66abc2f5 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt @@ -8,8 +8,9 @@ import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.web.PaymentCancelRequest import roomescape.payment.web.PaymentCancelResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.web.ReservationCreateResponse import roomescape.reservation.web.ReservationCreateWithPaymentRequest -import roomescape.reservation.web.ReservationRetrieveResponse +import roomescape.reservation.web.toCreateResponse import java.time.OffsetDateTime private val log = KotlinLogging.logger {} @@ -22,24 +23,24 @@ class ReservationWithPaymentService( ) { fun createReservationAndPayment( request: ReservationCreateWithPaymentRequest, - paymentInfo: PaymentApproveResponse, + approvedPaymentInfo: PaymentApproveResponse, memberId: Long, ): ReservationCreateResponse { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$approvedPaymentInfo" } val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId) + .also { paymentService.createPayment(approvedPaymentInfo, it) } - return paymentService.createPayment(paymentInfo, reservation) + return reservation.toCreateResponse() .also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 완료: reservationId=${reservation.id}, paymentId=${it.id}" } } - .reservation } fun createCanceledPayment( - cancelInfo: PaymentCancelResponse, + canceledPaymentInfo: PaymentCancelResponse, approvedAt: OffsetDateTime, paymentKey: String, ) { - paymentService.createCanceledPayment(cancelInfo, approvedAt, paymentKey) + paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey) } fun deleteReservationAndPayment( @@ -47,7 +48,7 @@ class ReservationWithPaymentService( memberId: Long, ): PaymentCancelRequest { log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId" } - val paymentCancelRequest = paymentService.createCanceledPaymentByReservationId(reservationId) + val paymentCancelRequest = paymentService.createCanceledPayment(reservationId) reservationService.deleteReservation(reservationId, memberId) log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 완료: reservationId=$reservationId" } @@ -56,9 +57,13 @@ class ReservationWithPaymentService( @Transactional(readOnly = true) fun isNotPaidReservation(reservationId: Long): Boolean { - log.debug { "[ReservationWithPaymentService.isNotPaidReservation] 예약 결제 여부 확인: reservationId=$reservationId" } - return !paymentService.isReservationPaid(reservationId) - .also { log.info { "[ReservationWithPaymentService.isNotPaidReservation] 결제 여부 확인 완료: reservationId=$reservationId, 결제 여부=${!it}" } } + log.info { "[ReservationWithPaymentService.isNotPaidReservation] 시작: reservationId=$reservationId" } + + val notPaid: Boolean = !paymentService.existsByReservationId(reservationId) + + return notPaid.also { + log.info { "[ReservationWithPaymentService.isNotPaidReservation] 완료: reservationId=$reservationId, 미결제=${notPaid}" } + } } fun updateCanceledTime(