generated from pricelees/issue-pr-template
[#41] 예약 스키마 재정의 #42
@ -1,54 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.payment.implement.PaymentFinderV2
|
||||
import roomescape.payment.infrastructure.persistence.v2.CanceledPaymentEntityV2
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2
|
||||
import roomescape.reservation.implement.ReservationFinder
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.web.ReservationDetailRetrieveResponse
|
||||
import roomescape.reservation.web.ReservationSummaryRetrieveListResponse
|
||||
import roomescape.reservation.web.toCancelDetailResponse
|
||||
import roomescape.reservation.web.toPaymentDetailResponse
|
||||
import roomescape.reservation.web.toReservationDetailRetrieveResponse
|
||||
import roomescape.reservation.web.toRetrieveResponse
|
||||
import roomescape.reservation.web.toSummaryListResponse
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class MyReservationFindService(
|
||||
private val reservationFinder: ReservationFinder,
|
||||
private val paymentFinder: PaymentFinderV2
|
||||
) {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun findReservationsByMemberId(memberId: Long): ReservationSummaryRetrieveListResponse {
|
||||
log.debug { "[ReservationFindServiceV2.findReservationsByMemberId] 시작: memberId=$memberId" }
|
||||
|
||||
return reservationFinder.findAllByMemberIdV2(memberId)
|
||||
.toSummaryListResponse()
|
||||
.also { log.info { "[ReservationFindServiceV2.findReservationsByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=$memberId" } }
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun showReservationDetails(reservationId: Long): ReservationDetailRetrieveResponse {
|
||||
log.debug { "[ReservationFindServiceV2.showReservationDetails] 시작: reservationId=$reservationId" }
|
||||
|
||||
val reservation: ReservationEntity = reservationFinder.findById(reservationId)
|
||||
val payment: PaymentEntityV2 = paymentFinder.findPaymentByReservationId(reservationId)
|
||||
val paymentDetail: PaymentDetailEntity = paymentFinder.findPaymentDetailByPaymentId(payment.id)
|
||||
val canceledPayment: CanceledPaymentEntityV2? = paymentFinder.findCanceledPaymentByPaymentIdOrNull(payment.id)
|
||||
|
||||
return reservation.toReservationDetailRetrieveResponse(
|
||||
payment = payment.toRetrieveResponse(detail = paymentDetail.toPaymentDetailResponse()),
|
||||
cancellation = canceledPayment?.toCancelDetailResponse()
|
||||
).also {
|
||||
log.info { "[ReservationFindServiceV2.showReservationDetails] 예약 상세 조회 완료: reservationId=$reservationId" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.reservation.implement.ReservationFinder
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.MyReservationRetrieveListResponse
|
||||
import roomescape.reservation.web.ReservationRetrieveListResponse
|
||||
import roomescape.reservation.web.toRetrieveListResponse
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class ReservationFindService(
|
||||
private val reservationFinder: ReservationFinder
|
||||
) {
|
||||
fun findReservations(): ReservationRetrieveListResponse {
|
||||
log.debug { "[ReservationService.findReservations] 시작" }
|
||||
|
||||
return reservationFinder.findAllByStatuses(*ReservationStatus.confirmedStatus())
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ReservationService.findReservations] ${it.reservations.size}개의 예약 조회 완료" } }
|
||||
}
|
||||
|
||||
fun findAllWaiting(): ReservationRetrieveListResponse {
|
||||
log.debug { "[ReservationService.findAllWaiting] 시작" }
|
||||
|
||||
return reservationFinder.findAllByStatuses(ReservationStatus.WAITING)
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ReservationService.findAllWaiting] ${it.reservations.size}개의 대기 조회 완료" } }
|
||||
}
|
||||
|
||||
fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse {
|
||||
log.debug { "[ReservationService.findReservationsByMemberId] 시작: memberId=$memberId" }
|
||||
|
||||
return reservationFinder.findAllByMemberId(memberId)
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ReservationService.findReservationsByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=$memberId" } }
|
||||
}
|
||||
|
||||
fun searchReservations(
|
||||
themeId: Long?,
|
||||
memberId: Long?,
|
||||
startFrom: LocalDate?,
|
||||
endAt: LocalDate?,
|
||||
): ReservationRetrieveListResponse {
|
||||
log.debug { "[ReservationService.searchReservations] 시작: themeId=$themeId, memberId=$memberId, dateFrom=$startFrom, dateTo=$endAt" }
|
||||
|
||||
return reservationFinder.searchReservations(themeId, memberId, startFrom, endAt)
|
||||
.toRetrieveListResponse()
|
||||
.also { log.info { "[ReservationService.searchReservations] ${it.reservations.size}개의 예약 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$startFrom, dateTo=$endAt" } }
|
||||
}
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.payment.business.PaymentService
|
||||
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.toCreateResponse
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationWithPaymentService(
|
||||
private val reservationWriteService: ReservationWriteService,
|
||||
private val paymentService: PaymentService,
|
||||
) {
|
||||
fun createReservationAndPayment(
|
||||
request: ReservationCreateWithPaymentRequest,
|
||||
approvedPaymentInfo: PaymentApproveResponse,
|
||||
memberId: Long,
|
||||
): ReservationCreateResponse {
|
||||
log.info { "[ReservationWithPaymentService.createReservationAndPayment] 시작: memberId=$memberId, paymentInfo=$approvedPaymentInfo" }
|
||||
|
||||
val reservation: ReservationEntity = reservationWriteService.createReservationWithPayment(request, memberId)
|
||||
.also { paymentService.createPayment(approvedPaymentInfo, it) }
|
||||
|
||||
return reservation.toCreateResponse()
|
||||
.also { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 완료: reservationId=${reservation.id}, paymentId=${it.id}" } }
|
||||
}
|
||||
|
||||
fun createCanceledPayment(
|
||||
canceledPaymentInfo: PaymentCancelResponse,
|
||||
approvedAt: OffsetDateTime,
|
||||
paymentKey: String,
|
||||
) {
|
||||
paymentService.createCanceledPayment(canceledPaymentInfo, approvedAt, paymentKey)
|
||||
}
|
||||
|
||||
fun deleteReservationAndPayment(
|
||||
reservationId: Long,
|
||||
memberId: Long,
|
||||
): PaymentCancelRequest {
|
||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 시작: reservationId=$reservationId" }
|
||||
val paymentCancelRequest = paymentService.createCanceledPayment(reservationId)
|
||||
|
||||
reservationWriteService.deleteReservation(reservationId, memberId)
|
||||
log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 완료: reservationId=$reservationId" }
|
||||
return paymentCancelRequest
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun isNotPaidReservation(reservationId: Long): Boolean {
|
||||
log.info { "[ReservationWithPaymentService.isNotPaidReservation] 시작: reservationId=$reservationId" }
|
||||
|
||||
val notPaid: Boolean = !paymentService.existsByReservationId(reservationId)
|
||||
|
||||
return notPaid.also {
|
||||
log.info { "[ReservationWithPaymentService.isNotPaidReservation] 완료: reservationId=$reservationId, isPaid=${notPaid}" }
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCanceledTime(
|
||||
paymentKey: String,
|
||||
canceledAt: OffsetDateTime,
|
||||
) {
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
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
|
||||
import roomescape.reservation.implement.ReservationFinder
|
||||
import roomescape.reservation.implement.ReservationWriter
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
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,
|
||||
) {
|
||||
@Transactional
|
||||
fun createPendingReservation(memberId: Long, request: ReservationCreateRequest): ReservationCreateResponseV2 {
|
||||
log.info {
|
||||
"[ReservationWithPaymentServiceV2.createPendingReservation] " +
|
||||
"PENDING 예약 저장 시작: memberId=$memberId, request=$request"
|
||||
}
|
||||
|
||||
val reservation: ReservationEntity = reservationWriter.create(
|
||||
date = request.date,
|
||||
timeId = request.timeId,
|
||||
themeId = request.themeId,
|
||||
status = ReservationStatus.PENDING,
|
||||
memberId = memberId,
|
||||
requesterId = memberId
|
||||
)
|
||||
|
||||
return reservation.toCreateResponseV2().also {
|
||||
log.info {
|
||||
"[ReservationWithPaymentServiceV2.createPendingReservation] " +
|
||||
"PENDING 예약 저장 완료: reservationId=${reservation.id}, response=$it"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun payReservation(
|
||||
memberId: Long,
|
||||
reservationId: Long,
|
||||
request: ReservationPaymentRequest
|
||||
): ReservationPaymentResponse {
|
||||
log.info {
|
||||
"[ReservationWithPaymentServiceV2.payReservation] " +
|
||||
"예약 결제 시작: memberId=$memberId, reservationId=$reservationId, request=$request"
|
||||
}
|
||||
|
||||
val paymentConfirmResponse: PaymentConfirmResponse = paymentRequester.requestConfirmPayment(
|
||||
paymentKey = request.paymentKey,
|
||||
orderId = request.orderId,
|
||||
amount = request.amount
|
||||
)
|
||||
|
||||
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
|
||||
) {
|
||||
log.info {
|
||||
"[ReservationWithPaymentServiceV2.cancelReservation] " +
|
||||
"예약 취소 시작: memberId=$memberId, reservationId=$reservationId, request=$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, payment, request.requestedAt, paymentCancelResponse)
|
||||
reservationWriter.modifyStatusToCanceledByUser(reservation, memberId)
|
||||
}.also {
|
||||
log.info {
|
||||
"[ReservationWithPaymentServiceV2.cancelReservation] " +
|
||||
"예약 취소 완료: reservationId=$reservationId, memberId=$memberId, cancelReason=${request.cancelReason}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,104 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import jakarta.transaction.Transactional
|
||||
import org.springframework.stereotype.Service
|
||||
import roomescape.reservation.implement.ReservationFinder
|
||||
import roomescape.reservation.implement.ReservationWriter
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.*
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
class ReservationWriteService(
|
||||
private val reservationFinder: ReservationFinder,
|
||||
private val reservationWriter: ReservationWriter
|
||||
) {
|
||||
fun createReservationWithPayment(
|
||||
request: ReservationCreateWithPaymentRequest,
|
||||
memberId: Long
|
||||
): ReservationEntity {
|
||||
log.debug { "[ReservationCommandService.createReservationByAdmin] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${memberId}" }
|
||||
|
||||
val created: ReservationEntity = reservationWriter.create(
|
||||
date = request.date,
|
||||
timeId = request.timeId,
|
||||
themeId = request.themeId,
|
||||
status = ReservationStatus.CONFIRMED,
|
||||
memberId = memberId,
|
||||
requesterId = memberId
|
||||
)
|
||||
|
||||
return created.also {
|
||||
log.info { "[ReservationCommandService.createReservationByAdmin] 완료: reservationId=${it.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
fun createReservationByAdmin(
|
||||
request: AdminReservationCreateRequest,
|
||||
memberId: Long
|
||||
): ReservationCreateResponse {
|
||||
log.debug { "[ReservationCommandService.createReservationByAdmin] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${request.memberId} by adminId=${memberId}" }
|
||||
|
||||
val created: ReservationEntity = reservationWriter.create(
|
||||
date = request.date,
|
||||
timeId = request.timeId,
|
||||
themeId = request.themeId,
|
||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED,
|
||||
memberId = request.memberId,
|
||||
requesterId = memberId
|
||||
)
|
||||
|
||||
return created.toCreateResponse()
|
||||
.also {
|
||||
log.info { "[ReservationCommandService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationCreateResponse {
|
||||
log.debug { "[ReservationCommandService.createWaiting] 시작: date=${request.date}, timeId=${request.timeId}, themeId=${request.themeId}, memberId=${memberId}" }
|
||||
|
||||
val created: ReservationEntity = reservationWriter.create(
|
||||
date = request.date,
|
||||
timeId = request.timeId,
|
||||
themeId = request.themeId,
|
||||
status = ReservationStatus.WAITING,
|
||||
memberId = memberId,
|
||||
requesterId = memberId
|
||||
)
|
||||
|
||||
return created.toCreateResponse()
|
||||
.also {
|
||||
log.info { "[ReservationCommandService.createWaiting] 완료: reservationId=${it.id}" }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteReservation(reservationId: Long, memberId: Long) {
|
||||
log.debug { "[ReservationCommandService.deleteReservation] 시작: reservationId=${reservationId}, memberId=$memberId" }
|
||||
|
||||
val reservation: ReservationEntity = reservationFinder.findById(reservationId)
|
||||
|
||||
reservationWriter.deleteConfirmed(reservation, requesterId = memberId)
|
||||
.also { log.info { "[ReservationCommandService.deleteReservation] 완료: reservationId=${reservationId}" } }
|
||||
}
|
||||
|
||||
fun confirmWaiting(reservationId: Long, memberId: Long) {
|
||||
log.debug { "[ReservationCommandService.confirmWaiting] 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||
|
||||
reservationWriter.confirm(reservationId)
|
||||
.also { log.info { "[ReservationCommandService.confirmWaiting] 완료: reservationId=$reservationId" } }
|
||||
}
|
||||
|
||||
fun deleteWaiting(reservationId: Long, memberId: Long) {
|
||||
log.debug { "[ReservationCommandService.deleteWaiting] 시작: reservationId=$reservationId (by adminId=$memberId)" }
|
||||
|
||||
val reservation: ReservationEntity = reservationFinder.findById(reservationId)
|
||||
|
||||
reservationWriter.deleteWaiting(reservation, requesterId = memberId)
|
||||
.also { log.info { "[ReservationCommandService.deleteWaiting] 완료: reservationId=$reservationId" } }
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
package roomescape.reservation.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
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 roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.web.ReservationDetailRetrieveResponse
|
||||
import roomescape.reservation.web.ReservationSummaryRetrieveListResponse
|
||||
|
||||
interface MyReservationAPI {
|
||||
@LoginRequired
|
||||
@Operation(summary = "내 예약 개요 조회", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "성공"),
|
||||
)
|
||||
fun findAllMyReservations(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationSummaryRetrieveListResponse>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 상세 조회", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "성공"),
|
||||
)
|
||||
fun showReservationDetails(
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationDetailRetrieveResponse>>
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
package roomescape.reservation.docs
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import io.swagger.v3.oas.annotations.headers.Header
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import roomescape.auth.web.support.LoginRequired
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.web.*
|
||||
|
||||
interface ReservationWithPaymentAPI {
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 추가", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "성공",
|
||||
useReturnTypeSchema = true,
|
||||
headers = [Header(
|
||||
name = HttpHeaders.LOCATION,
|
||||
description = "생성된 예약 정보 URL",
|
||||
schema = Schema(example = "/reservations/1")
|
||||
)]
|
||||
)
|
||||
)
|
||||
fun createPendingReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateRequest
|
||||
): ResponseEntity<CommonApiResponse<ReservationCreateResponseV2>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 취소", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "204", description = "성공"),
|
||||
)
|
||||
fun cancelReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long,
|
||||
@Valid @RequestBody cancelRequest: ReservationCancelRequest
|
||||
): ResponseEntity<CommonApiResponse<Unit>>
|
||||
|
||||
@LoginRequired
|
||||
@Operation(summary = "예약 결제", tags = ["로그인이 필요한 API"])
|
||||
@ApiResponses(
|
||||
ApiResponse(responseCode = "200", description = "성공"),
|
||||
)
|
||||
fun createPaymentAndConfirmReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long,
|
||||
@Valid @RequestBody request: ReservationPaymentRequest
|
||||
): ResponseEntity<CommonApiResponse<ReservationPaymentResponse>>
|
||||
}
|
||||
@ -1,113 +0,0 @@
|
||||
package roomescape.reservation.implement
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Component
|
||||
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.ReservationSearchSpecification
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.MyReservationRetrieveResponse
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class ReservationFinder(
|
||||
private val reservationRepository: ReservationRepository,
|
||||
private val reservationValidator: ReservationValidator,
|
||||
) {
|
||||
fun findById(id: Long): ReservationEntity {
|
||||
log.debug { "[ReservationFinder.findById] 시작: id=$id" }
|
||||
|
||||
return reservationRepository.findByIdOrNull(id)
|
||||
?.also { log.debug { "[ReservationFinder.findById] 완료: reservationId=$id, date:${it.date}, timeId:${it.time.id}, themeId:${it.theme.id}" } }
|
||||
?: run {
|
||||
log.warn { "[ReservationFinder.findById] 조회 실패: reservationId=$id" }
|
||||
throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
fun findAllByStatuses(vararg statuses: ReservationStatus): List<ReservationEntity> {
|
||||
log.debug { "[ReservationFinder.findAll] 시작: status=${statuses}" }
|
||||
|
||||
val spec = ReservationSearchSpecification()
|
||||
.status(*statuses)
|
||||
.build()
|
||||
|
||||
return reservationRepository.findAll(spec)
|
||||
.also { log.debug { "[ReservationFinder.findAll] ${it.size}개 예약 조회 완료: status=${statuses}" } }
|
||||
}
|
||||
|
||||
fun findAllByDateAndTheme(
|
||||
date: LocalDate, theme: ThemeEntity
|
||||
): List<ReservationEntity> {
|
||||
log.debug { "[ReservationFinder.findAllByDateAndTheme] 시작: date=$date, themeId=${theme.id}" }
|
||||
|
||||
return reservationRepository.findAllByDateAndTheme(date, theme)
|
||||
.also { log.debug { "[ReservationFinder.findAllByDateAndTheme] ${it.size}개 조회 완료: date=$date, themeId=${theme.id}" } }
|
||||
}
|
||||
|
||||
fun findAllByMemberIdV2(memberId: Long): List<ReservationEntity> {
|
||||
log.debug { "[ReservationFinder.findAllByMember] 시작: memberId=${memberId}" }
|
||||
|
||||
return reservationRepository.findAllByMember_Id(memberId)
|
||||
.filter { it.status == ReservationStatus.CONFIRMED || it.status == ReservationStatus.CANCELED_BY_USER }
|
||||
.sortedByDescending { it.date }
|
||||
.also { log.debug { "[ReservationFinder.findAllByMember] ${it.size}개 예약 조회 완료: memberId=${memberId}" } }
|
||||
}
|
||||
|
||||
fun findAllByMemberId(memberId: Long): List<MyReservationRetrieveResponse> {
|
||||
log.debug { "[ReservationFinder.findAllByMemberId] 시작: memberId=${memberId}" }
|
||||
|
||||
return reservationRepository.findAllByMemberId(memberId)
|
||||
.also { log.debug { "[ReservationFinder.findAllByMemberId] ${it.size}개 예약(대기) 조회 완료: memberId=${memberId}" } }
|
||||
}
|
||||
|
||||
fun searchReservations(
|
||||
themeId: Long?,
|
||||
memberId: Long?,
|
||||
startFrom: LocalDate?,
|
||||
endAt: LocalDate?,
|
||||
): List<ReservationEntity> {
|
||||
reservationValidator.validateSearchDateRange(startFrom, endAt)
|
||||
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.sameThemeId(themeId)
|
||||
.sameMemberId(memberId)
|
||||
.dateStartFrom(startFrom)
|
||||
.dateEndAt(endAt)
|
||||
.status(ReservationStatus.CONFIRMED, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||
.build()
|
||||
|
||||
return reservationRepository.findAll(spec)
|
||||
.also {
|
||||
log.debug { "[ReservationFinder.searchReservations] ${it.size}개 예약 조회 완료. " +
|
||||
"themeId=${themeId}, memberId=${memberId}, startFrom=${startFrom}, endAt=${endAt}" }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun isTimeReserved(time: TimeEntity): Boolean {
|
||||
log.debug { "[ReservationFinder.isTimeReserved] 시작: timeId=${time.id}, startAt=${time.startAt}" }
|
||||
|
||||
return reservationRepository.existsByTime(time)
|
||||
.also { log.debug { "[ReservationFinder.isTimeReserved] 완료: isExist=$it, timeId=${time.id}, startAt=${time.startAt}" } }
|
||||
}
|
||||
|
||||
fun findPendingReservation(reservationId: Long, memberId: Long): ReservationEntity {
|
||||
log.debug { "[ReservationFinder.findPendingReservationIfExists] 시작: reservationId=$reservationId, memberId=$memberId" }
|
||||
|
||||
return findById(reservationId).also {
|
||||
reservationValidator.validateIsReservedByMemberAndPending(it, memberId)
|
||||
}.also {
|
||||
log.debug { "[ReservationFinder.findPendingReservationIfExists] 완료: reservationId=${it.id}, status=${it.status}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,178 +0,0 @@
|
||||
package roomescape.reservation.implement
|
||||
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import org.springframework.stereotype.Component
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
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.ReservationSearchSpecification
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class ReservationValidator(
|
||||
private val reservationRepository: ReservationRepository,
|
||||
) {
|
||||
fun validateIsPast(
|
||||
requestDate: LocalDate,
|
||||
requestTime: LocalTime,
|
||||
) {
|
||||
val now = LocalDateTime.now()
|
||||
val requestDateTime = LocalDateTime.of(requestDate, requestTime)
|
||||
log.debug { "[ReservationValidator.validateIsPast] 시작. request=$requestDateTime, now=$now" }
|
||||
|
||||
if (requestDateTime.isBefore(now)) {
|
||||
log.info { "[ReservationValidator.validateIsPast] 날짜 범위 오류. request=$requestDateTime, now=$now" }
|
||||
throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateIsPast] 완료. request=$requestDateTime, now=$now" }
|
||||
}
|
||||
|
||||
fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) {
|
||||
log.debug { "[ReservationValidator.validateSearchDateRange] 시작: startFrom=$startFrom, endAt=$endAt" }
|
||||
if (startFrom == null || endAt == null) {
|
||||
log.debug { "[ReservationValidator.validateSearchDateRange] 완료: startFrom=$startFrom, endAt=$endAt" }
|
||||
return
|
||||
}
|
||||
if (startFrom.isAfter(endAt)) {
|
||||
log.info { "[ReservationValidator.validateSearchDateRange] 날짜 범위 오류: startFrom=$startFrom, endAt=$endAt" }
|
||||
throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE)
|
||||
}
|
||||
log.debug { "[ReservationValidator.validateSearchDateRange] 완료: startFrom=$startFrom, endAt=$endAt" }
|
||||
}
|
||||
|
||||
fun validateIsAlreadyExists(date: LocalDate, time: TimeEntity, theme: ThemeEntity) {
|
||||
val themeId = theme.id
|
||||
val timeId = time.id
|
||||
|
||||
log.debug { "[ReservationValidator.validateIsAlreadyExists] 시작: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.sameThemeId(themeId)
|
||||
.sameTimeId(timeId)
|
||||
.sameDate(date)
|
||||
.build()
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
log.warn { "[ReservationValidator.validateIsAlreadyExists] 중복된 예약 존재: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||
throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateIsAlreadyExists] 완료: date=$date, timeId=$timeId, themeId=$themeId" }
|
||||
}
|
||||
|
||||
fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, requesterId: Long) {
|
||||
log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 시작: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" }
|
||||
|
||||
val spec: Specification<ReservationEntity> = ReservationSearchSpecification()
|
||||
.sameMemberId(requesterId)
|
||||
.sameThemeId(themeId)
|
||||
.sameTimeId(timeId)
|
||||
.sameDate(date)
|
||||
.build()
|
||||
|
||||
if (reservationRepository.exists(spec)) {
|
||||
log.warn { "[ReservationValidator.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" }
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_RESERVE)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateMemberAlreadyReserve] 완료: themeId=$themeId, timeId=$timeId, date=$date, requesterId=$requesterId" }
|
||||
}
|
||||
|
||||
fun validateIsWaiting(reservation: ReservationEntity) {
|
||||
log.debug { "[ReservationValidator.validateIsWaiting] 시작: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||
|
||||
if (!reservation.isWaiting()) {
|
||||
log.warn { "[ReservationValidator.validateIsWaiting] 대기 상태가 아님: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||
throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateIsWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||
}
|
||||
|
||||
fun validateCreateAuthority(requester: MemberEntity) {
|
||||
log.debug { "[ReservationValidator.validateCreateAuthority] 시작: requesterId=${requester.id}" }
|
||||
|
||||
if (!requester.isAdmin()) {
|
||||
log.error { "[ReservationValidator.validateCreateAuthority] 관리자가 아닌 다른 회원의 예약 시도: requesterId=${requester.id}" }
|
||||
throw ReservationException(ReservationErrorCode.NO_PERMISSION)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateCreateAuthority] 완료: requesterId=${requester.id}" }
|
||||
}
|
||||
|
||||
fun validateDeleteAuthority(reservation: ReservationEntity, requester: MemberEntity) {
|
||||
val requesterId: Long = requester.id!!
|
||||
log.debug { "[ReservationValidator.validateDeleteAuthority] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||
|
||||
if (requester.isAdmin()) {
|
||||
log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id} requesterId=${requesterId}(Admin)" }
|
||||
return
|
||||
}
|
||||
|
||||
if (!reservation.isReservedBy(requesterId)) {
|
||||
log.error {
|
||||
"[ReservationValidator.validateDeleteAuthority] 예약자 본인이 아님: reservationId=${reservation.id}" +
|
||||
", memberId=${reservation.member.id} requesterId=${requesterId} "
|
||||
}
|
||||
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateDeleteAuthority] 완료: reservationId=${reservation.id}, requesterId=$requesterId" }
|
||||
}
|
||||
|
||||
fun validateAlreadyConfirmed(reservationId: Long) {
|
||||
log.debug { "[ReservationValidator.validateAlreadyConfirmed] 시작: reservationId=$reservationId" }
|
||||
|
||||
if (reservationRepository.isExistConfirmedReservation(reservationId)) {
|
||||
log.warn { "[ReservationWriter.confirm] 이미 확정된 예약: reservationId=$reservationId" }
|
||||
throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS)
|
||||
}
|
||||
|
||||
log.debug { "[ReservationValidator.validateAlreadyConfirmed] 완료: reservationId=$reservationId" }
|
||||
}
|
||||
|
||||
fun validateIsReservedByMemberAndPending(reservation: ReservationEntity, requesterId: Long) {
|
||||
if (reservation.member.id != requesterId) {
|
||||
log.error { "[ReservationValidator.validateIsReservedByMemberAndPending] 예약자 본인이 아님: reservationId=${reservation.id}, reservation.memberId=${reservation.member.id} requesterId=$requesterId" }
|
||||
throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER)
|
||||
}
|
||||
if (reservation.status != ReservationStatus.PENDING) {
|
||||
log.warn { "[ReservationValidator.validateIsReservedByMemberAndPending] 예약 상태가 대기 중이 아님: reservationId=${reservation.id}, status=${reservation.status}" }
|
||||
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" }
|
||||
}
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
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
|
||||
import roomescape.theme.implement.ThemeFinder
|
||||
import roomescape.time.implement.TimeFinder
|
||||
import java.time.LocalDate
|
||||
|
||||
private val log: KLogger = KotlinLogging.logger {}
|
||||
|
||||
@Component
|
||||
class ReservationWriter(
|
||||
private val reservationValidator: ReservationValidator,
|
||||
private val reservationRepository: ReservationRepository,
|
||||
private val memberFinder: MemberFinder,
|
||||
private val timeFinder: TimeFinder,
|
||||
private val themeFinder: ThemeFinder,
|
||||
private val tsidFactory: TsidFactory,
|
||||
) {
|
||||
fun create(
|
||||
date: LocalDate,
|
||||
timeId: Long,
|
||||
themeId: Long,
|
||||
memberId: Long,
|
||||
status: ReservationStatus,
|
||||
requesterId: Long
|
||||
): ReservationEntity {
|
||||
log.debug {
|
||||
"[ReservationWriter.create] 시작: " +
|
||||
"date=${date}, timeId=${timeId}, themeId=${themeId}, memberId=${memberId}, status=${status}"
|
||||
}
|
||||
val time = timeFinder.findById(timeId).also {
|
||||
reservationValidator.validateIsPast(date, it.startAt)
|
||||
}
|
||||
val theme = themeFinder.findById(themeId)
|
||||
|
||||
val member = memberFinder.findById(memberId).also {
|
||||
if (status == ReservationStatus.WAITING) {
|
||||
reservationValidator.validateMemberAlreadyReserve(themeId, timeId, date, it.id!!)
|
||||
} else {
|
||||
reservationValidator.validateIsAlreadyExists(date, time, theme)
|
||||
}
|
||||
|
||||
if (memberId != requesterId) {
|
||||
val requester = memberFinder.findById(requesterId)
|
||||
reservationValidator.validateCreateAuthority(requester)
|
||||
}
|
||||
}
|
||||
|
||||
val reservation = ReservationEntity(
|
||||
_id = tsidFactory.next(),
|
||||
date = date,
|
||||
time = time,
|
||||
theme = theme,
|
||||
member = member,
|
||||
status = status
|
||||
)
|
||||
|
||||
return reservationRepository.save(reservation)
|
||||
.also { log.debug { "[ReservationWriter.create] 완료: reservationId=${it.id}, status=${it.status}" } }
|
||||
}
|
||||
|
||||
fun deleteWaiting(reservation: ReservationEntity, requesterId: Long) {
|
||||
log.debug { "[ReservationWriter.deleteWaiting] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||
|
||||
reservationValidator.validateIsWaiting(reservation)
|
||||
|
||||
delete(reservation, requesterId)
|
||||
.also { log.debug { "[ReservationWriter.deleteWaiting] 완료: reservationId=${reservation.id}, status=${reservation.status}" } }
|
||||
}
|
||||
|
||||
fun deleteConfirmed(reservation: ReservationEntity, requesterId: Long) {
|
||||
log.debug { "[ReservationWriter.deleteConfirmed] 시작: reservationId=${reservation.id}, requesterId=${requesterId}" }
|
||||
|
||||
delete(reservation, requesterId)
|
||||
.also { log.debug { "[ReservationWriter.deleteConfirmed] 완료: reservationId=${reservation.id}, status=${reservation.status}" } }
|
||||
}
|
||||
|
||||
private fun delete(reservation: ReservationEntity, requesterId: Long) {
|
||||
memberFinder.findById(requesterId)
|
||||
.also { reservationValidator.validateDeleteAuthority(reservation, requester = it) }
|
||||
|
||||
reservationRepository.delete(reservation)
|
||||
}
|
||||
|
||||
fun confirm(reservationId: Long) {
|
||||
log.debug { "[ReservationWriter.confirm] 대기 여부 확인 시작: reservationId=$reservationId" }
|
||||
|
||||
reservationValidator.validateAlreadyConfirmed(reservationId)
|
||||
|
||||
reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)
|
||||
|
||||
log.debug { "[ReservationWriter.confirm] 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" }
|
||||
}
|
||||
|
||||
fun modifyStatusToCanceledByUser(reservation: ReservationEntity, requesterId: Long) {
|
||||
log.debug { "[ReservationWriter.cancel] 예약 취소 시작: reservationId=${reservation.id}, requesterId=$requesterId" }
|
||||
|
||||
memberFinder.findById(requesterId)
|
||||
.also { reservationValidator.validateDeleteAuthority(reservation, requester = it) }
|
||||
|
||||
reservation.cancelByUser().also {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.domain.Specification
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
import java.time.LocalDate
|
||||
|
||||
class ReservationSearchSpecification(
|
||||
private var spec: Specification<ReservationEntity> = Specification { _, _, _ -> null }
|
||||
) {
|
||||
fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<ThemeEntity>("theme").get<Long>("id"), themeId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameMemberId(memberId: Long?): ReservationSearchSpecification = andIfNotNull(memberId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<MemberEntity>("member").get<Long>("id"), memberId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<TimeEntity>("time").get<Long>("id"), timeId)
|
||||
}
|
||||
})
|
||||
|
||||
fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.equal(root.get<LocalDate>("date"), date)
|
||||
}
|
||||
})
|
||||
|
||||
fun status(vararg statuses: ReservationStatus) = andIfNotNull { root, _, cb ->
|
||||
root.get<ReservationStatus>("status").`in`(statuses.toList())
|
||||
}
|
||||
|
||||
fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.or(
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.CONFIRMED
|
||||
),
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb ->
|
||||
cb.equal(
|
||||
root.get<ReservationStatus>("status"),
|
||||
ReservationStatus.WAITING
|
||||
)
|
||||
}
|
||||
|
||||
fun dateStartFrom(dateFrom: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateFrom?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.greaterThanOrEqualTo(root.get("date"), dateFrom)
|
||||
}
|
||||
})
|
||||
|
||||
fun dateEndAt(dateTo: LocalDate?): ReservationSearchSpecification = andIfNotNull(dateTo?.let {
|
||||
Specification { root, _, cb ->
|
||||
cb.lessThanOrEqualTo(root.get("date"), dateTo)
|
||||
}
|
||||
})
|
||||
|
||||
fun build(): Specification<ReservationEntity> {
|
||||
return this.spec
|
||||
}
|
||||
|
||||
private fun andIfNotNull(condition: Specification<ReservationEntity>?): ReservationSearchSpecification {
|
||||
condition?.let { this.spec = this.spec.and(condition) }
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.business.MyReservationFindService
|
||||
import roomescape.reservation.docs.MyReservationAPI
|
||||
|
||||
@RestController
|
||||
class MyReservationController(
|
||||
private val reservationFindService: MyReservationFindService
|
||||
) : MyReservationAPI {
|
||||
|
||||
@GetMapping("/v2/reservations")
|
||||
override fun findAllMyReservations(
|
||||
@MemberId @Parameter(hidden=true) memberId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationSummaryRetrieveListResponse>> {
|
||||
val response = reservationFindService.findReservationsByMemberId(memberId)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@GetMapping("/v2/reservations/{id}/details")
|
||||
override fun showReservationDetails(
|
||||
@PathVariable("id") reservationId: Long
|
||||
): ResponseEntity<CommonApiResponse<ReservationDetailRetrieveResponse>> {
|
||||
val response = reservationFindService.showReservationDetails(reservationId)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
}
|
||||
@ -1,196 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import roomescape.payment.infrastructure.persistence.v2.CanceledPaymentEntityV2
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentBankTransferDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentCardDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentEasypayPrepaidDetailEntity
|
||||
import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.PaymentDetailResponse.BankTransferDetailResponse
|
||||
import roomescape.reservation.web.PaymentDetailResponse.CardDetailResponse
|
||||
import roomescape.reservation.web.PaymentDetailResponse.EasyPayPrepaidDetailResponse
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.Int
|
||||
|
||||
data class ReservationSummaryRetrieveResponse(
|
||||
val id: Long,
|
||||
val themeName: String,
|
||||
val date: LocalDate,
|
||||
val startAt: LocalTime,
|
||||
val status: ReservationStatus
|
||||
)
|
||||
|
||||
fun ReservationEntity.toReservationSummaryRetrieveResponse(): ReservationSummaryRetrieveResponse {
|
||||
return ReservationSummaryRetrieveResponse(
|
||||
id = this.id!!,
|
||||
themeName = this.theme.name,
|
||||
date = this.date,
|
||||
startAt = this.time.startAt,
|
||||
status = this.status
|
||||
)
|
||||
}
|
||||
|
||||
data class ReservationSummaryRetrieveListResponse(
|
||||
val reservations: List<ReservationSummaryRetrieveResponse>
|
||||
)
|
||||
|
||||
fun List<ReservationEntity>.toSummaryListResponse(): ReservationSummaryRetrieveListResponse {
|
||||
return ReservationSummaryRetrieveListResponse(
|
||||
reservations = this.map { it.toReservationSummaryRetrieveResponse() }
|
||||
)
|
||||
}
|
||||
|
||||
data class ReservationDetailRetrieveResponse(
|
||||
val id: Long,
|
||||
val user: UserDetailRetrieveResponse,
|
||||
val themeName: String,
|
||||
val date: LocalDate,
|
||||
val startAt: LocalTime,
|
||||
val applicationDateTime: LocalDateTime,
|
||||
val payment: PaymentRetrieveResponse,
|
||||
val cancellation: PaymentCancelDetailResponse? = null
|
||||
)
|
||||
|
||||
data class UserDetailRetrieveResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val email: String
|
||||
)
|
||||
|
||||
fun MemberEntity.toUserDetailRetrieveResponse(): UserDetailRetrieveResponse {
|
||||
return UserDetailRetrieveResponse(
|
||||
id = this.id!!,
|
||||
name = this.name,
|
||||
email = this.email
|
||||
)
|
||||
}
|
||||
|
||||
fun ReservationEntity.toReservationDetailRetrieveResponse(
|
||||
payment: PaymentRetrieveResponse,
|
||||
cancellation: PaymentCancelDetailResponse? = null
|
||||
): ReservationDetailRetrieveResponse {
|
||||
return ReservationDetailRetrieveResponse(
|
||||
id = this.id!!,
|
||||
user = this.member.toUserDetailRetrieveResponse(),
|
||||
themeName = this.theme.name,
|
||||
date = this.date,
|
||||
startAt = this.time.startAt,
|
||||
applicationDateTime = this.createdAt!!,
|
||||
payment = payment,
|
||||
cancellation = cancellation,
|
||||
)
|
||||
}
|
||||
|
||||
data class PaymentRetrieveResponse(
|
||||
val orderId: String,
|
||||
val totalAmount: Int,
|
||||
val method: String,
|
||||
val status: PaymentStatus,
|
||||
val requestedAt: OffsetDateTime,
|
||||
val approvedAt: OffsetDateTime,
|
||||
val detail: PaymentDetailResponse,
|
||||
)
|
||||
|
||||
fun PaymentEntityV2.toRetrieveResponse(detail: PaymentDetailResponse): PaymentRetrieveResponse {
|
||||
return PaymentRetrieveResponse(
|
||||
orderId = this.orderId,
|
||||
totalAmount = this.totalAmount,
|
||||
method = this.method.koreanName,
|
||||
status = this.status,
|
||||
requestedAt = this.requestedAt,
|
||||
approvedAt = this.approvedAt,
|
||||
detail = detail
|
||||
)
|
||||
}
|
||||
|
||||
sealed class PaymentDetailResponse {
|
||||
|
||||
data class CardDetailResponse(
|
||||
val type: String = "CARD",
|
||||
val issuerCode: String,
|
||||
val cardType: String,
|
||||
val ownerType: String,
|
||||
val cardNumber: String,
|
||||
val amount: Int,
|
||||
val approvalNumber: String,
|
||||
val installmentPlanMonths: Int,
|
||||
val easypayProviderName: String?,
|
||||
val easypayDiscountAmount: Int?,
|
||||
) : PaymentDetailResponse()
|
||||
|
||||
|
||||
data class BankTransferDetailResponse(
|
||||
val type: String = "BANK_TRANSFER",
|
||||
val bankName: String,
|
||||
) : PaymentDetailResponse()
|
||||
|
||||
|
||||
data class EasyPayPrepaidDetailResponse(
|
||||
val type: String = "EASYPAY_PREPAID",
|
||||
val providerName: String,
|
||||
val amount: Int,
|
||||
val discountAmount: Int,
|
||||
) : PaymentDetailResponse()
|
||||
}
|
||||
|
||||
fun PaymentDetailEntity.toPaymentDetailResponse(): PaymentDetailResponse {
|
||||
return when (this) {
|
||||
is PaymentCardDetailEntity -> this.toCardDetailResponse()
|
||||
is PaymentBankTransferDetailEntity -> this.toBankTransferDetailResponse()
|
||||
is PaymentEasypayPrepaidDetailEntity -> this.toEasyPayPrepaidDetailResponse()
|
||||
else -> throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE)
|
||||
}
|
||||
}
|
||||
|
||||
fun PaymentCardDetailEntity.toCardDetailResponse(): CardDetailResponse {
|
||||
return CardDetailResponse(
|
||||
issuerCode = this.issuerCode.koreanName,
|
||||
cardType = this.cardType.koreanName,
|
||||
ownerType = this.ownerType.koreanName,
|
||||
cardNumber = this.cardNumber,
|
||||
amount = this.amount,
|
||||
approvalNumber = this.approvalNumber,
|
||||
installmentPlanMonths = this.installmentPlanMonths,
|
||||
easypayProviderName = this.easypayProviderCode?.koreanName,
|
||||
easypayDiscountAmount = this.easypayDiscountAmount
|
||||
)
|
||||
}
|
||||
|
||||
fun PaymentBankTransferDetailEntity.toBankTransferDetailResponse(): BankTransferDetailResponse {
|
||||
return BankTransferDetailResponse(
|
||||
bankName = this.bankCode.koreanName
|
||||
)
|
||||
}
|
||||
|
||||
fun PaymentEasypayPrepaidDetailEntity.toEasyPayPrepaidDetailResponse(): EasyPayPrepaidDetailResponse {
|
||||
return EasyPayPrepaidDetailResponse(
|
||||
providerName = this.easypayProviderCode.koreanName,
|
||||
amount = this.amount,
|
||||
discountAmount = this.discountAmount
|
||||
)
|
||||
}
|
||||
|
||||
data class PaymentCancelDetailResponse(
|
||||
val cancellationRequestedAt: LocalDateTime,
|
||||
val cancellationApprovedAt: OffsetDateTime?,
|
||||
val cancelReason: String,
|
||||
val canceledBy: Long,
|
||||
)
|
||||
|
||||
fun CanceledPaymentEntityV2.toCancelDetailResponse(): PaymentCancelDetailResponse {
|
||||
return PaymentCancelDetailResponse(
|
||||
cancellationRequestedAt = this.requestedAt,
|
||||
cancellationApprovedAt = this.canceledAt,
|
||||
cancelReason = this.cancelReason,
|
||||
canceledBy = this.canceledBy
|
||||
)
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.payment.infrastructure.client.PaymentApproveRequest
|
||||
import java.time.LocalDate
|
||||
|
||||
data class AdminReservationCreateRequest(
|
||||
val date: LocalDate,
|
||||
val timeId: Long,
|
||||
val themeId: Long,
|
||||
val memberId: Long
|
||||
)
|
||||
|
||||
data class ReservationCreateWithPaymentRequest(
|
||||
val date: LocalDate,
|
||||
val timeId: Long,
|
||||
val themeId: Long,
|
||||
|
||||
@Schema(description = "결제 위젯을 통해 받은 결제 키")
|
||||
val paymentKey: String,
|
||||
|
||||
@Schema(description = "결제 위젯을 통해 받은 주문번호.")
|
||||
val orderId: String,
|
||||
|
||||
@Schema(description = "결제 위젯을 통해 받은 결제 금액")
|
||||
val amount: Long,
|
||||
|
||||
@Schema(description = "결제 타입", example = "NORMAL")
|
||||
val paymentType: String
|
||||
)
|
||||
|
||||
fun ReservationCreateWithPaymentRequest.toPaymentApproveRequest(): PaymentApproveRequest = PaymentApproveRequest(
|
||||
paymentKey, orderId, amount, paymentType
|
||||
)
|
||||
|
||||
data class WaitingCreateRequest(
|
||||
val date: LocalDate,
|
||||
val timeId: Long,
|
||||
val themeId: Long
|
||||
)
|
||||
@ -1,93 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import roomescape.member.web.MemberRetrieveResponse
|
||||
import roomescape.member.web.toRetrieveResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.theme.web.ThemeRetrieveResponse
|
||||
import roomescape.theme.web.toRetrieveResponse
|
||||
import roomescape.time.web.TimeCreateResponse
|
||||
import roomescape.time.web.toCreateResponse
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
data class ReservationCreateResponse(
|
||||
val id: Long,
|
||||
val date: LocalDate,
|
||||
|
||||
@JsonProperty("member")
|
||||
val member: MemberRetrieveResponse,
|
||||
|
||||
@JsonProperty("time")
|
||||
val time: TimeCreateResponse,
|
||||
|
||||
@JsonProperty("theme")
|
||||
val theme: ThemeRetrieveResponse,
|
||||
|
||||
val status: ReservationStatus
|
||||
)
|
||||
|
||||
fun ReservationEntity.toCreateResponse() = ReservationCreateResponse(
|
||||
id = this.id!!,
|
||||
date = this.date,
|
||||
member = this.member.toRetrieveResponse(),
|
||||
time = this.time.toCreateResponse(),
|
||||
theme = this.theme.toRetrieveResponse(),
|
||||
status = this.status
|
||||
)
|
||||
|
||||
data class MyReservationRetrieveResponse(
|
||||
val id: Long,
|
||||
val themeName: String,
|
||||
val date: LocalDate,
|
||||
val time: LocalTime,
|
||||
val status: ReservationStatus,
|
||||
@Schema(description = "대기 순번. 확정된 예약은 0의 값을 가집니다.")
|
||||
val rank: Long,
|
||||
@Schema(description = "결제 키. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
val paymentKey: String?,
|
||||
@Schema(description = "결제 금액. 결제가 완료된 예약에만 값이 존재합니다.")
|
||||
val amount: Long?
|
||||
)
|
||||
|
||||
data class MyReservationRetrieveListResponse(
|
||||
@Schema(description = "현재 로그인한 회원의 예약 및 대기 목록")
|
||||
val reservations: List<MyReservationRetrieveResponse>
|
||||
)
|
||||
|
||||
fun List<MyReservationRetrieveResponse>.toRetrieveListResponse() = MyReservationRetrieveListResponse(this)
|
||||
|
||||
data class ReservationRetrieveResponse(
|
||||
val id: Long,
|
||||
val date: LocalDate,
|
||||
|
||||
@JsonProperty("member")
|
||||
val member: MemberRetrieveResponse,
|
||||
|
||||
@JsonProperty("time")
|
||||
val time: TimeCreateResponse,
|
||||
|
||||
@JsonProperty("theme")
|
||||
val theme: ThemeRetrieveResponse,
|
||||
|
||||
val status: ReservationStatus
|
||||
)
|
||||
|
||||
fun ReservationEntity.toRetrieveResponse(): ReservationRetrieveResponse = ReservationRetrieveResponse(
|
||||
id = this.id!!,
|
||||
date = this.date,
|
||||
member = this.member.toRetrieveResponse(),
|
||||
time = this.time.toCreateResponse(),
|
||||
theme = this.theme.toRetrieveResponse(),
|
||||
status = this.status
|
||||
)
|
||||
|
||||
data class ReservationRetrieveListResponse(
|
||||
val reservations: List<ReservationRetrieveResponse>
|
||||
)
|
||||
|
||||
fun List<ReservationEntity>.toRetrieveListResponse()= ReservationRetrieveListResponse(
|
||||
this.map { it.toRetrieveResponse() }
|
||||
)
|
||||
@ -1,60 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.PatchMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import roomescape.auth.web.support.MemberId
|
||||
import roomescape.common.dto.response.CommonApiResponse
|
||||
import roomescape.reservation.business.ReservationWithPaymentService
|
||||
import roomescape.reservation.business.ReservationWithPaymentServiceV2
|
||||
import roomescape.reservation.docs.ReservationWithPaymentAPI
|
||||
|
||||
@RestController
|
||||
class ReservationWithPaymentController(
|
||||
private val reservationWithPaymentService: ReservationWithPaymentServiceV2
|
||||
) : ReservationWithPaymentAPI {
|
||||
|
||||
@PostMapping("/v2/reservations")
|
||||
override fun createPendingReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@Valid @RequestBody reservationCreateWithPaymentRequest: ReservationCreateRequest
|
||||
): ResponseEntity<CommonApiResponse<ReservationCreateResponseV2>> {
|
||||
val response = reservationWithPaymentService.createPendingReservation(
|
||||
memberId = memberId,
|
||||
request = reservationCreateWithPaymentRequest
|
||||
)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping("/v2/reservations/{id}/pay")
|
||||
override fun createPaymentAndConfirmReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long,
|
||||
@Valid @RequestBody request: ReservationPaymentRequest,
|
||||
): ResponseEntity<CommonApiResponse<ReservationPaymentResponse>> {
|
||||
val response = reservationWithPaymentService.payReservation(
|
||||
memberId = memberId,
|
||||
reservationId = reservationId,
|
||||
request = request
|
||||
)
|
||||
|
||||
return ResponseEntity.ok(CommonApiResponse(response))
|
||||
}
|
||||
|
||||
@PostMapping("/v2/reservations/{id}/cancel")
|
||||
override fun cancelReservation(
|
||||
@MemberId @Parameter(hidden = true) memberId: Long,
|
||||
@PathVariable("id") reservationId: Long,
|
||||
@Valid @RequestBody cancelRequest: ReservationCancelRequest
|
||||
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||
reservationWithPaymentService.cancelReservation(memberId, reservationId, cancelRequest)
|
||||
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import roomescape.payment.infrastructure.client.v2.PaymentConfirmRequest
|
||||
import roomescape.payment.infrastructure.common.PaymentStatus
|
||||
import roomescape.payment.infrastructure.common.PaymentType
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
|
||||
data class ReservationCreateRequest(
|
||||
val date: LocalDate,
|
||||
val timeId: Long,
|
||||
val themeId: Long,
|
||||
)
|
||||
|
||||
data class ReservationCreateResponseV2(
|
||||
val reservationId: Long,
|
||||
val memberEmail: String,
|
||||
val date: LocalDate,
|
||||
val startAt: LocalTime,
|
||||
val themeName: String
|
||||
)
|
||||
|
||||
fun ReservationEntity.toCreateResponseV2() = ReservationCreateResponseV2(
|
||||
reservationId = this.id!!,
|
||||
memberEmail = this.member.email,
|
||||
date = this.date,
|
||||
startAt = this.time.startAt,
|
||||
themeName = this.theme.name
|
||||
)
|
||||
|
||||
data class ReservationPaymentRequest(
|
||||
val paymentKey: String,
|
||||
val orderId: String,
|
||||
val amount: Int,
|
||||
val paymentType: PaymentType
|
||||
)
|
||||
|
||||
fun ReservationPaymentRequest.toPaymentConfirmRequest() = PaymentConfirmRequest(
|
||||
paymentKey = this.paymentKey,
|
||||
amount = this.amount,
|
||||
orderId = this.orderId,
|
||||
)
|
||||
|
||||
data class ReservationPaymentResponse(
|
||||
val reservationId: Long,
|
||||
val reservationStatus: ReservationStatus,
|
||||
val paymentId: Long,
|
||||
val paymentStatus: PaymentStatus,
|
||||
)
|
||||
|
||||
data class ReservationCancelRequest(
|
||||
val cancelReason: String,
|
||||
val requestedAt: LocalDateTime = LocalDateTime.now()
|
||||
)
|
||||
@ -1,32 +0,0 @@
|
||||
package roomescape.reservation_v2.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.EnumType
|
||||
import jakarta.persistence.Enumerated
|
||||
import jakarta.persistence.Table
|
||||
import roomescape.common.entity.AuditingBaseEntity
|
||||
|
||||
@Entity
|
||||
@Table(name = "reservation")
|
||||
class ReservationEntityV2(
|
||||
id: Long,
|
||||
|
||||
val memberId: Long,
|
||||
val scheduleId: Long,
|
||||
val reserverName: String,
|
||||
val reserverContact: String,
|
||||
val participantCount: Short,
|
||||
val requirement: String,
|
||||
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var status: ReservationStatusV2,
|
||||
|
||||
): AuditingBaseEntity(id) {
|
||||
fun confirm() { this.status = ReservationStatusV2.CONFIRMED }
|
||||
fun fail() { this.status = ReservationStatusV2.FAILED }
|
||||
fun cancel() { this.status = ReservationStatusV2.CANCELED }
|
||||
}
|
||||
|
||||
enum class ReservationStatusV2 {
|
||||
PENDING, CONFIRMED, CANCELED, FAILED, EXPIRED
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package roomescape.reservation_v2.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface ReservationRepositoryV2 : JpaRepository<ReservationEntityV2, Long> {
|
||||
|
||||
fun findAllByMemberId(memberId: Long): List<ReservationEntityV2>
|
||||
}
|
||||
@ -1,122 +0,0 @@
|
||||
package roomescape.reservation.business
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import roomescape.payment.business.PaymentService
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.payment.web.PaymentCancelRequest
|
||||
import roomescape.payment.web.toCreateResponse
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.reservation.web.ReservationCreateResponse
|
||||
import roomescape.reservation.web.ReservationCreateWithPaymentRequest
|
||||
import roomescape.util.*
|
||||
|
||||
class ReservationWithPaymentServiceTest : FunSpec({
|
||||
val reservationService: ReservationWriteService = mockk()
|
||||
val paymentService: PaymentService = mockk()
|
||||
|
||||
val reservationWithPaymentService = ReservationWithPaymentService(
|
||||
reservationWriteService = reservationService,
|
||||
paymentService = paymentService
|
||||
)
|
||||
|
||||
val reservationCreateWithPaymentRequest: ReservationCreateWithPaymentRequest = ReservationFixture.createRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse()
|
||||
val memberId = 1L
|
||||
val reservationEntity: ReservationEntity = ReservationFixture.create(
|
||||
id = 1L,
|
||||
date = reservationCreateWithPaymentRequest.date,
|
||||
time = TimeFixture.create(id = reservationCreateWithPaymentRequest.timeId),
|
||||
theme = ThemeFixture.create(id = reservationCreateWithPaymentRequest.themeId),
|
||||
member = MemberFixture.create(id = memberId),
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
val paymentEntity: PaymentEntity = PaymentFixture.create(
|
||||
id = 1L,
|
||||
orderId = reservationCreateWithPaymentRequest.orderId,
|
||||
paymentKey = reservationCreateWithPaymentRequest.paymentKey,
|
||||
totalAmount = reservationCreateWithPaymentRequest.amount,
|
||||
reservation = reservationEntity,
|
||||
)
|
||||
|
||||
context("addReservationWithPayment") {
|
||||
test("예약 및 결제 정보를 저장한다.") {
|
||||
every {
|
||||
reservationService.createReservationWithPayment(reservationCreateWithPaymentRequest, memberId)
|
||||
} returns reservationEntity
|
||||
|
||||
every {
|
||||
paymentService.createPayment(paymentApproveResponse, reservationEntity)
|
||||
} returns paymentEntity.toCreateResponse()
|
||||
|
||||
val result: ReservationCreateResponse = reservationWithPaymentService.createReservationAndPayment(
|
||||
request = reservationCreateWithPaymentRequest,
|
||||
approvedPaymentInfo = paymentApproveResponse,
|
||||
memberId = memberId
|
||||
)
|
||||
|
||||
assertSoftly(result) {
|
||||
this.id shouldBe reservationEntity.id
|
||||
this.date shouldBe reservationEntity.date
|
||||
this.member.id shouldBe reservationEntity.member.id
|
||||
this.time.id shouldBe reservationEntity.time.id
|
||||
this.theme.id shouldBe reservationEntity.theme.id
|
||||
this.status shouldBe ReservationStatus.CONFIRMED
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context("removeReservationWithPayment") {
|
||||
test("예약 및 결제 정보를 삭제하고, 결제 취소 정보를 저장한다.") {
|
||||
val paymentCancelRequest: PaymentCancelRequest = PaymentFixture.createCancelRequest().copy(
|
||||
paymentKey = paymentEntity.paymentKey,
|
||||
amount = paymentEntity.totalAmount,
|
||||
cancelReason = "고객 요청"
|
||||
)
|
||||
|
||||
every {
|
||||
paymentService.createCanceledPayment(reservationEntity.id!!)
|
||||
} returns paymentCancelRequest
|
||||
|
||||
every {
|
||||
reservationService.deleteReservation(reservationEntity.id!!, reservationEntity.member.id!!)
|
||||
} just Runs
|
||||
|
||||
val result: PaymentCancelRequest = reservationWithPaymentService.deleteReservationAndPayment(
|
||||
reservationId = reservationEntity.id!!,
|
||||
memberId = reservationEntity.member.id!!
|
||||
)
|
||||
|
||||
result shouldBe paymentCancelRequest
|
||||
}
|
||||
}
|
||||
|
||||
context("isNotPaidReservation") {
|
||||
test("결제된 예약이면 true를 반환한다.") {
|
||||
every {
|
||||
paymentService.existsByReservationId(reservationEntity.id!!)
|
||||
} returns false
|
||||
|
||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||
|
||||
result shouldBe true
|
||||
}
|
||||
|
||||
test("결제되지 않은 예약이면 false를 반환한다.") {
|
||||
every {
|
||||
paymentService.existsByReservationId(reservationEntity.id!!)
|
||||
} returns true
|
||||
|
||||
val result: Boolean = reservationWithPaymentService.isNotPaidReservation(reservationEntity.id!!)
|
||||
|
||||
result shouldBe false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,205 +0,0 @@
|
||||
package roomescape.reservation.infrastructure.persistence
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.web.MyReservationRetrieveResponse
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
import roomescape.util.ThemeFixture
|
||||
import roomescape.util.TimeFixture
|
||||
|
||||
@DataJpaTest(showSql = false)
|
||||
class ReservationRepositoryTest(
|
||||
val entityManager: EntityManager,
|
||||
val reservationRepository: ReservationRepository,
|
||||
) : FunSpec() {
|
||||
init {
|
||||
context("findByTime") {
|
||||
val time = TimeFixture.create()
|
||||
|
||||
beforeTest {
|
||||
listOf(
|
||||
ReservationFixture.create(time = time),
|
||||
ReservationFixture.create(
|
||||
time = TimeFixture.create(
|
||||
startAt = time.startAt.plusSeconds(1)
|
||||
)
|
||||
)
|
||||
).forEach {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("입력된 시간과 일치하는 예약을 반환한다.") {
|
||||
assertSoftly(reservationRepository.findAllByTime(time)) {
|
||||
it shouldHaveSize 1
|
||||
assertSoftly(it.first().time.startAt) { result ->
|
||||
result.hour shouldBe time.startAt.hour
|
||||
result.minute shouldBe time.startAt.minute
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("findByDateAndThemeId") {
|
||||
val date = ReservationFixture.create().date
|
||||
lateinit var theme1: ThemeEntity
|
||||
lateinit var theme2: ThemeEntity
|
||||
|
||||
beforeTest {
|
||||
theme1 = ThemeFixture.create(name = "theme1").also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
theme2 = ThemeFixture.create(name = "theme2").also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
|
||||
listOf(
|
||||
ReservationFixture.create(date = date, theme = theme1),
|
||||
ReservationFixture.create(date = date.plusDays(1), theme = theme1),
|
||||
ReservationFixture.create(date = date, theme = theme2),
|
||||
).forEach {
|
||||
entityManager.persist(it.time)
|
||||
entityManager.persist(it.member)
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
test("입력된 날짜와 테마 ID에 해당하는 예약을 반환한다.") {
|
||||
assertSoftly(reservationRepository.findByDateAndThemeId(date, theme1.id!!)) {
|
||||
it shouldHaveSize 1
|
||||
it.first().theme shouldBe theme1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("updateStatusByReservationId") {
|
||||
lateinit var reservation: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
reservation = ReservationFixture.create().also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("예약 상태를 업데이트한다.") {
|
||||
ReservationStatus.entries.forEach {
|
||||
val reservationId = reservation.id!!
|
||||
val updatedRows = reservationRepository.updateStatusByReservationId(reservationId, it)
|
||||
updatedRows shouldBe 1
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
reservationRepository.findByIdOrNull(reservationId)?.also { updated ->
|
||||
updated.status shouldBe it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("isExistConfirmedReservation") {
|
||||
lateinit var waiting: ReservationEntity
|
||||
lateinit var confirmed: ReservationEntity
|
||||
lateinit var confirmedPaymentRequired: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
waiting = ReservationFixture.create(status = ReservationStatus.WAITING).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
confirmed = ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
confirmedPaymentRequired =
|
||||
ReservationFixture.create(status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED).also {
|
||||
persistReservation(it)
|
||||
}
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
test("예약이 없으면 false를 반환한다.") {
|
||||
val maxId: Long = listOf(waiting, confirmed, confirmedPaymentRequired)
|
||||
.maxOfOrNull { it.id ?: 0L } ?: 0L
|
||||
reservationRepository.isExistConfirmedReservation(maxId + 1L) shouldBe false
|
||||
}
|
||||
|
||||
test("예약이 대기중이면 false를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(waiting.id!!) shouldBe false
|
||||
}
|
||||
|
||||
test("예약이 결제 완료 상태이면 true를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(confirmed.id!!) shouldBe true
|
||||
}
|
||||
|
||||
test("예약이 결제 대기 상태이면 true를 반환한다.") {
|
||||
reservationRepository.isExistConfirmedReservation(confirmedPaymentRequired.id!!) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
context("findMyReservations") {
|
||||
lateinit var reservation: ReservationEntity
|
||||
|
||||
beforeTest {
|
||||
reservation = ReservationFixture.create()
|
||||
persistReservation(reservation)
|
||||
}
|
||||
|
||||
test("결제 정보를 포함한 회원의 예약 목록을 반환한다.") {
|
||||
val payment: PaymentEntity = PaymentFixture.create(
|
||||
reservation = reservation
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}
|
||||
|
||||
val result: List<MyReservationRetrieveResponse> =
|
||||
reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
it.id shouldBe reservation.id
|
||||
it.paymentKey shouldBe payment.paymentKey
|
||||
it.amount shouldBe payment.totalAmount
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 정보가 없다면 paymentKey와 amount는 null로 반환한다.") {
|
||||
val result: List<MyReservationRetrieveResponse> =
|
||||
reservationRepository.findAllByMemberId(reservation.member.id!!)
|
||||
|
||||
result shouldHaveSize 1
|
||||
assertSoftly(result.first()) {
|
||||
it.id shouldBe reservation.id
|
||||
it.paymentKey shouldBe null
|
||||
it.amount shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun persistReservation(reservation: ReservationEntity) {
|
||||
entityManager.persist(reservation.time)
|
||||
entityManager.persist(reservation.theme)
|
||||
entityManager.persist(reservation.member)
|
||||
entityManager.persist(reservation)
|
||||
}
|
||||
}
|
||||
@ -1,730 +0,0 @@
|
||||
package roomescape.reservation.web
|
||||
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import com.ninjasquad.springmockk.SpykBean
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.restassured.module.kotlin.extensions.Given
|
||||
import io.restassured.module.kotlin.extensions.Then
|
||||
import io.restassured.module.kotlin.extensions.When
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.hamcrest.Matchers.equalTo
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import roomescape.auth.exception.AuthErrorCode
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.support.MemberIdResolver
|
||||
import roomescape.member.business.MemberService
|
||||
import roomescape.member.infrastructure.persistence.MemberEntity
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.payment.exception.PaymentErrorCode
|
||||
import roomescape.payment.exception.PaymentException
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.exception.ReservationErrorCode
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationEntity
|
||||
import roomescape.reservation.infrastructure.persistence.ReservationStatus
|
||||
import roomescape.theme.exception.ThemeErrorCode
|
||||
import roomescape.theme.infrastructure.persistence.ThemeEntity
|
||||
import roomescape.time.infrastructure.persistence.TimeEntity
|
||||
import roomescape.util.*
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class ReservationControllerTest(
|
||||
@LocalServerPort val port: Int,
|
||||
val entityManager: EntityManager,
|
||||
val transactionTemplate: TransactionTemplate,
|
||||
) : FunSpec({
|
||||
extension(DatabaseCleanerExtension(mode = CleanerMode.AFTER_EACH_TEST))
|
||||
}) {
|
||||
@MockkBean
|
||||
lateinit var paymentClient: TossPaymentClient
|
||||
|
||||
@SpykBean
|
||||
lateinit var memberIdResolver: MemberIdResolver
|
||||
|
||||
@SpykBean
|
||||
lateinit var memberService: MemberService
|
||||
|
||||
@MockkBean
|
||||
lateinit var jwtHandler: JwtHandler
|
||||
|
||||
lateinit var testDataHelper: TestDataHelper
|
||||
|
||||
fun login(member: MemberEntity) {
|
||||
every { jwtHandler.getMemberIdFromToken(any()) } returns member.id!!
|
||||
every { memberService.findById(member.id!!) } returns member
|
||||
every { memberIdResolver.resolveArgument(any(), any(), any(), any()) } returns member.id!!
|
||||
}
|
||||
|
||||
init {
|
||||
beforeSpec {
|
||||
testDataHelper = TestDataHelper(entityManager, transactionTemplate)
|
||||
}
|
||||
|
||||
context("POST /reservations") {
|
||||
beforeTest {
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
}
|
||||
|
||||
test("정상 응답") {
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every { paymentClient.confirm(any()) } returns paymentApproveResponse
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(reservationRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
statusCode(201)
|
||||
body("data.date", equalTo(reservationRequest.date.toString()))
|
||||
body("data.status", equalTo(ReservationStatus.CONFIRMED.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 과정에서 발생하는 에러는 그대로 응답") {
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentException = PaymentException(PaymentErrorCode.PAYMENT_PROVIDER_ERROR)
|
||||
|
||||
every { paymentClient.confirm(any()) } throws paymentException
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(reservationRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
statusCode(paymentException.errorCode.httpStatus.value())
|
||||
body("code", equalTo(paymentException.errorCode.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제 완료 후 예약 / 결제 정보 저장 과정에서 에러 발생시 결제 취소 후 에러 응답을 받는다.") {
|
||||
val reservationRequest = testDataHelper.createReservationRequest()
|
||||
val paymentApproveResponse = PaymentFixture.createApproveResponse().copy(
|
||||
paymentKey = reservationRequest.paymentKey,
|
||||
orderId = reservationRequest.orderId,
|
||||
totalAmount = reservationRequest.amount,
|
||||
)
|
||||
|
||||
every { paymentClient.confirm(any()) } returns paymentApproveResponse
|
||||
|
||||
// 예약 저장 과정에서 테마가 없는 예외
|
||||
val invalidRequest = reservationRequest.copy(themeId = reservationRequest.themeId + 1)
|
||||
val expectedException = ThemeErrorCode.THEME_NOT_FOUND
|
||||
|
||||
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(invalidRequest)
|
||||
}.When {
|
||||
post("/reservations")
|
||||
}.Then {
|
||||
statusCode(expectedException.httpStatus.value())
|
||||
body("code", equalTo(expectedException.errorCode))
|
||||
}
|
||||
|
||||
val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations") {
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자이면 정상 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations-mine") {
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("로그인한 회원이 자신의 예약 목록을 조회한다.") {
|
||||
val member = reservations.keys.first()
|
||||
login(member)
|
||||
val expectedReservations: Int = reservations[member]?.size ?: 0
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations-mine")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(expectedReservations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations/search") {
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 검색할 수 있다.") {
|
||||
login(reservations.keys.first())
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("파라미터를 지정하지 않으면 전체 목록 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
|
||||
test("시작 날짜가 종료 날짜 이전이면 예외 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
|
||||
val startDate = LocalDate.now().plusDays(1)
|
||||
val endDate = LocalDate.now()
|
||||
val expectedError = ReservationErrorCode.INVALID_SEARCH_DATE_RANGE
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("dateFrom", startDate.toString())
|
||||
param("dateTo", endDate.toString())
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("동일한 회원의 모든 예약 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val member = reservations.keys.first()
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("memberId", member.id)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations[member]?.size ?: 0))
|
||||
}
|
||||
}
|
||||
|
||||
test("동일한 테마의 모든 예약 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val themes = reservations.values.flatten().map { it.theme }
|
||||
val requestThemeId: Long = themes.first().id!!
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("themeId", requestThemeId)
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(themes.count { it.id == requestThemeId }))
|
||||
}
|
||||
}
|
||||
|
||||
test("시작 날짜와 종료 날짜 사이의 예약 응답") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val dateFrom: LocalDate = reservations.values.flatten().minOf { it.date }
|
||||
val dateTo: LocalDate = reservations.values.flatten().maxOf { it.date }
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
param("dateFrom", dateFrom.toString())
|
||||
param("dateTo", dateTo.toString())
|
||||
}.When {
|
||||
get("/reservations/search")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(reservations.values.sumOf { it.size }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("DELETE /reservations/{id}") {
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = testDataHelper.createDummyReservations()
|
||||
}
|
||||
|
||||
test("관리자만 예약을 삭제할 수 있다.") {
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val reservation = reservations.values.flatten().first()
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/${reservation.id}")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("결제되지 않은 예약은 바로 제거") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservationId = reservations.values.flatten().first().id!!
|
||||
|
||||
transactionTemplate.executeWithoutResult {
|
||||
val reservation = entityManager.find(ReservationEntity::class.java, reservationId)
|
||||
reservation.status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
}
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/$reservationId")
|
||||
}.Then {
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
val deletedReservation = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservationId)
|
||||
}
|
||||
deletedReservation shouldBe null
|
||||
}
|
||||
|
||||
test("결제된 예약은 취소 후 제거") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = reservations.values.flatten().first { it.status == ReservationStatus.CONFIRMED }
|
||||
testDataHelper.createPayment(reservation)
|
||||
|
||||
every { paymentClient.cancel(any()) } returns PaymentFixture.createCancelResponse()
|
||||
|
||||
val canceledPaymentSizeBeforeApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/${reservation.id}")
|
||||
}.Then {
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
val canceledPaymentSizeAfterApiCall: Long = entityManager.createQuery(
|
||||
"SELECT COUNT(c) FROM CanceledPaymentEntity c",
|
||||
Long::class.java
|
||||
).singleResult
|
||||
|
||||
canceledPaymentSizeAfterApiCall shouldBe canceledPaymentSizeBeforeApiCall + 1L
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/admin") {
|
||||
test("관리자가 예약을 추가하면 결제 대기 상태로 예약 생성") {
|
||||
val admin = testDataHelper.createMember(role = Role.ADMIN)
|
||||
login(admin)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
|
||||
val adminRequest = AdminReservationCreateRequest(
|
||||
date = LocalDate.now().plusDays(1),
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
memberId = admin.id!!,
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(adminRequest)
|
||||
}.When {
|
||||
post("/reservations/admin")
|
||||
}.Then {
|
||||
statusCode(201)
|
||||
body("data.status", equalTo(ReservationStatus.CONFIRMED_PAYMENT_REQUIRED.name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("GET /reservations/waiting") {
|
||||
lateinit var reservations: Map<MemberEntity, List<ReservationEntity>>
|
||||
beforeTest {
|
||||
reservations = testDataHelper.createDummyReservations(reservationCount = 5)
|
||||
}
|
||||
|
||||
test("관리자가 아니면 조회할 수 없다.") {
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/waiting")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("대기 중인 예약 목록을 조회한다.") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val expected = reservations.values.flatten()
|
||||
.count { it.status == ReservationStatus.WAITING }
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
}.When {
|
||||
get("/reservations/waiting")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
body("data.reservations.size()", equalTo(expected))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting") {
|
||||
test("회원이 대기 예약을 추가한다.") {
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
|
||||
val waitingCreateRequest = WaitingCreateRequest(
|
||||
date = LocalDate.now().plusDays(1),
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(waitingCreateRequest)
|
||||
}.When {
|
||||
post("/reservations/waiting")
|
||||
}.Then {
|
||||
statusCode(201)
|
||||
body("data.member.id", equalTo(member.id!!.toString()))
|
||||
body("data.status", equalTo(ReservationStatus.WAITING.name))
|
||||
}
|
||||
}
|
||||
|
||||
test("이미 예약된 시간, 테마로 대기 예약 요청 시 예외 응답") {
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val theme = testDataHelper.createTheme()
|
||||
val time = testDataHelper.createTime()
|
||||
val date = LocalDate.now().plusDays(1)
|
||||
|
||||
testDataHelper.createReservation(
|
||||
date = date,
|
||||
theme = theme,
|
||||
time = time,
|
||||
member = member,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
|
||||
val waitingCreateRequest = WaitingCreateRequest(
|
||||
date = date,
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!
|
||||
)
|
||||
val expectedError = ReservationErrorCode.ALREADY_RESERVE
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||
body(waitingCreateRequest)
|
||||
}.When {
|
||||
post("/reservations/waiting")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("DELETE /reservations/waiting/{id}") {
|
||||
test("대기 중인 예약을 취소한다.") {
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val waiting = testDataHelper.createReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/waiting/${waiting.id}")
|
||||
}.Then {
|
||||
statusCode(HttpStatus.NO_CONTENT.value())
|
||||
}
|
||||
|
||||
val deleted = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, waiting.id)
|
||||
}
|
||||
deleted shouldBe null
|
||||
}
|
||||
|
||||
test("이미 확정된 예약을 삭제하면 예외 응답") {
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
login(member)
|
||||
val reservation = testDataHelper.createReservation(
|
||||
member = member,
|
||||
status = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
)
|
||||
|
||||
val expectedError = ReservationErrorCode.ALREADY_CONFIRMED
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
delete("/reservations/waiting/${reservation.id}")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting/{id}/confirm") {
|
||||
test("관리자만 승인할 수 있다.") {
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/1/confirm")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("대기 예약을 승인하면 결제 대기 상태로 변경") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = testDataHelper.createReservation(
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/${reservation.id!!}/confirm")
|
||||
}.Then {
|
||||
statusCode(200)
|
||||
}
|
||||
|
||||
val updatedReservation = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservation.id)
|
||||
}
|
||||
updatedReservation?.status shouldBe ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
}
|
||||
|
||||
test("다른 확정된 예약을 승인하면 예외 응답") {
|
||||
val admin = testDataHelper.createMember(role = Role.ADMIN)
|
||||
login(admin)
|
||||
val alreadyReserved = testDataHelper.createReservation(
|
||||
member = admin,
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
|
||||
val member = testDataHelper.createMember(role = Role.MEMBER)
|
||||
val waiting = testDataHelper.createReservation(
|
||||
date = alreadyReserved.date,
|
||||
time = alreadyReserved.time,
|
||||
theme = alreadyReserved.theme,
|
||||
member = member,
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
val expectedError = ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/${waiting.id!!}/confirm")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("POST /reservations/waiting/{id}/reject") {
|
||||
test("관리자만 거절할 수 있다.") {
|
||||
login(testDataHelper.createMember(role = Role.MEMBER))
|
||||
val expectedError = AuthErrorCode.ACCESS_DENIED
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/1/reject")
|
||||
}.Then {
|
||||
statusCode(expectedError.httpStatus.value())
|
||||
body("code", equalTo(expectedError.errorCode))
|
||||
}
|
||||
}
|
||||
|
||||
test("거절된 예약은 삭제된다.") {
|
||||
login(testDataHelper.createMember(role = Role.ADMIN))
|
||||
val reservation = testDataHelper.createReservation(
|
||||
status = ReservationStatus.WAITING
|
||||
)
|
||||
|
||||
Given {
|
||||
port(port)
|
||||
}.When {
|
||||
post("/reservations/waiting/${reservation.id!!}/reject")
|
||||
}.Then {
|
||||
statusCode(204)
|
||||
}
|
||||
|
||||
val rejected = transactionTemplate.execute {
|
||||
entityManager.find(ReservationEntity::class.java, reservation.id)
|
||||
}
|
||||
rejected shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestDataHelper(
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionTemplate: TransactionTemplate,
|
||||
) {
|
||||
private var memberSequence = 0L
|
||||
private var themeSequence = 0L
|
||||
private var timeSequence = 0L
|
||||
|
||||
fun createMember(
|
||||
role: Role = Role.MEMBER,
|
||||
account: String = "member${++memberSequence}@test.com",
|
||||
): MemberEntity {
|
||||
val member = MemberFixture.create(role = role, account = account)
|
||||
return persist(member)
|
||||
}
|
||||
|
||||
fun createTheme(name: String = "theme-${++themeSequence}"): ThemeEntity {
|
||||
val theme = ThemeFixture.create(name = name)
|
||||
return persist(theme)
|
||||
}
|
||||
|
||||
fun createTime(startAt: LocalTime = LocalTime.of(10, 0).plusMinutes(++timeSequence * 10)): TimeEntity {
|
||||
val time = TimeFixture.create(startAt = startAt)
|
||||
return persist(time)
|
||||
}
|
||||
|
||||
fun createReservation(
|
||||
date: LocalDate = LocalDate.now().plusDays(1),
|
||||
theme: ThemeEntity = createTheme(),
|
||||
time: TimeEntity = createTime(),
|
||||
member: MemberEntity = createMember(),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED,
|
||||
): ReservationEntity {
|
||||
val reservation = ReservationFixture.create(
|
||||
date = date,
|
||||
theme = theme,
|
||||
time = time,
|
||||
member = member,
|
||||
status = status
|
||||
)
|
||||
return persist(reservation)
|
||||
}
|
||||
|
||||
fun createPayment(reservation: ReservationEntity): PaymentEntity {
|
||||
val payment = PaymentFixture.create(reservation = reservation)
|
||||
return persist(payment)
|
||||
}
|
||||
|
||||
fun createReservationRequest(
|
||||
theme: ThemeEntity = createTheme(),
|
||||
time: TimeEntity = createTime(),
|
||||
): ReservationCreateWithPaymentRequest {
|
||||
return ReservationFixture.createRequest(
|
||||
themeId = theme.id!!,
|
||||
timeId = time.id!!,
|
||||
)
|
||||
}
|
||||
|
||||
fun createDummyReservations(
|
||||
memberCount: Int = 2,
|
||||
reservationCount: Int = 10,
|
||||
): Map<MemberEntity, List<ReservationEntity>> {
|
||||
val members = (1..memberCount).map { createMember(role = Role.MEMBER) }
|
||||
val reservations = (1..reservationCount).map { index ->
|
||||
createReservation(
|
||||
member = members[index % memberCount],
|
||||
status = ReservationStatus.CONFIRMED
|
||||
)
|
||||
}
|
||||
return reservations.groupBy { it.member }
|
||||
}
|
||||
|
||||
private fun <T> persist(entity: T): T {
|
||||
transactionTemplate.executeWithoutResult {
|
||||
entityManager.persist(entity)
|
||||
}
|
||||
return entity
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user