From 7c77f50e6631a2519df6038ba43aaa8eb690667b Mon Sep 17 00:00:00 2001 From: pricelees Date: Sun, 7 Sep 2025 21:47:58 +0900 Subject: [PATCH] =?UTF-8?q?remove:=20=EC=98=88=EC=95=BD=20&=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EB=8F=84=EB=A7=A4=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=A1=B4=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/MyReservationFindService.kt | 54 -- .../business/ReservationFindService.kt | 56 -- .../business/ReservationWithPaymentService.kt | 75 -- .../ReservationWithPaymentServiceV2.kt | 108 --- .../business/ReservationWriteService.kt | 104 --- .../reservation/docs/MyReservationAPI.kt | 33 - .../docs/ReservationWithPaymentAPI.kt | 61 -- .../implement/ReservationFinder.kt | 113 --- .../implement/ReservationValidator.kt | 178 ----- .../implement/ReservationWriter.kt | 131 ---- .../ReservationSearchSpecification.kt | 80 -- .../web/MyReservationController.kt | 35 - .../reservation/web/MyReservationResponse.kt | 196 ----- .../reservation/web/ReservationRequest.kt | 40 - .../reservation/web/ReservationResponse.kt | 93 --- .../web/ReservationWithPaymentController.kt | 60 -- .../web/ReservationWithPaymentDTO.kt | 57 -- .../persistence/ReservationEntityV2.kt | 32 - .../persistence/ReservationRepositoryV2.kt | 8 - .../ReservationWithPaymentServiceTest.kt | 122 --- .../persistence/ReservationRepositoryTest.kt | 205 ----- .../web/ReservationControllerTest.kt | 730 ------------------ 22 files changed, 2571 deletions(-) delete mode 100644 src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt delete mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationFindService.kt delete mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt delete mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt delete mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationWriteService.kt delete mode 100644 src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt delete mode 100644 src/main/kotlin/roomescape/reservation/docs/ReservationWithPaymentAPI.kt delete mode 100644 src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt delete mode 100644 src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt delete mode 100644 src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt delete mode 100644 src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/MyReservationController.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentController.kt delete mode 100644 src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentDTO.kt delete mode 100644 src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationEntityV2.kt delete mode 100644 src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationRepositoryV2.kt delete mode 100644 src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt delete mode 100644 src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt delete mode 100644 src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt diff --git a/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt b/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt deleted file mode 100644 index 7f0c9e24..00000000 --- a/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt +++ /dev/null @@ -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" } - } - } -} diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationFindService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationFindService.kt deleted file mode 100644 index 3b2bf390..00000000 --- a/src/main/kotlin/roomescape/reservation/business/ReservationFindService.kt +++ /dev/null @@ -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" } } - } -} diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt deleted file mode 100644 index 3c50e5de..00000000 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt +++ /dev/null @@ -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) - } -} diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt deleted file mode 100644 index 0c7fef87..00000000 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceV2.kt +++ /dev/null @@ -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}" - } - } - } -} diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWriteService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWriteService.kt deleted file mode 100644 index 56cbe099..00000000 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWriteService.kt +++ /dev/null @@ -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" } } - } -} diff --git a/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt b/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt deleted file mode 100644 index 62799114..00000000 --- a/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt +++ /dev/null @@ -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> - - @LoginRequired - @Operation(summary = "예약 상세 조회", tags = ["로그인이 필요한 API"]) - @ApiResponses( - ApiResponse(responseCode = "200", description = "성공"), - ) - fun showReservationDetails( - @PathVariable("id") reservationId: Long - ): ResponseEntity> -} diff --git a/src/main/kotlin/roomescape/reservation/docs/ReservationWithPaymentAPI.kt b/src/main/kotlin/roomescape/reservation/docs/ReservationWithPaymentAPI.kt deleted file mode 100644 index ffeb3716..00000000 --- a/src/main/kotlin/roomescape/reservation/docs/ReservationWithPaymentAPI.kt +++ /dev/null @@ -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> - - @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> - - @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> -} diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt deleted file mode 100644 index 17d74611..00000000 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationFinder.kt +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - 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 { - reservationValidator.validateSearchDateRange(startFrom, endAt) - - val spec: Specification = 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}" } - } - } -} diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt deleted file mode 100644 index 78285bda..00000000 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationValidator.kt +++ /dev/null @@ -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 = 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 = 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" } - } -} diff --git a/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt b/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt deleted file mode 100644 index add04a0d..00000000 --- a/src/main/kotlin/roomescape/reservation/implement/ReservationWriter.kt +++ /dev/null @@ -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) - } - } -} diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt deleted file mode 100644 index f760a897..00000000 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationSearchSpecification.kt +++ /dev/null @@ -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 = Specification { _, _, _ -> null } -) { - fun sameThemeId(themeId: Long?): ReservationSearchSpecification = andIfNotNull(themeId?.let { - Specification { root, _, cb -> - cb.equal(root.get("theme").get("id"), themeId) - } - }) - - fun sameMemberId(memberId: Long?): ReservationSearchSpecification = andIfNotNull(memberId?.let { - Specification { root, _, cb -> - cb.equal(root.get("member").get("id"), memberId) - } - }) - - fun sameTimeId(timeId: Long?): ReservationSearchSpecification = andIfNotNull(timeId?.let { - Specification { root, _, cb -> - cb.equal(root.get("time").get("id"), timeId) - } - }) - - fun sameDate(date: LocalDate?): ReservationSearchSpecification = andIfNotNull(date?.let { - Specification { root, _, cb -> - cb.equal(root.get("date"), date) - } - }) - - fun status(vararg statuses: ReservationStatus) = andIfNotNull { root, _, cb -> - root.get("status").`in`(statuses.toList()) - } - - fun confirmed(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> - cb.or( - cb.equal( - root.get("status"), - ReservationStatus.CONFIRMED - ), - cb.equal( - root.get("status"), - ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ) - ) - } - - fun waiting(): ReservationSearchSpecification = andIfNotNull { root, _, cb -> - cb.equal( - root.get("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 { - return this.spec - } - - private fun andIfNotNull(condition: Specification?): ReservationSearchSpecification { - condition?.let { this.spec = this.spec.and(condition) } - return this - } -} diff --git a/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt b/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt deleted file mode 100644 index b0feffd7..00000000 --- a/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt +++ /dev/null @@ -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> { - val response = reservationFindService.findReservationsByMemberId(memberId) - - return ResponseEntity.ok(CommonApiResponse(response)) - } - - @GetMapping("/v2/reservations/{id}/details") - override fun showReservationDetails( - @PathVariable("id") reservationId: Long - ): ResponseEntity> { - val response = reservationFindService.showReservationDetails(reservationId) - - return ResponseEntity.ok(CommonApiResponse(response)) - } -} diff --git a/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt deleted file mode 100644 index 27668c15..00000000 --- a/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt +++ /dev/null @@ -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 -) - -fun List.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 - ) -} diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt b/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt deleted file mode 100644 index c7bad04d..00000000 --- a/src/main/kotlin/roomescape/reservation/web/ReservationRequest.kt +++ /dev/null @@ -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 -) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt deleted file mode 100644 index 2318c9c6..00000000 --- a/src/main/kotlin/roomescape/reservation/web/ReservationResponse.kt +++ /dev/null @@ -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 -) - -fun List.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 -) - -fun List.toRetrieveListResponse()= ReservationRetrieveListResponse( - this.map { it.toRetrieveResponse() } -) diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentController.kt b/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentController.kt deleted file mode 100644 index 590e71e7..00000000 --- a/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentController.kt +++ /dev/null @@ -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> { - 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> { - 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> { - reservationWithPaymentService.cancelReservation(memberId, reservationId, cancelRequest) - - return ResponseEntity.noContent().build() - } -} diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentDTO.kt b/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentDTO.kt deleted file mode 100644 index 07b2a444..00000000 --- a/src/main/kotlin/roomescape/reservation/web/ReservationWithPaymentDTO.kt +++ /dev/null @@ -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() -) diff --git a/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationEntityV2.kt b/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationEntityV2.kt deleted file mode 100644 index b6a6fa20..00000000 --- a/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationEntityV2.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationRepositoryV2.kt b/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationRepositoryV2.kt deleted file mode 100644 index d1562750..00000000 --- a/src/main/kotlin/roomescape/reservation_v2/infrastructure/persistence/ReservationRepositoryV2.kt +++ /dev/null @@ -1,8 +0,0 @@ -package roomescape.reservation_v2.infrastructure.persistence - -import org.springframework.data.jpa.repository.JpaRepository - -interface ReservationRepositoryV2 : JpaRepository { - - fun findAllByMemberId(memberId: Long): List -} \ No newline at end of file diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt deleted file mode 100644 index b3d0ed60..00000000 --- a/src/test/kotlin/roomescape/reservation/business/ReservationWithPaymentServiceTest.kt +++ /dev/null @@ -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 - } - } - } -}) diff --git a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt b/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt deleted file mode 100644 index 78d44d3f..00000000 --- a/src/test/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepositoryTest.kt +++ /dev/null @@ -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 = - 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 = - 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) - } -} diff --git a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt b/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt deleted file mode 100644 index 06d59c98..00000000 --- a/src/test/kotlin/roomescape/reservation/web/ReservationControllerTest.kt +++ /dev/null @@ -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> - 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> - 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> - 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> - 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> - 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> { - 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 persist(entity: T): T { - transactionTemplate.executeWithoutResult { - entityManager.persist(entity) - } - return entity - } -} \ No newline at end of file