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