package roomescape.reservation.business import org.springframework.data.jpa.domain.Specification import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import roomescape.common.exception.ErrorType import roomescape.common.exception.RoomescapeException import roomescape.member.business.MemberService import roomescape.reservation.infrastructure.persistence.* import roomescape.reservation.web.* import roomescape.theme.business.ThemeService import java.time.LocalDate import java.time.LocalDateTime @Service @Transactional class ReservationService( private val reservationRepository: ReservationRepository, private val reservationTimeService: ReservationTimeService, private val memberService: MemberService, private val themeService: ThemeService, ) { @Transactional(readOnly = true) fun findAllReservations(): ReservationsResponse { val spec: Specification = ReservationSearchSpecification() .confirmed() .build() return ReservationsResponse(findAllReservationByStatus(spec)) } @Transactional(readOnly = true) fun findAllWaiting(): ReservationsResponse { val spec: Specification = ReservationSearchSpecification() .waiting() .build() return ReservationsResponse(findAllReservationByStatus(spec)) } private fun findAllReservationByStatus(spec: Specification): List { return reservationRepository.findAll(spec).map { it.toResponse() } } fun removeReservationById(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) reservationRepository.deleteById(reservationId) } fun addReservation(request: ReservationRequest, memberId: Long): ReservationEntity { validateIsReservationExist(request.themeId, request.timeId, request.date) return getReservationForSave( request.timeId, request.themeId, request.date, memberId, ReservationStatus.CONFIRMED ).also { reservationRepository.save(it) } } fun addReservationByAdmin(request: AdminReservationRequest): ReservationResponse { validateIsReservationExist(request.themeId, request.timeId, request.date) return addReservationWithoutPayment( request.themeId, request.timeId, request.date, request.memberId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ) } fun addWaiting(request: WaitingRequest, memberId: Long): ReservationResponse { validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId) return addReservationWithoutPayment( request.themeId, request.timeId, request.date, memberId, ReservationStatus.WAITING ) } private fun addReservationWithoutPayment( themeId: Long, timeId: Long, date: LocalDate, memberId: Long, status: ReservationStatus ): ReservationResponse = getReservationForSave(timeId, themeId, date, memberId, status) .also { reservationRepository.save(it) }.toResponse() private fun validateMemberAlreadyReserve(themeId: Long?, timeId: Long?, date: LocalDate?, memberId: Long?) { val spec: Specification = ReservationSearchSpecification() .sameMemberId(memberId) .sameThemeId(themeId) .sameTimeId(timeId) .sameDate(date) .build() if (reservationRepository.exists(spec)) { throw RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST) } } private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) { val spec: Specification = ReservationSearchSpecification() .confirmed() .sameThemeId(themeId) .sameTimeId(timeId) .sameDate(date) .build() if (reservationRepository.exists(spec)) { throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT) } } private fun validateDateAndTime( requestDate: LocalDate, requestReservationTime: ReservationTimeEntity ) { val now = LocalDateTime.now() val request = LocalDateTime.of(requestDate, requestReservationTime.startAt) if (request.isBefore(now)) { throw RoomescapeException( ErrorType.RESERVATION_PERIOD_IN_PAST, "[now: $now | request: $request]", HttpStatus.BAD_REQUEST ) } } private fun getReservationForSave( timeId: Long, themeId: Long, date: LocalDate, memberId: Long, status: ReservationStatus ): ReservationEntity { val time = reservationTimeService.findTimeById(timeId) val theme = themeService.findById(themeId) val member = memberService.findById(memberId) validateDateAndTime(date, time) return ReservationEntity( date = date, reservationTime = time, theme = theme, member = member, reservationStatus = status ) } @Transactional(readOnly = true) fun findFilteredReservations( themeId: Long?, memberId: Long?, dateFrom: LocalDate?, dateTo: LocalDate? ): ReservationsResponse { validateDateForSearch(dateFrom, dateTo) val spec: Specification = ReservationSearchSpecification() .confirmed() .sameThemeId(themeId) .sameMemberId(memberId) .dateStartFrom(dateFrom) .dateEndAt(dateTo) .build() return ReservationsResponse(findAllReservationByStatus(spec)) } private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) { if (startFrom == null || endAt == null) { return } if (startFrom.isAfter(endAt)) { throw RoomescapeException( ErrorType.INVALID_DATE_RANGE, "[startFrom: $startFrom, endAt: $endAt", HttpStatus.BAD_REQUEST ) } } @Transactional(readOnly = true) fun findMemberReservations(memberId: Long): MyReservationsResponse { return MyReservationsResponse(reservationRepository.findMyReservations(memberId)) } fun approveWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) if (reservationRepository.isExistConfirmedReservation(reservationId)) { throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT) } reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) } fun cancelWaiting(reservationId: Long, memberId: Long) { reservationRepository.findByIdOrNull(reservationId)?.takeIf { it.isWaiting() && it.isSameMember(memberId) }?.let { reservationRepository.delete(it) } ?: throw throwReservationNotFound(reservationId) } fun denyWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) reservationRepository.findByIdOrNull(reservationId)?.takeIf { it.isWaiting() }?.let { reservationRepository.delete(it) } ?: throw throwReservationNotFound(reservationId) } private fun validateIsMemberAdmin(memberId: Long) { memberService.findById(memberId).takeIf { it.isAdmin() } ?: throw RoomescapeException( ErrorType.PERMISSION_DOES_NOT_EXIST, "[memberId: $memberId]", HttpStatus.FORBIDDEN ) } private fun throwReservationNotFound(reservationId: Long?): RoomescapeException { return RoomescapeException( ErrorType.RESERVATION_NOT_FOUND, "[reservationId: $reservationId]", HttpStatus.NOT_FOUND ) } }