From d1772dfcc5661a24d5f05a6b72fda43c591f0d9e Mon Sep 17 00:00:00 2001 From: pricelees Date: Wed, 6 Aug 2025 17:05:12 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20ReservationService=EB=A5=BC=20Query?= =?UTF-8?q?=20/=20Command=20Service=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?=ED=9B=84=20=EC=B6=94=EA=B0=80=EB=90=9C=20Finder,=20Writer,=20V?= =?UTF-8?q?alidator=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../payment/implement/PaymentWriter.kt | 1 + .../persistence/PaymentRepository.kt | 2 +- .../business/ReservationCommandService.kt | 104 ++++++ .../business/ReservationQueryService.kt | 56 +++ .../business/ReservationService.kt | 322 ------------------ .../business/ReservationWithPaymentService.kt | 8 +- .../reservation/docs/ReservationAPI.kt | 5 +- .../persistence/ReservationRepository.kt | 1 + .../reservation/web/ReservationController.kt | 35 +- .../persistence/PaymentRepositoryTest.kt | 30 -- .../business/ReservationServiceTest.kt | 287 ---------------- 11 files changed, 189 insertions(+), 662 deletions(-) create mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationCommandService.kt create mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationQueryService.kt delete mode 100644 src/main/kotlin/roomescape/reservation/business/ReservationService.kt delete mode 100644 src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt diff --git a/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt b/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt index 43778651..be815940 100644 --- a/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt +++ b/src/main/kotlin/roomescape/payment/implement/PaymentWriter.kt @@ -74,6 +74,7 @@ class PaymentWriter( return canceledPaymentRepository.save(canceledPayment) .also { + paymentRepository.deleteByPaymentKey(paymentKey) log.debug { "[PaymentWriter.createCanceled] 완료: paymentKey=${paymentKey}, canceledPaymentId=${it.id}" } } } diff --git a/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt b/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt index 79c1e07e..52180c4d 100644 --- a/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt +++ b/src/main/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepository.kt @@ -8,5 +8,5 @@ interface PaymentRepository : JpaRepository { fun findByReservationId(reservationId: Long): PaymentEntity? - fun findByPaymentKey(paymentKey: String): PaymentEntity? + fun deleteByPaymentKey(paymentKey: String) } diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationCommandService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationCommandService.kt new file mode 100644 index 00000000..16866695 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/business/ReservationCommandService.kt @@ -0,0 +1,104 @@ +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 ReservationCommandService( + private val reservationFinder: ReservationFinder, + private val reservationWriter: ReservationWriter +) { + fun createReservationWithPayment( + request: ReservationCreateWithPaymentRequest, + memberId: Long + ): ReservationEntity { + log.info { "[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.info { "[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.info { "[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.info { "[ReservationCommandService.confirmWaiting] 시작: reservationId=$reservationId (by adminId=$memberId)" } + + reservationWriter.confirm(reservationId) + .also { log.info { "[ReservationCommandService.confirmWaiting] 완료: reservationId=$reservationId" } } + } + + fun deleteWaiting(reservationId: Long, memberId: Long) { + log.info { "[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/business/ReservationQueryService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationQueryService.kt new file mode 100644 index 00000000..41a22fa5 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/business/ReservationQueryService.kt @@ -0,0 +1,56 @@ +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 ReservationQueryService( + private val reservationFinder: ReservationFinder +) { + fun findReservations(): ReservationRetrieveListResponse { + log.info { "[ReservationService.findReservations] 시작" } + + return reservationFinder.findAllByStatuses(*ReservationStatus.confirmedStatus()) + .toRetrieveListResponse() + .also { log.info { "[ReservationService.findReservations] ${it.reservations.size}개의 예약 조회 완료" } } + } + + fun findAllWaiting(): ReservationRetrieveListResponse { + log.info { "[ReservationService.findAllWaiting] 시작" } + + return reservationFinder.findAllByStatuses(ReservationStatus.WAITING) + .toRetrieveListResponse() + .also { log.info { "[ReservationService.findAllWaiting] ${it.reservations.size}개의 대기 조회 완료" } } + } + + fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { + log.info { "[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.info { "[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/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt deleted file mode 100644 index d22be309..00000000 --- a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt +++ /dev/null @@ -1,322 +0,0 @@ -package roomescape.reservation.business - -import com.github.f4b6a3.tsid.TsidFactory -import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.data.jpa.domain.Specification -import org.springframework.data.repository.findByIdOrNull -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import roomescape.common.config.next -import roomescape.member.business.MemberService -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.reservation.web.* -import roomescape.theme.business.ThemeService -import roomescape.theme.infrastructure.persistence.ThemeEntity -import roomescape.time.business.TimeService -import roomescape.time.infrastructure.persistence.TimeEntity -import java.time.LocalDate -import java.time.LocalDateTime - -private val log = KotlinLogging.logger {} - -@Service -@Transactional -class ReservationService( - private val tsidFactory: TsidFactory, - private val reservationRepository: ReservationRepository, - private val timeService: TimeService, - private val memberService: MemberService, - private val themeService: ThemeService, -) { - - @Transactional(readOnly = true) - fun findReservations(): ReservationRetrieveListResponse { - val spec: Specification = ReservationSearchSpecification() - .confirmed() - .build() - val reservations = findAllReservationByStatus(spec) - log.info { "[ReservationService.findReservations] ${reservations.size} 개의 확정 예약 조회 완료" } - - return ReservationRetrieveListResponse(reservations) - } - - @Transactional(readOnly = true) - fun findAllWaiting(): ReservationRetrieveListResponse { - val spec: Specification = ReservationSearchSpecification() - .waiting() - .build() - val reservations = findAllReservationByStatus(spec) - log.info { "[ReservationService.findAllWaiting] ${reservations.size} 개의 대기 예약 조회 완료" } - - return ReservationRetrieveListResponse(reservations) - } - - private fun findAllReservationByStatus(spec: Specification): List { - return reservationRepository.findAll(spec).map { it.toRetrieveResponse() } - } - - fun deleteReservation(reservationId: Long, memberId: Long) { - validateIsMemberAdmin(memberId, "deleteReservation") - log.debug { "[ReservationService.deleteReservation] 예약 삭제 시작: reservationId=$reservationId, memberId=$memberId" } - reservationRepository.deleteById(reservationId) - log.info { "[ReservationService.deleteReservation] 예약 삭제 완료: reservationId=$reservationId" } - } - - fun createConfirmedReservation( - request: ReservationCreateWithPaymentRequest, - memberId: Long, - ): ReservationEntity { - val themeId = request.themeId - val timeId = request.timeId - val date: LocalDate = request.date - validateIsReservationExist(themeId, timeId, date, "createConfirmedReservation") - - log.debug { "[ReservationService.createConfirmedReservation] 예약 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } - val reservation: ReservationEntity = - createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) - - return reservationRepository.save(reservation) - .also { log.info { "[ReservationService.createConfirmedReservation] 예약 추가 완료: reservationId=${it.id}, status=${it.status}" } } - } - - fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse { - validateIsReservationExist(request.themeId, request.timeId, request.date) - - log.debug { "[ReservationService.createReservationByAdmin] 관리자의 예약 추가: memberId=${request.memberId}, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } - return addReservationWithoutPayment( - request.themeId, - request.timeId, - request.date, - request.memberId, - ReservationStatus.CONFIRMED_PAYMENT_REQUIRED - ).also { - log.info { "[ReservationService.createReservationByAdmin] 관리자 예약 추가 완료: reservationId=${it.id}" } - } - } - - fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse { - validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId) - log.debug { "[ReservationService.createWaiting] 예약 대기 추가 시작: memberId=$memberId, themeId=${request.themeId}, timeId=${request.timeId}, date=${request.date}" } - return addReservationWithoutPayment( - request.themeId, - request.timeId, - request.date, - memberId, - ReservationStatus.WAITING - ).also { - log.info { "[ReservationService.createWaiting] 예약 대기 추가 완료: reservationId=${it.id}, status=${it.status}" } - } - } - - private fun addReservationWithoutPayment( - themeId: Long, - timeId: Long, - date: LocalDate, - memberId: Long, - status: ReservationStatus, - ): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status) - .also { - reservationRepository.save(it) - }.toRetrieveResponse() - - private fun validateMemberAlreadyReserve(themeId: Long, timeId: Long, date: LocalDate, memberId: Long) { - log.debug { - "[ReservationService.validateMemberAlreadyReserve] 회원의 중복 예약 여부 확인: themeId=$themeId, timeId=$timeId, date=$date, memberId=$memberId" - } - val spec: Specification = ReservationSearchSpecification() - .sameMemberId(memberId) - .sameThemeId(themeId) - .sameTimeId(timeId) - .sameDate(date) - .build() - - if (reservationRepository.exists(spec)) { - log.warn { "[ReservationService.validateMemberAlreadyReserve] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" } - throw ReservationException(ReservationErrorCode.ALREADY_RESERVE) - } - } - - private fun validateIsReservationExist( - themeId: Long, - timeId: Long, - date: LocalDate, - calledBy: String = "validateIsReservationExist" - ) { - log.debug { - "[ReservationService.$calledBy] 예약 존재 여부 확인: themeId=$themeId, timeId=$timeId, date=$date" - } - val spec: Specification = ReservationSearchSpecification() - .confirmed() - .sameThemeId(themeId) - .sameTimeId(timeId) - .sameDate(date) - .build() - - if (reservationRepository.exists(spec)) { - log.warn { "[ReservationService.$calledBy] 중복된 예약 존재: themeId=$themeId, timeId=$timeId, date=$date" } - throw ReservationException(ReservationErrorCode.RESERVATION_DUPLICATED) - } - } - - private fun validateDateAndTime( - requestDate: LocalDate, - requestTime: TimeEntity, - ) { - val now = LocalDateTime.now() - val request = LocalDateTime.of(requestDate, requestTime.startAt) - - if (request.isBefore(now)) { - log.info { "[ReservationService.validateDateAndTime] 날짜 범위 오류. request=$request, now=$now" } - throw ReservationException(ReservationErrorCode.PAST_REQUEST_DATETIME) - } - } - - private fun createEntity( - timeId: Long, - themeId: Long, - date: LocalDate, - memberId: Long, - status: ReservationStatus, - ): ReservationEntity { - val time: TimeEntity = timeService.findById(timeId) - val theme: ThemeEntity = themeService.findById(themeId) - val member: MemberEntity = memberService.findById(memberId) - - validateDateAndTime(date, time) - - return ReservationEntity( - _id = tsidFactory.next(), - date = date, - time = time, - theme = theme, - member = member, - status = status - ) - } - - @Transactional(readOnly = true) - fun searchReservations( - themeId: Long?, - memberId: Long?, - dateFrom: LocalDate?, - dateTo: LocalDate?, - ): ReservationRetrieveListResponse { - log.debug { "[ReservationService.searchReservations] 예약 검색 시작: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } - validateSearchDateRange(dateFrom, dateTo) - val spec: Specification = ReservationSearchSpecification() - .confirmed() - .sameThemeId(themeId) - .sameMemberId(memberId) - .dateStartFrom(dateFrom) - .dateEndAt(dateTo) - .build() - val reservations = findAllReservationByStatus(spec) - - return ReservationRetrieveListResponse(reservations) - .also { log.info { "[ReservationService.searchReservations] 예약 ${reservations.size}개 조회 완료: themeId=$themeId, memberId=$memberId, dateFrom=$dateFrom, dateTo=$dateTo" } } - } - - private fun validateSearchDateRange(startFrom: LocalDate?, endAt: LocalDate?) { - if (startFrom == null || endAt == null) { - return - } - if (startFrom.isAfter(endAt)) { - log.info { "[ReservationService.validateSearchDateRange] 조회 범위 오류: startFrom=$startFrom, endAt=$endAt" } - throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) - } - } - - @Transactional(readOnly = true) - fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { - val reservations = reservationRepository.findAllByMemberId(memberId) - log.info { "[ReservationService.findReservationsByMemberId] memberId=${memberId}인 ${reservations.size}개의 예약 조회 완료" } - return MyReservationRetrieveListResponse(reservations) - } - - fun confirmWaiting(reservationId: Long, memberId: Long) { - log.debug { "[ReservationService.confirmWaiting] 대기 예약 승인 시작: reservationId=$reservationId (by adminId=$memberId)" } - validateIsMemberAdmin(memberId, "confirmWaiting") - - log.debug { "[ReservationService.confirmWaiting] 대기 여부 확인 시작: reservationId=$reservationId" } - if (reservationRepository.isExistConfirmedReservation(reservationId)) { - log.warn { "[ReservationService.confirmWaiting] 승인 실패(이미 확정된 예약 존재): reservationId=$reservationId" } - throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) - } - - log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 시작: reservationId=$reservationId" } - reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) - log.debug { "[ReservationService.confirmWaiting] 대기 예약 상태 변경 완료: reservationId=$reservationId, status=${ReservationStatus.CONFIRMED_PAYMENT_REQUIRED}" } - - log.info { "[ReservationService.confirmWaiting] 대기 예약 승인 완료: reservationId=$reservationId" } - } - - fun deleteWaiting(reservationId: Long, memberId: Long) { - log.debug { "[ReservationService.deleteWaiting] 대기 취소 시작: reservationId=$reservationId, memberId=$memberId" } - - val reservation: ReservationEntity = findReservationOrThrow(reservationId, "deleteWaiting") - if (!reservation.isWaiting()) { - log.warn { - "[ReservationService.deleteWaiting] 대기 취소 실패(대기 예약이 아님): reservationId=$reservationId" + - ", currentStatus=${reservation.status} memberId=$memberId" - } - throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) - } - if (!reservation.isReservedBy(memberId)) { - log.error { - "[ReservationService.deleteWaiting] 대기 취소 실패(예약자 본인의 취소 요청이 아님): reservationId=$reservationId" + - ", memberId=$memberId " - } - throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) - } - log.debug { "[ReservationService.deleteWaiting] 대기 예약 삭제 시작: reservationId=$reservationId" } - reservationRepository.delete(reservation) - - log.info { "[ReservationService.deleteWaiting] 대기 취소 완료: reservationId=$reservationId, memberId=$memberId" } - } - - fun rejectWaiting(reservationId: Long, memberId: Long) { - validateIsMemberAdmin(memberId, "rejectWaiting") - log.debug { "[ReservationService.rejectWaiting] 대기 예약 삭제 시작: reservationId=$reservationId (by adminId=$memberId)" } - val reservation: ReservationEntity = findReservationOrThrow(reservationId, "rejectWaiting") - - if (!reservation.isWaiting()) { - log.warn { - "[ReservationService.rejectWaiting] 대기 예약 삭제 실패(이미 확정 상태): reservationId=$reservationId" + - ", status=${reservation.status}" - } - throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) - } - reservationRepository.delete(reservation) - log.info { "[ReservationService.rejectWaiting] 대기 예약 삭제 완료: reservationId=$reservationId" } - } - - private fun validateIsMemberAdmin(memberId: Long, calledBy: String = "validateIsMemberAdmin") { - log.debug { "[ReservationService.$calledBy] 관리자 여부 확인: memberId=$memberId" } - val member: MemberEntity = memberService.findById(memberId) - if (member.isAdmin()) { - return - } - log.warn { "[ReservationService.$calledBy] 관리자가 아님: memberId=$memberId, role=${member.role}" } - throw ReservationException(ReservationErrorCode.NO_PERMISSION) - } - - private fun findReservationOrThrow( - reservationId: Long, - calledBy: String = "findReservationOrThrow" - ): ReservationEntity { - log.debug { "[ReservationService.$calledBy] 예약 조회: reservationId=$reservationId" } - return reservationRepository.findByIdOrNull(reservationId) - ?.also { log.info { "[ReservationService.$calledBy] 예약 조회 완료: reservationId=$reservationId" } } - ?: run { - log.warn { "[ReservationService.$calledBy] 예약 조회 실패: reservationId=$reservationId" } - throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) - } - - } -} diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt index 66abc2f5..83a6ed0a 100644 --- a/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt +++ b/src/main/kotlin/roomescape/reservation/business/ReservationWithPaymentService.kt @@ -18,7 +18,7 @@ private val log = KotlinLogging.logger {} @Service @Transactional class ReservationWithPaymentService( - private val reservationService: ReservationService, + private val reservationCommandService: ReservationCommandService, private val paymentService: PaymentService, ) { fun createReservationAndPayment( @@ -28,7 +28,7 @@ class ReservationWithPaymentService( ): ReservationCreateResponse { log.info { "[ReservationWithPaymentService.createReservationAndPayment] 예약 & 결제 정보 저장 시작: memberId=$memberId, paymentInfo=$approvedPaymentInfo" } - val reservation: ReservationEntity = reservationService.createConfirmedReservation(request, memberId) + val reservation: ReservationEntity = reservationCommandService.createReservationWithPayment(request, memberId) .also { paymentService.createPayment(approvedPaymentInfo, it) } return reservation.toCreateResponse() @@ -50,7 +50,7 @@ class ReservationWithPaymentService( log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 시작: reservationId=$reservationId" } val paymentCancelRequest = paymentService.createCanceledPayment(reservationId) - reservationService.deleteReservation(reservationId, memberId) + reservationCommandService.deleteReservation(reservationId, memberId) log.info { "[ReservationWithPaymentService.deleteReservationAndPayment] 결제 취소 정보 저장 & 예약 삭제 완료: reservationId=$reservationId" } return paymentCancelRequest } @@ -62,7 +62,7 @@ class ReservationWithPaymentService( val notPaid: Boolean = !paymentService.existsByReservationId(reservationId) return notPaid.also { - log.info { "[ReservationWithPaymentService.isNotPaidReservation] 완료: reservationId=$reservationId, 미결제=${notPaid}" } + log.info { "[ReservationWithPaymentService.isNotPaidReservation] 완료: reservationId=$reservationId, isPaid=${notPaid}" } } } diff --git a/src/main/kotlin/roomescape/reservation/docs/ReservationAPI.kt b/src/main/kotlin/roomescape/reservation/docs/ReservationAPI.kt index a2762c96..fb5b7dd3 100644 --- a/src/main/kotlin/roomescape/reservation/docs/ReservationAPI.kt +++ b/src/main/kotlin/roomescape/reservation/docs/ReservationAPI.kt @@ -92,7 +92,8 @@ interface ReservationAPI { ) fun createReservationByAdmin( @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest, - ): ResponseEntity> + @MemberId @Parameter(hidden = true) memberId: Long + ): ResponseEntity> @Admin @Operation(summary = "모든 예약 대기 조회", tags = ["관리자 로그인이 필요한 API"]) @@ -116,7 +117,7 @@ interface ReservationAPI { fun createWaiting( @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest, @MemberId @Parameter(hidden = true) memberId: Long, - ): ResponseEntity> + ): ResponseEntity> @LoginRequired @Operation(summary = "예약 대기 취소", tags = ["로그인이 필요한 API"]) diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index b2de4be4..b3b0c607 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -1,5 +1,6 @@ package roomescape.reservation.infrastructure.persistence +import org.springframework.data.jpa.domain.Specification import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaSpecificationExecutor import org.springframework.data.jpa.repository.Modifying diff --git a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt index 6c4d5b74..8e4ff5ec 100644 --- a/src/main/kotlin/roomescape/reservation/web/ReservationController.kt +++ b/src/main/kotlin/roomescape/reservation/web/ReservationController.kt @@ -10,7 +10,8 @@ import roomescape.payment.infrastructure.client.PaymentApproveRequest import roomescape.payment.infrastructure.client.PaymentApproveResponse import roomescape.payment.infrastructure.client.TossPaymentClient import roomescape.payment.web.PaymentCancelRequest -import roomescape.reservation.business.ReservationService +import roomescape.reservation.business.ReservationCommandService +import roomescape.reservation.business.ReservationQueryService import roomescape.reservation.business.ReservationWithPaymentService import roomescape.reservation.docs.ReservationAPI import java.net.URI @@ -19,12 +20,13 @@ import java.time.LocalDate @RestController class ReservationController( private val reservationWithPaymentService: ReservationWithPaymentService, - private val reservationService: ReservationService, + private val reservationQueryService: ReservationQueryService, + private val reservationCommandService: ReservationCommandService, private val paymentClient: TossPaymentClient ) : ReservationAPI { @GetMapping("/reservations") override fun findReservations(): ResponseEntity> { - val response: ReservationRetrieveListResponse = reservationService.findReservations() + val response: ReservationRetrieveListResponse = reservationQueryService.findReservations() return ResponseEntity.ok(CommonApiResponse(response)) } @@ -33,7 +35,7 @@ class ReservationController( override fun findReservationsByMemberId( @MemberId @Parameter(hidden = true) memberId: Long ): ResponseEntity> { - val response: MyReservationRetrieveListResponse = reservationService.findReservationsByMemberId(memberId) + val response: MyReservationRetrieveListResponse = reservationQueryService.findReservationsByMemberId(memberId) return ResponseEntity.ok(CommonApiResponse(response)) } @@ -45,7 +47,7 @@ class ReservationController( @RequestParam(required = false) dateFrom: LocalDate?, @RequestParam(required = false) dateTo: LocalDate? ): ResponseEntity> { - val response: ReservationRetrieveListResponse = reservationService.searchReservations( + val response: ReservationRetrieveListResponse = reservationQueryService.searchReservations( themeId, memberId, dateFrom, @@ -61,7 +63,7 @@ class ReservationController( @PathVariable("id") reservationId: Long ): ResponseEntity> { if (reservationWithPaymentService.isNotPaidReservation(reservationId)) { - reservationService.deleteReservation(reservationId, memberId) + reservationCommandService.deleteReservation(reservationId, memberId) return ResponseEntity.noContent().build() } @@ -110,10 +112,11 @@ class ReservationController( @PostMapping("/reservations/admin") override fun createReservationByAdmin( - @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest - ): ResponseEntity> { - val response: ReservationRetrieveResponse = - reservationService.createReservationByAdmin(adminReservationRequest) + @Valid @RequestBody adminReservationRequest: AdminReservationCreateRequest, + @MemberId @Parameter(hidden = true) memberId: Long, + ): ResponseEntity> { + val response: ReservationCreateResponse = + reservationCommandService.createReservationByAdmin(adminReservationRequest, memberId) return ResponseEntity.created(URI.create("/reservations/${response.id}")) .body(CommonApiResponse(response)) @@ -121,7 +124,7 @@ class ReservationController( @GetMapping("/reservations/waiting") override fun findAllWaiting(): ResponseEntity> { - val response: ReservationRetrieveListResponse = reservationService.findAllWaiting() + val response: ReservationRetrieveListResponse = reservationQueryService.findAllWaiting() return ResponseEntity.ok(CommonApiResponse(response)) } @@ -130,8 +133,8 @@ class ReservationController( override fun createWaiting( @Valid @RequestBody waitingCreateRequest: WaitingCreateRequest, @MemberId @Parameter(hidden = true) memberId: Long, - ): ResponseEntity> { - val response: ReservationRetrieveResponse = reservationService.createWaiting( + ): ResponseEntity> { + val response: ReservationCreateResponse = reservationCommandService.createWaiting( waitingCreateRequest, memberId ) @@ -145,7 +148,7 @@ class ReservationController( @MemberId @Parameter(hidden = true) memberId: Long, @PathVariable("id") reservationId: Long ): ResponseEntity> { - reservationService.deleteWaiting(reservationId, memberId) + reservationCommandService.deleteWaiting(reservationId, memberId) return ResponseEntity.noContent().build() } @@ -155,7 +158,7 @@ class ReservationController( @MemberId @Parameter(hidden = true) memberId: Long, @PathVariable("id") reservationId: Long ): ResponseEntity> { - reservationService.confirmWaiting(reservationId, memberId) + reservationCommandService.confirmWaiting(reservationId, memberId) return ResponseEntity.ok().build() } @@ -165,7 +168,7 @@ class ReservationController( @MemberId @Parameter(hidden = true) memberId: Long, @PathVariable("id") reservationId: Long ): ResponseEntity> { - reservationService.rejectWaiting(reservationId, memberId) + reservationCommandService.deleteWaiting(reservationId, memberId) return ResponseEntity.noContent().build() } diff --git a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt index 5ed4652d..a58d0580 100644 --- a/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt +++ b/src/test/kotlin/roomescape/payment/infrastructure/persistence/PaymentRepositoryTest.kt @@ -38,36 +38,6 @@ class PaymentRepositoryTest( .also { it shouldBe false } } } - - context("findByPaymentKey") { - lateinit var payment: PaymentEntity - - beforeTest { - reservation = setupReservation() - payment = PaymentFixture.create(reservation = reservation) - .also { paymentRepository.save(it) } - } - - test("정상 반환") { - paymentRepository.findByPaymentKey(payment.paymentKey) - ?.also { - assertSoftly(it) { - this.id shouldBe payment.id - this.orderId shouldBe payment.orderId - this.paymentKey shouldBe payment.paymentKey - this.totalAmount shouldBe payment.totalAmount - this.reservation.id shouldBe payment.reservation.id - this.approvedAt shouldBe payment.approvedAt - } - } - ?: throw AssertionError("Unexpected null value") - } - - test("null 반환") { - paymentRepository.findByPaymentKey("non-existent-key") - .also { it shouldBe null } - } - } } private fun setupReservation(): ReservationEntity { diff --git a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt b/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt deleted file mode 100644 index 54c6cccf..00000000 --- a/src/test/kotlin/roomescape/reservation/business/ReservationServiceTest.kt +++ /dev/null @@ -1,287 +0,0 @@ -package roomescape.reservation.business - -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.shouldBe -import io.mockk.every -import io.mockk.mockk -import org.springframework.data.repository.findByIdOrNull -import roomescape.member.business.MemberService -import roomescape.member.infrastructure.persistence.Role -import roomescape.reservation.exception.ReservationErrorCode -import roomescape.reservation.exception.ReservationException -import roomescape.reservation.infrastructure.persistence.ReservationRepository -import roomescape.reservation.infrastructure.persistence.ReservationStatus -import roomescape.theme.business.ThemeService -import roomescape.time.business.TimeService -import roomescape.util.MemberFixture -import roomescape.util.ReservationFixture -import roomescape.util.TsidFactory -import roomescape.util.TimeFixture -import java.time.LocalDate -import java.time.LocalTime - -class ReservationServiceTest : FunSpec({ - - val reservationRepository: ReservationRepository = mockk() - val timeService: TimeService = mockk() - val memberService: MemberService = mockk() - val themeService: ThemeService = mockk() - val reservationService = ReservationService( - TsidFactory, - reservationRepository, - timeService, - memberService, - themeService - ) - - context("예약을 추가할 때") { - test("이미 예약이 있으면 예외를 던진다.") { - every { - reservationRepository.exists(any()) - } returns true - - val reservationRequest = ReservationFixture.createRequest() - - shouldThrow { - reservationService.createConfirmedReservation(reservationRequest, 1L) - }.also { - it.errorCode shouldBe ReservationErrorCode.RESERVATION_DUPLICATED - } - } - - context("날짜, 시간이 잘못 입력되면 예외를 던진다.") { - every { - reservationRepository.exists(any()) - } returns false - - every { - themeService.findById(any()) - } returns mockk() - - every { - memberService.findById(any()) - } returns mockk() - - - test("지난 날짜이면 예외를 던진다.") { - val reservationRequest = ReservationFixture.createRequest().copy( - date = LocalDate.now().minusDays(1) - ) - - every { - timeService.findById(any()) - } returns TimeFixture.create() - - shouldThrow { - reservationService.createConfirmedReservation(reservationRequest, 1L) - }.also { - it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME - } - } - - test("지난 시간이면 예외를 던진다.") { - val reservationRequest = ReservationFixture.createRequest().copy( - date = LocalDate.now(), - ) - - every { - timeService.findById(reservationRequest.timeId) - } returns TimeFixture.create( - startAt = LocalTime.now().minusMinutes(1) - ) - - shouldThrow { - reservationService.createConfirmedReservation(reservationRequest, 1L) - }.also { - it.errorCode shouldBe ReservationErrorCode.PAST_REQUEST_DATETIME - } - } - } - } - - context("예약 대기를 걸 때") { - test("이미 예약한 회원이 같은 날짜와 테마로 대기를 걸면 예외를 던진다.") { - val reservationRequest = ReservationFixture.createRequest().copy( - date = LocalDate.now(), - themeId = 1L, - timeId = 1L, - ) - - every { - reservationRepository.exists(any()) - } returns true - - shouldThrow { - val waitingRequest = ReservationFixture.createWaitingRequest( - date = reservationRequest.date, - themeId = reservationRequest.themeId, - timeId = reservationRequest.timeId - ) - reservationService.createWaiting(waitingRequest, 1L) - }.also { - it.errorCode shouldBe ReservationErrorCode.ALREADY_RESERVE - } - } - } - - context("예약 대기를 취소할 때") { - val reservationId = 1L - val member = MemberFixture.create(id = 1L, role = Role.MEMBER) - test("예약을 찾을 수 없으면 예외를 던진다.") { - every { - reservationRepository.findByIdOrNull(reservationId) - } returns null - - shouldThrow { - reservationService.deleteWaiting(reservationId, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND - } - } - - test("대기중인 해당 예약이 이미 확정된 상태라면 예외를 던진다.") { - val alreadyConfirmed = ReservationFixture.create( - id = reservationId, - status = ReservationStatus.CONFIRMED - ) - every { - reservationRepository.findByIdOrNull(reservationId) - } returns alreadyConfirmed - - shouldThrow { - reservationService.deleteWaiting(reservationId, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED - } - } - - test("타인의 대기를 취소하려고 하면 예외를 던진다.") { - val otherMembersWaiting = ReservationFixture.create( - id = reservationId, - member = MemberFixture.create(id = member.id!! + 1L), - status = ReservationStatus.WAITING - ) - - every { - reservationRepository.findByIdOrNull(reservationId) - } returns otherMembersWaiting - - shouldThrow { - reservationService.deleteWaiting(reservationId, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.NOT_RESERVATION_OWNER - } - } - } - - context("예약을 조회할 때") { - test("종료 날짜가 시작 날짜보다 이전이면 예외를 던진다.") { - val startFrom = LocalDate.now() - val endAt = startFrom.minusDays(1) - - shouldThrow { - reservationService.searchReservations( - null, - null, - startFrom, - endAt - ) - }.also { - it.errorCode shouldBe ReservationErrorCode.INVALID_SEARCH_DATE_RANGE - } - } - } - - context("대기중인 예약을 승인할 때") { - test("관리자가 아니면 예외를 던진다.") { - val member = MemberFixture.create(id = 1L, role = Role.MEMBER) - - every { - memberService.findById(any()) - } returns member - - shouldThrow { - reservationService.confirmWaiting(1L, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION - } - } - - test("이미 확정된 예약이 있으면 예외를 던진다.") { - val member = MemberFixture.create(id = 1L, role = Role.ADMIN) - val reservationId = 1L - - every { - memberService.findById(any()) - } returns member - - every { - reservationRepository.isExistConfirmedReservation(reservationId) - } returns true - - shouldThrow { - reservationService.confirmWaiting(reservationId, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS - } - } - } - - context("대기중인 예약을 거절할 때") { - test("관리자가 아니면 예외를 던진다.") { - val member = MemberFixture.create(id = 1L, role = Role.MEMBER) - - every { - memberService.findById(any()) - } returns member - - shouldThrow { - reservationService.rejectWaiting(1L, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.NO_PERMISSION - } - } - - test("예약을 찾을 수 없으면 예외를 던진다.") { - val member = MemberFixture.create(id = 1L, role = Role.ADMIN) - val reservationId = 1L - - every { - memberService.findById(member.id!!) - } returns member - - every { - reservationRepository.findByIdOrNull(reservationId) - } returns null - - shouldThrow { - reservationService.rejectWaiting(reservationId, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.RESERVATION_NOT_FOUND - } - } - - test("이미 확정된 예약이면 예외를 던진다.") { - val member = MemberFixture.create(id = 1L, role = Role.ADMIN) - val reservation = ReservationFixture.create( - id = 1L, - status = ReservationStatus.CONFIRMED - ) - - every { - memberService.findById(member.id!!) - } returns member - - every { - reservationRepository.findByIdOrNull(reservation.id!!) - } returns reservation - - shouldThrow { - reservationService.rejectWaiting(reservation.id!!, member.id!!) - }.also { - it.errorCode shouldBe ReservationErrorCode.ALREADY_CONFIRMED - } - } - } -})