diff --git a/src/main/kotlin/roomescape/reservation/business/ReservationService.kt b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt new file mode 100644 index 00000000..9dbae3e0 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/business/ReservationService.kt @@ -0,0 +1,163 @@ +package roomescape.reservation.business + +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.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.common.config.next +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.Role +import roomescape.member.web.MemberSummaryRetrieveResponse +import roomescape.payment.business.PaymentService +import roomescape.payment.web.PaymentRetrieveResponse +import roomescape.reservation.exception.ReservationErrorCode +import roomescape.reservation.exception.ReservationException +import roomescape.reservation.infrastructure.persistence.* +import roomescape.reservation.web.* +import roomescape.schedule.business.ScheduleService +import roomescape.schedule.infrastructure.persistence.ScheduleStatus +import roomescape.schedule.web.ScheduleSummaryResponse +import roomescape.schedule.web.ScheduleUpdateRequest +import roomescape.theme.business.ThemeServiceV2 +import roomescape.theme.web.ThemeRetrieveResponseV2 +import java.time.LocalDateTime + +private val log: KLogger = KotlinLogging.logger {} + +@Service +class ReservationService( + private val reservationRepository: ReservationRepository, + private val scheduleService: ScheduleService, + private val memberService: MemberService, + private val themeService: ThemeServiceV2, + private val canceledReservationRepository: CanceledReservationRepository, + private val tsidFactory: TsidFactory, + private val paymentService: PaymentService +) { + + @Transactional + fun createPendingReservation( + memberId: Long, + request: PendingReservationCreateRequest + ): PendingReservationCreateResponse { + log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } + + val reservation: ReservationEntity = request.toEntity( + id = tsidFactory.next(), + memberId = memberId + ) + + return PendingReservationCreateResponse(reservationRepository.save(reservation).id) + .also { "[ReservationService.createPendingReservation] Pending 예약 생성 완료: reservationId=${it}, schedule=${request.scheduleId}" } + } + + @Transactional + fun confirmReservation(id: Long) { + log.info { "[ReservationService.confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } + val reservation: ReservationEntity = findOrThrow(id) + + if (reservation.status != ReservationStatus.PENDING) { + log.warn { "[ReservationService.confirmReservation] 예약이 Pending 상태가 아님: reservationId=${id}, status=${reservation.status}" } + } + + run { + reservation.confirm() + scheduleService.updateSchedule( + reservation.scheduleId, + ScheduleUpdateRequest(status = ScheduleStatus.RESERVED) + ) + }.also { + log.info { "[ReservationService.confirmReservation] Pending 예약 확정 완료: reservationId=${id}" } + } + } + + @Transactional + fun cancelReservation(memberId: Long, reservationId: Long, request: ReservationCancelRequest) { + log.info { "[ReservationService.cancelReservation] 예약 취소 시작: memberId=${memberId}, reservationId=${reservationId}" } + + val reservation: ReservationEntity = findOrThrow(reservationId) + val member: MemberSummaryRetrieveResponse = memberService.findSummaryById(memberId) + + run { + scheduleService.updateSchedule( + reservation.scheduleId, + ScheduleUpdateRequest(status = ScheduleStatus.AVAILABLE) + ) + saveCanceledReservation(member, reservation, request.cancelReason) + reservation.cancel() + }.also { + log.info { "[ReservationService.cancelReservation] 예약 취소 완료: reservationId=${reservationId}" } + } + } + + @Transactional(readOnly = true) + fun findSummaryByMemberId(memberId: Long): ReservationSummaryRetrieveListResponse { + log.info { "[ReservationService.findSummaryByMemberId] 예약 조회 시작: memberId=${memberId}" } + + val reservations: List = reservationRepository.findAllByMemberId(memberId) + + return ReservationSummaryRetrieveListResponse(reservations.map { + val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId) + val theme: ThemeRetrieveResponseV2 = themeService.findById(schedule.themeId) + + ReservationSummaryRetrieveResponse( + id = it.id, + themeName = theme.name, + date = schedule.date, + startAt = schedule.time, + status = it.status + ) + }) + } + + @Transactional(readOnly = true) + fun findDetailById(id: Long): ReservationDetailRetrieveResponse { + log.info { "[ReservationService.findDetailById] 예약 상세 조회 시작: reservationId=${id}" } + + val reservation: ReservationEntity = findOrThrow(id) + val member: MemberSummaryRetrieveResponse = memberService.findSummaryById(reservation.memberId) + val paymentDetail: PaymentRetrieveResponse = paymentService.findDetailByReservationId(id) + + return reservation.toReservationDetailRetrieveResponse( + member = member, + payment = paymentDetail + ).also { + log.info { "[ReservationService.findDetailById] 예약 상세 조회 완료: reservationId=${id}" } + } + } + + private fun findOrThrow(id: Long): ReservationEntity { + log.info { "[ReservationService.findOrThrow] 예약 조회 시작: reservationId=${id}" } + + return reservationRepository.findByIdOrNull(id) + ?.also { log.info { "[ReservationService.findOrThrow] 예약 조회 완료: reservationId=${id}" } } + ?: run { + log.warn { "[ReservationService.findOrThrow] 예약 조회 실패: reservationId=${id}" } + throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) + } + } + + private fun saveCanceledReservation( + member: MemberSummaryRetrieveResponse, + reservation: ReservationEntity, + cancelReason: String + ) { + if (member.role != Role.ADMIN && reservation.memberId != member.id) { + log.warn { "[ReservationService.createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, memberId=${member.id}" } + throw ReservationException(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION) + } + + CanceledReservationEntity( + id = tsidFactory.next(), + reservationId = reservation.id, + canceledBy = member.id, + cancelReason = cancelReason, + canceledAt = LocalDateTime.now(), + status = CanceledReservationStatus.PROCESSING + ).also { + canceledReservationRepository.save(it) + } + } +} diff --git a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt index e8e20b6b..a36616a6 100644 --- a/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt +++ b/src/main/kotlin/roomescape/reservation/infrastructure/persistence/ReservationRepository.kt @@ -1,75 +1,8 @@ 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 -import org.springframework.data.jpa.repository.Query -import org.springframework.data.repository.query.Param -import roomescape.reservation.web.MyReservationRetrieveResponse -import roomescape.theme.infrastructure.persistence.ThemeEntity -import roomescape.time.infrastructure.persistence.TimeEntity -import java.time.LocalDate -interface ReservationRepository - : JpaRepository, JpaSpecificationExecutor { - fun findAllByTime(time: TimeEntity): List - fun existsByTime(time: TimeEntity): Boolean +interface ReservationRepository : JpaRepository { - fun findByDateAndThemeId(date: LocalDate, themeId: Long): List - - @Modifying - @Query( - """ - UPDATE ReservationEntity r - SET r.status = :status - WHERE r._id = :_id - """ - ) - fun updateStatusByReservationId( - @Param(value = "_id") reservationId: Long, - @Param(value = "status") statusForChange: ReservationStatus - ): Int - - @Query( - """ - SELECT EXISTS ( - SELECT 1 - FROM ReservationEntity r2 - WHERE r2._id = :_id - AND EXISTS ( - SELECT 1 FROM ReservationEntity r - WHERE r.theme._id = r2.theme._id - AND r.time._id = r2.time._id - AND r.date = r2.date - AND r.status != 'WAITING' - ) - ) - """ - ) - fun isExistConfirmedReservation(@Param("_id") reservationId: Long): Boolean - - @Query( - """ - SELECT new roomescape.reservation.web.MyReservationRetrieveResponse( - r._id, - t.name, - r.date, - r.time.startAt, - r.status, - (SELECT COUNT (r2) * 1L FROM ReservationEntity r2 WHERE r2.theme = r.theme AND r2.date = r.date AND r2.time = r.time AND r2._id < r._id), - p.paymentKey, - p.totalAmount - ) - FROM ReservationEntity r - JOIN r.theme t - LEFT JOIN PaymentEntity p - ON p.reservation = r - WHERE r.member._id = :memberId - """ - ) - fun findAllByMemberId(memberId: Long): List - fun findAllByDateAndTheme(date: LocalDate, theme: ThemeEntity): List - - fun findAllByMember_Id(memberId: Long): List + fun findAllByMemberId(memberId: Long): List }