package roomescape.reservation.business 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.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 @Service @Transactional class ReservationService( 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() return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) } @Transactional(readOnly = true) fun findAllWaiting(): ReservationRetrieveListResponse { val spec: Specification = ReservationSearchSpecification() .waiting() .build() return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) } private fun findAllReservationByStatus(spec: Specification): List { return reservationRepository.findAll(spec).map { it.toRetrieveResponse() } } fun deleteReservation(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) reservationRepository.deleteById(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) val reservation: ReservationEntity = createEntity(timeId, themeId, date, memberId, ReservationStatus.CONFIRMED) return reservationRepository.save(reservation) } fun createReservationByAdmin(request: AdminReservationCreateRequest): ReservationRetrieveResponse { validateIsReservationExist(request.themeId, request.timeId, request.date) return addReservationWithoutPayment( request.themeId, request.timeId, request.date, request.memberId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED ) } fun createWaiting(request: WaitingCreateRequest, memberId: Long): ReservationRetrieveResponse { 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 ): ReservationRetrieveResponse = createEntity(timeId, themeId, date, memberId, status) .also { reservationRepository.save(it) }.toRetrieveResponse() 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 ReservationException(ReservationErrorCode.ALREADY_RESERVE) } } 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 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)) { 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( date = date, time = time, theme = theme, member = member, reservationStatus = status ) } @Transactional(readOnly = true) fun searchReservations( themeId: Long?, memberId: Long?, dateFrom: LocalDate?, dateTo: LocalDate? ): ReservationRetrieveListResponse { validateDateForSearch(dateFrom, dateTo) val spec: Specification = ReservationSearchSpecification() .confirmed() .sameThemeId(themeId) .sameMemberId(memberId) .dateStartFrom(dateFrom) .dateEndAt(dateTo) .build() return ReservationRetrieveListResponse(findAllReservationByStatus(spec)) } private fun validateDateForSearch(startFrom: LocalDate?, endAt: LocalDate?) { if (startFrom == null || endAt == null) { return } if (startFrom.isAfter(endAt)) { throw ReservationException(ReservationErrorCode.INVALID_SEARCH_DATE_RANGE) } } @Transactional(readOnly = true) fun findReservationsByMemberId(memberId: Long): MyReservationRetrieveListResponse { return MyReservationRetrieveListResponse(reservationRepository.findAllByMemberId(memberId)) } fun confirmWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) if (reservationRepository.isExistConfirmedReservation(reservationId)) { throw ReservationException(ReservationErrorCode.CONFIRMED_RESERVATION_ALREADY_EXISTS) } reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED) } fun deleteWaiting(reservationId: Long, memberId: Long) { val reservation: ReservationEntity = findReservationOrThrow(reservationId) if (!reservation.isWaiting()) { throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) } if (!reservation.isReservedBy(memberId)) { throw ReservationException(ReservationErrorCode.NOT_RESERVATION_OWNER) } reservationRepository.delete(reservation) } fun rejectWaiting(reservationId: Long, memberId: Long) { validateIsMemberAdmin(memberId) val reservation: ReservationEntity = findReservationOrThrow(reservationId) if (!reservation.isWaiting()) { throw ReservationException(ReservationErrorCode.ALREADY_CONFIRMED) } reservationRepository.delete(reservation) } private fun validateIsMemberAdmin(memberId: Long) { val member: MemberEntity = memberService.findById(memberId) if (member.isAdmin()) { return } throw ReservationException(ReservationErrorCode.NO_PERMISSION) } private fun findReservationOrThrow(reservationId: Long): ReservationEntity { return reservationRepository.findByIdOrNull(reservationId) ?: throw ReservationException(ReservationErrorCode.RESERVATION_NOT_FOUND) } }