package roomescape.reservation.business; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import org.springframework.data.jpa.domain.Specification; 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.member.infrastructure.persistence.MemberEntity; import roomescape.reservation.infrastructure.persistence.Reservation; import roomescape.reservation.infrastructure.persistence.ReservationRepository; import roomescape.reservation.infrastructure.persistence.ReservationSearchSpecification; import roomescape.reservation.infrastructure.persistence.ReservationStatus; import roomescape.reservation.infrastructure.persistence.ReservationTimeEntity; import roomescape.reservation.web.AdminReservationRequest; import roomescape.reservation.web.MyReservationsResponse; import roomescape.reservation.web.ReservationRequest; import roomescape.reservation.web.ReservationResponse; import roomescape.reservation.web.ReservationsResponse; import roomescape.reservation.web.WaitingRequest; import roomescape.theme.business.ThemeService; import roomescape.theme.infrastructure.persistence.ThemeEntity; @Service @Transactional public class ReservationService { private final ReservationRepository reservationRepository; private final ReservationTimeService reservationTimeService; private final MemberService memberService; private final ThemeService themeService; public ReservationService( ReservationRepository reservationRepository, ReservationTimeService reservationTimeService, MemberService memberService, ThemeService themeService ) { this.reservationRepository = reservationRepository; this.reservationTimeService = reservationTimeService; this.memberService = memberService; this.themeService = themeService; } @Transactional(readOnly = true) public ReservationsResponse findAllReservations() { Specification spec = new ReservationSearchSpecification().confirmed().build(); List response = findAllReservationByStatus(spec); return new ReservationsResponse(response); } @Transactional(readOnly = true) public ReservationsResponse findAllWaiting() { Specification spec = new ReservationSearchSpecification().waiting().build(); List response = findAllReservationByStatus(spec); return new ReservationsResponse(response); } private List findAllReservationByStatus(Specification spec) { return reservationRepository.findAll(spec) .stream() .map(ReservationResponse::from) .toList(); } public void removeReservationById(Long reservationId, Long memberId) { validateIsMemberAdmin(memberId); reservationRepository.deleteById(reservationId); } public Reservation addReservation(ReservationRequest request, Long memberId) { validateIsReservationExist(request.themeId, request.timeId, request.date); Reservation reservation = getReservationForSave(request.timeId, request.themeId, request.date, memberId, ReservationStatus.CONFIRMED); return reservationRepository.save(reservation); } public ReservationResponse addReservationByAdmin(AdminReservationRequest request) { validateIsReservationExist(request.themeId, request.timeId, request.date); return addReservationWithoutPayment(request.themeId, request.timeId, request.date, request.memberId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED); } public ReservationResponse addWaiting(WaitingRequest request, Long memberId) { validateMemberAlreadyReserve(request.themeId, request.timeId, request.date, memberId); return addReservationWithoutPayment(request.themeId, request.timeId, request.date, memberId, ReservationStatus.WAITING); } private ReservationResponse addReservationWithoutPayment(Long themeId, Long timeId, LocalDate date, Long memberId, ReservationStatus status) { Reservation reservation = getReservationForSave(timeId, themeId, date, memberId, status); Reservation saved = reservationRepository.save(reservation); return ReservationResponse.from(saved); } private void validateMemberAlreadyReserve(Long themeId, Long timeId, LocalDate date, Long memberId) { Specification spec = new ReservationSearchSpecification() .sameMemberId(memberId) .sameThemeId(themeId) .sameTimeId(timeId) .sameDate(date) .build(); if (reservationRepository.exists(spec)) { throw new RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST); } } private void validateIsReservationExist(Long themeId, Long timeId, LocalDate date) { Specification spec = new ReservationSearchSpecification() .confirmed() .sameThemeId(themeId) .sameTimeId(timeId) .sameDate(date) .build(); if (reservationRepository.exists(spec)) { throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); } } private void validateDateAndTime( LocalDate requestDate, ReservationTimeEntity requestReservationTime ) { LocalDateTime now = LocalDateTime.now(); LocalDateTime request = LocalDateTime.of(requestDate, requestReservationTime.getStartAt()); if (request.isBefore(now)) { throw new RoomescapeException(ErrorType.RESERVATION_PERIOD_IN_PAST, String.format("[now: %s %s | request: %s %s]", now.toLocalDate(), now.toLocalTime(), requestDate, requestReservationTime.getStartAt()), HttpStatus.BAD_REQUEST ); } } private Reservation getReservationForSave(Long timeId, Long themeId, LocalDate date, Long memberId, ReservationStatus status) { ReservationTimeEntity time = reservationTimeService.findTimeById(timeId); ThemeEntity theme = themeService.findThemeById(themeId); MemberEntity member = memberService.findById(memberId); validateDateAndTime(date, time); return new Reservation(date, time, theme, member, status); } @Transactional(readOnly = true) public ReservationsResponse findFilteredReservations(Long themeId, Long memberId, LocalDate dateFrom, LocalDate dateTo) { validateDateForSearch(dateFrom, dateTo); Specification spec = new ReservationSearchSpecification() .confirmed() .sameThemeId(themeId) .sameMemberId(memberId) .dateStartFrom(dateFrom) .dateEndAt(dateTo) .build(); List response = reservationRepository.findAll(spec) .stream() .map(ReservationResponse::from) .toList(); return new ReservationsResponse(response); } private void validateDateForSearch(LocalDate startFrom, LocalDate endAt) { if (startFrom == null || endAt == null) { return; } if (startFrom.isAfter(endAt)) { throw new RoomescapeException(ErrorType.INVALID_DATE_RANGE, String.format("[startFrom: %s, endAt: %s", startFrom, endAt), HttpStatus.BAD_REQUEST); } } @Transactional(readOnly = true) public MyReservationsResponse findMemberReservations(Long memberId) { return new MyReservationsResponse(reservationRepository.findMyReservations(memberId)); } public void approveWaiting(Long reservationId, Long memberId) { validateIsMemberAdmin(memberId); if (reservationRepository.isExistConfirmedReservation(reservationId)) { throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); } reservationRepository.updateStatusByReservationId(reservationId, ReservationStatus.CONFIRMED_PAYMENT_REQUIRED); } public void cancelWaiting(Long reservationId, Long memberId) { Reservation waiting = reservationRepository.findById(reservationId) .filter(Reservation::isWaiting) .filter(r -> r.isSameMember(memberId)) .orElseThrow(() -> throwReservationNotFound(reservationId)); reservationRepository.delete(waiting); } public void denyWaiting(Long reservationId, Long memberId) { validateIsMemberAdmin(memberId); Reservation waiting = reservationRepository.findById(reservationId) .filter(Reservation::isWaiting) .orElseThrow(() -> throwReservationNotFound(reservationId)); reservationRepository.delete(waiting); } private void validateIsMemberAdmin(Long memberId) { MemberEntity member = memberService.findById(memberId); if (member.isAdmin()) { return; } throw new RoomescapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN); } private RoomescapeException throwReservationNotFound(Long reservationId) { return new RoomescapeException(ErrorType.RESERVATION_NOT_FOUND, String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND); } }