generated from pricelees/issue-pr-template
[#66] 결제 & 예약 확정 로직 수정 #67
@ -1,146 +1,69 @@
|
||||
package com.sangdol.roomescape.order.business
|
||||
|
||||
import com.sangdol.common.persistence.IDGenerator
|
||||
import com.sangdol.common.persistence.TransactionExecutionUtil
|
||||
import com.sangdol.common.types.exception.ErrorCode
|
||||
import com.sangdol.common.types.exception.RoomescapeException
|
||||
import com.sangdol.roomescape.order.exception.OrderErrorCode
|
||||
import com.sangdol.roomescape.order.exception.OrderException
|
||||
import com.sangdol.roomescape.order.infrastructure.persistence.AttemptResult
|
||||
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptEntity
|
||||
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository
|
||||
import com.sangdol.roomescape.payment.business.PaymentService
|
||||
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
|
||||
import com.sangdol.roomescape.payment.dto.PaymentGatewayResponse
|
||||
import com.sangdol.roomescape.payment.exception.PaymentException
|
||||
import com.sangdol.roomescape.reservation.business.ReservationService
|
||||
import com.sangdol.roomescape.reservation.business.event.ReservationConfirmEvent
|
||||
import com.sangdol.roomescape.reservation.dto.ReservationStateResponse
|
||||
import com.sangdol.roomescape.schedule.business.ScheduleService
|
||||
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class OrderService(
|
||||
private val idGenerator: IDGenerator,
|
||||
private val reservationService: ReservationService,
|
||||
private val scheduleService: ScheduleService,
|
||||
private val paymentService: PaymentService,
|
||||
private val transactionExecutionUtil: TransactionExecutionUtil,
|
||||
private val orderValidator: OrderValidator,
|
||||
private val paymentAttemptRepository: PaymentAttemptRepository,
|
||||
private val orderPostProcessorService: OrderPostProcessorService
|
||||
private val eventPublisher: ApplicationEventPublisher
|
||||
) {
|
||||
|
||||
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
|
||||
var trial: Long = 0
|
||||
val paymentKey = paymentConfirmRequest.paymentKey
|
||||
|
||||
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
|
||||
try {
|
||||
trial = transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
||||
getTrialAfterValidateCanConfirm(reservationId).also {
|
||||
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
|
||||
validateCanConfirm(reservationId)
|
||||
reservationService.markInProgress(reservationId)
|
||||
}
|
||||
} ?: run {
|
||||
log.warn { "[confirm] 모든 paymentAttempts 조회 과정에서의 예상치 못한 null 응답: reservationId=${reservationId}" }
|
||||
throw OrderException(OrderErrorCode.BOOKING_UNEXPECTED_ERROR)
|
||||
}
|
||||
|
||||
val paymentClientResponse: PaymentGatewayResponse =
|
||||
requestConfirmPayment(reservationId, paymentConfirmRequest)
|
||||
paymentService.requestConfirm(reservationId, paymentConfirmRequest)
|
||||
eventPublisher.publishEvent(ReservationConfirmEvent(reservationId))
|
||||
|
||||
orderPostProcessorService.processAfterPaymentConfirmation(reservationId, paymentClientResponse)
|
||||
log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료" }
|
||||
} catch (e: Exception) {
|
||||
val errorCode: ErrorCode = if (e is RoomescapeException) {
|
||||
e.errorCode
|
||||
} else {
|
||||
OrderErrorCode.BOOKING_UNEXPECTED_ERROR
|
||||
OrderErrorCode.ORDER_UNEXPECTED_ERROR
|
||||
}
|
||||
|
||||
throw OrderException(errorCode, e.message ?: errorCode.message, trial)
|
||||
throw OrderException(errorCode, e.message ?: errorCode.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrialAfterValidateCanConfirm(reservationId: Long): Long {
|
||||
private fun validateCanConfirm(reservationId: Long) {
|
||||
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
|
||||
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
|
||||
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)
|
||||
|
||||
try {
|
||||
orderValidator.validateCanConfirm(reservation, schedule)
|
||||
|
||||
return getTrialIfSuccessAttemptNotExists(reservationId).also {
|
||||
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 완료: reservationId=${reservationId}" }
|
||||
}
|
||||
} catch (e: OrderException) {
|
||||
val errorCode = OrderErrorCode.NOT_CONFIRMABLE
|
||||
throw OrderException(errorCode, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTrialIfSuccessAttemptNotExists(reservationId: Long): Long {
|
||||
val paymentAttempts: List<PaymentAttemptEntity> = paymentAttemptRepository.findAllByReservationId(reservationId)
|
||||
|
||||
if (paymentAttempts.any { it.result == AttemptResult.SUCCESS }) {
|
||||
log.info { "[validateCanConfirm] 이미 결제 완료된 예약: id=${reservationId}" }
|
||||
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
|
||||
}
|
||||
|
||||
return paymentAttempts.size.toLong()
|
||||
}
|
||||
|
||||
private fun requestConfirmPayment(
|
||||
reservationId: Long,
|
||||
paymentConfirmRequest: PaymentConfirmRequest
|
||||
): PaymentGatewayResponse {
|
||||
log.info { "[requestConfirmPayment] 결제 및 이력 저장 시작: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}" }
|
||||
val paymentResponse: PaymentGatewayResponse
|
||||
var attempt: PaymentAttemptEntity? = null
|
||||
|
||||
try {
|
||||
paymentResponse = paymentService.requestConfirm(paymentConfirmRequest)
|
||||
|
||||
attempt = PaymentAttemptEntity(
|
||||
id = idGenerator.create(),
|
||||
reservationId = reservationId,
|
||||
result = AttemptResult.SUCCESS,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
val errorCode: String = if (e is PaymentException) {
|
||||
log.info { "[requestConfirmPayment] 결제 요청 실패: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}" }
|
||||
e.errorCode.name
|
||||
} else {
|
||||
log.warn {
|
||||
"[requestConfirmPayment] 결제 요청 실패: reservationId=${reservationId}, paymentKey=${paymentConfirmRequest.paymentKey}"
|
||||
}
|
||||
OrderErrorCode.BOOKING_UNEXPECTED_ERROR.name
|
||||
}
|
||||
|
||||
attempt = PaymentAttemptEntity(
|
||||
id = idGenerator.create(),
|
||||
reservationId = reservationId,
|
||||
result = AttemptResult.FAILED,
|
||||
errorCode = errorCode,
|
||||
message = e.message
|
||||
)
|
||||
|
||||
throw e
|
||||
} finally {
|
||||
val savedAttempt: PaymentAttemptEntity? = attempt?.let {
|
||||
log.info { "[requestPayment] 결제 요청 이력 저장 시작: id=${it.id}, reservationId=${it.reservationId}, result=${it.result}, errorCode=${it.errorCode}, message=${it.message}" }
|
||||
paymentAttemptRepository.save(it)
|
||||
}
|
||||
savedAttempt?.also {
|
||||
log.info { "[requestPayment] 결제 요청 이력 저장 완료: id=${savedAttempt.id}" }
|
||||
} ?: run {
|
||||
log.info { "[requestPayment] 결제 요청 이력 저장 실패: reservationId=${reservationId}" }
|
||||
}
|
||||
}
|
||||
|
||||
return paymentResponse
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ package com.sangdol.roomescape.order.business
|
||||
import com.sangdol.common.utils.KoreaDateTime
|
||||
import com.sangdol.roomescape.order.exception.OrderErrorCode
|
||||
import com.sangdol.roomescape.order.exception.OrderException
|
||||
import com.sangdol.roomescape.order.infrastructure.persistence.PaymentAttemptRepository
|
||||
import com.sangdol.roomescape.reservation.dto.ReservationStateResponse
|
||||
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
|
||||
@ -29,7 +28,7 @@ class OrderValidator {
|
||||
when (reservation.status) {
|
||||
ReservationStatus.CONFIRMED -> {
|
||||
log.info { "[validateCanConfirm] 이미 확정된 예약: id=${reservation.id}" }
|
||||
throw OrderException(OrderErrorCode.BOOKING_ALREADY_COMPLETED)
|
||||
throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED)
|
||||
}
|
||||
ReservationStatus.EXPIRED -> {
|
||||
log.info { "[validateCanConfirm] 만료된 예약: id=${reservation.id}" }
|
||||
|
||||
@ -9,11 +9,11 @@ enum class OrderErrorCode(
|
||||
override val message: String
|
||||
) : ErrorCode {
|
||||
NOT_CONFIRMABLE(HttpStatus.CONFLICT, "B000", "예약을 확정할 수 없어요."),
|
||||
BOOKING_ALREADY_COMPLETED(HttpStatus.CONFLICT, "B001", "이미 완료된 예약이에요."),
|
||||
ORDER_ALREADY_CONFIRMED(HttpStatus.CONFLICT, "B001", "이미 완료된 예약이에요."),
|
||||
EXPIRED_RESERVATION(HttpStatus.CONFLICT, "B002", "결제 가능 시간이 지나 만료된 예약이에요. 처음부터 다시 시도해주세요."),
|
||||
CANCELED_RESERVATION(HttpStatus.CONFLICT, "B003", "이미 취소된 예약이에요. 본인이 취소하지 않았다면 매장에 문의해주세요."),
|
||||
PAST_SCHEDULE(HttpStatus.CONFLICT, "B004", "지난 일정은 예약할 수 없어요."),
|
||||
|
||||
BOOKING_UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B999", "예상치 못한 예외가 발생했어요. 잠시 후 다시 시도해주세요.")
|
||||
ORDER_UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "B999", "예상치 못한 예외가 발생했어요. 잠시 후 다시 시도해주세요.")
|
||||
;
|
||||
}
|
||||
|
||||
@ -6,11 +6,4 @@ import com.sangdol.common.types.exception.RoomescapeException
|
||||
class OrderException(
|
||||
override val errorCode: ErrorCode,
|
||||
override val message: String = errorCode.message,
|
||||
var trial: Long = 0
|
||||
) : RoomescapeException(errorCode, message)
|
||||
|
||||
class OrderErrorResponse(
|
||||
val code: String,
|
||||
val message: String,
|
||||
val trial: Long
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user