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.common.dto.CurrentUserContext import roomescape.common.dto.PrincipalType import roomescape.common.util.DateUtils import roomescape.member.business.UserService import roomescape.member.web.UserContactRetrieveResponse 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.ThemeService import roomescape.theme.web.ThemeInfoRetrieveResponse import java.time.LocalDate import java.time.LocalDateTime private val log: KLogger = KotlinLogging.logger {} @Service class ReservationService( private val reservationRepository: ReservationRepository, private val reservationValidator: ReservationValidator, private val scheduleService: ScheduleService, private val userService: UserService, private val themeService: ThemeService, private val canceledReservationRepository: CanceledReservationRepository, private val tsidFactory: TsidFactory, private val paymentService: PaymentService ) { @Transactional fun createPendingReservation( user: CurrentUserContext, request: PendingReservationCreateRequest ): PendingReservationCreateResponse { log.info { "[ReservationService.createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } validateCanCreate(request) val reservation: ReservationEntity = request.toEntity(id = tsidFactory.next(), userId = user.id) 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) run { reservation.confirm() scheduleService.updateSchedule( reservation.scheduleId, ScheduleUpdateRequest(status = ScheduleStatus.RESERVED) ) }.also { log.info { "[ReservationService.confirmReservation] Pending 예약 확정 완료: reservationId=${id}" } } } @Transactional fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) { log.info { "[ReservationService.cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" } val reservation: ReservationEntity = findOrThrow(reservationId) run { scheduleService.updateSchedule( reservation.scheduleId, ScheduleUpdateRequest(status = ScheduleStatus.AVAILABLE) ) saveCanceledReservation(user, reservation, request.cancelReason) reservation.cancel() }.also { log.info { "[ReservationService.cancelReservation] 예약 취소 완료: reservationId=${reservationId}" } } } @Transactional(readOnly = true) fun findUserSummaryReservation(user: CurrentUserContext): ReservationSummaryRetrieveListResponse { log.info { "[ReservationService.findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" } val reservations: List = reservationRepository.findAllByUserId(user.id) return ReservationSummaryRetrieveListResponse(reservations.map { val schedule: ScheduleSummaryResponse = scheduleService.findSummaryById(it.scheduleId) val theme: ThemeInfoRetrieveResponse = themeService.findSummaryById(schedule.themeId) ReservationSummaryRetrieveResponse( id = it.id, themeName = theme.name, date = schedule.date, startAt = schedule.time, status = it.status ) }).also { log.info { "[ReservationService.findSummaryByMemberId] ${it.reservations.size}개의 예약 조회 완료: userId=${user.id}" } } } @Transactional(readOnly = true) fun findDetailById(id: Long): ReservationDetailRetrieveResponse { log.info { "[ReservationService.findDetailById] 예약 상세 조회 시작: reservationId=${id}" } val reservation: ReservationEntity = findOrThrow(id) val user: UserContactRetrieveResponse = userService.findContactById(reservation.userId) val paymentDetail: PaymentRetrieveResponse = paymentService.findDetailByReservationId(id) return reservation.toReservationDetailRetrieveResponse( user = user, payment = paymentDetail ).also { log.info { "[ReservationService.findDetailById] 예약 상세 조회 완료: reservationId=${id}" } } } @Transactional(readOnly = true) fun findMostReservedThemeIds(count: Int): MostReservedThemeIdListResponse { log.info { "[ReservationService.findMostReservedThemeIds] 인기 테마 조회 시작: count=$count" } val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(LocalDate.now()) val previousWeekSaturday = previousWeekSunday.plusDays(6) val themeIds: List = reservationRepository.findMostReservedThemeIds( dateFrom = previousWeekSunday, dateTo = previousWeekSaturday, count = count ) return MostReservedThemeIdListResponse(themeIds = themeIds).also { log.info { "[ReservationService.findMostReservedThemeIds] 인기 테마 조회 완료: count=${it.themeIds.size}" } } } 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( user: CurrentUserContext, reservation: ReservationEntity, cancelReason: String ) { if (user.type != PrincipalType.ADMIN && reservation.userId != user.id) { log.warn { "[ReservationService.createCanceledPayment] 예약자 본인 또는 관리자가 아닌 회원의 취소 요청: reservationId=${reservation.id}, userId=${user.id}" } throw ReservationException(ReservationErrorCode.NO_PERMISSION_TO_CANCEL_RESERVATION) } CanceledReservationEntity( id = tsidFactory.next(), reservationId = reservation.id, canceledBy = user.id, cancelReason = cancelReason, canceledAt = LocalDateTime.now(), status = CanceledReservationStatus.PROCESSING ).also { canceledReservationRepository.save(it) } } private fun validateCanCreate(request: PendingReservationCreateRequest) { val schedule = scheduleService.findSummaryById(request.scheduleId) val theme = themeService.findSummaryById(schedule.themeId) reservationValidator.validateCanCreate(schedule, theme, request) } }