[#35] 결제 스키마 재정의 & 예약 조회 페이지 개선 #36

Merged
pricelees merged 37 commits from refactor/#35 into main 2025-08-22 06:43:16 +00:00
5 changed files with 108 additions and 85 deletions
Showing only changes of commit b0b0eb6fcf - Show all commits

View File

@ -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)
}
}

View File

@ -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}" }
}
}
}

View File

@ -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,47 +61,48 @@ 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)
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(
memberId: Long,
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}"
}
}
}
}

View File

@ -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" }
}
}

View File

@ -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)
}
}
}