feat: 결제 & 예약 통합 로직 및 API

This commit is contained in:
이상진 2025-10-07 22:34:19 +09:00
parent edf4d3af24
commit 17fb44573d
4 changed files with 181 additions and 1 deletions

View File

@ -0,0 +1,130 @@
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.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.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
) {
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
val trial = paymentAttemptRepository.countByReservationId(reservationId)
val paymentKey = paymentConfirmRequest.paymentKey
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
try {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
validateAndMarkInProgress(reservationId)
}
val paymentClientResponse: PaymentGatewayResponse =
requestConfirmPayment(reservationId, paymentConfirmRequest)
orderPostProcessorService.processAfterPaymentConfirmation(reservationId, paymentClientResponse)
} catch (e: Exception) {
val errorCode: ErrorCode = if (e is RoomescapeException) {
e.errorCode
} else {
OrderErrorCode.BOOKING_UNEXPECTED_ERROR
}
throw OrderException(errorCode, e.message ?: errorCode.message, trial)
}
}
private fun validateAndMarkInProgress(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)
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 완료: reservationId=${reservationId}" }
} catch (e: OrderException) {
val errorCode = OrderErrorCode.NOT_CONFIRMABLE
throw OrderException(errorCode, e.message)
}
reservationService.markInProgress(reservationId)
}
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
}
}

View File

@ -0,0 +1,22 @@
package com.sangdol.roomescape.order.docs
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.auth.web.support.UserOnly
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
interface OrderAPI {
@UserOnly
@Operation(summary = "결제 및 예약 완료 처리")
@ApiResponses(ApiResponse(responseCode = "200"))
fun confirm(
@PathVariable("reservationId") reservationId: Long,
@RequestBody request: PaymentConfirmRequest
): ResponseEntity<CommonApiResponse<Unit>>
}

View File

@ -0,0 +1,25 @@
package com.sangdol.roomescape.order.web
import com.sangdol.common.types.web.CommonApiResponse
import com.sangdol.roomescape.order.business.OrderService
import com.sangdol.roomescape.order.docs.OrderAPI
import com.sangdol.roomescape.payment.dto.PaymentConfirmRequest
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/orders")
class OrderController(
private val orderService: OrderService
) : OrderAPI {
@PostMapping("/{reservationId}/confirm")
override fun confirm(
@PathVariable("reservationId") reservationId: Long,
@RequestBody request: PaymentConfirmRequest
): ResponseEntity<CommonApiResponse<Unit>> {
orderService.confirm(reservationId, request)
return ResponseEntity.ok(CommonApiResponse())
}
}

View File

@ -31,8 +31,11 @@ class PaymentService(
private val transactionExecutionUtil: TransactionExecutionUtil, private val transactionExecutionUtil: TransactionExecutionUtil,
) { ) {
fun requestConfirm(request: PaymentConfirmRequest): PaymentGatewayResponse { fun requestConfirm(request: PaymentConfirmRequest): PaymentGatewayResponse {
log.info { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" }
try { try {
return paymentClient.confirm(request.paymentKey, request.orderId, request.amount) return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also {
log.info { "[requestConfirm] 결제 완료: paymentKey=${request.paymentKey}" }
}
} catch (e: ExternalPaymentException) { } catch (e: ExternalPaymentException) {
val errorCode = if (e.httpStatusCode in 400..<500) { val errorCode = if (e.httpStatusCode in 400..<500) {
PaymentErrorCode.PAYMENT_CLIENT_ERROR PaymentErrorCode.PAYMENT_CLIENT_ERROR