diff --git a/src/main/java/roomescape/reservation/business/ReservationService.kt b/src/main/java/roomescape/reservation/business/ReservationService.kt index e38c0f98..f93c7eb3 100644 --- a/src/main/java/roomescape/reservation/business/ReservationService.kt +++ b/src/main/java/roomescape/reservation/business/ReservationService.kt @@ -1,227 +1,244 @@ -package roomescape.reservation.business; +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.ReservationEntity; -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; +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 -public class ReservationService { +class ReservationService( + private val reservationRepository: ReservationRepository, + private val reservationTimeService: ReservationTimeService, + private val memberService: MemberService, + private val themeService: ThemeService, +) { - private final ReservationRepository reservationRepository; - private final ReservationTimeService reservationTimeService; - private final MemberService memberService; - private final ThemeService themeService; + @Transactional(readOnly = true) + fun findAllReservations(): ReservationsResponse { + val spec: Specification = ReservationSearchSpecification() + .confirmed() + .build() - 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 ReservationsResponse(findAllReservationByStatus(spec)) + } - return new ReservationsResponse(response); - } + @Transactional(readOnly = true) + fun findAllWaiting(): ReservationsResponse { + val spec: Specification = ReservationSearchSpecification() + .waiting() + .build() - @Transactional(readOnly = true) - public ReservationsResponse findAllWaiting() { - Specification spec = new ReservationSearchSpecification().waiting().build(); - List response = findAllReservationByStatus(spec); + return ReservationsResponse(findAllReservationByStatus(spec)) + } - return new ReservationsResponse(response); - } + private fun findAllReservationByStatus(spec: Specification): List { + return reservationRepository.findAll(spec).map { it.toResponse() } + } - private List findAllReservationByStatus(Specification spec) { - return reservationRepository.findAll(spec) - .stream() - .map(ReservationResponse::from) - .toList(); - } + fun removeReservationById(reservationId: Long, memberId: Long) { + validateIsMemberAdmin(memberId) + reservationRepository.deleteById(reservationId) + } - public void removeReservationById(Long reservationId, Long memberId) { - 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) + } + } - public ReservationEntity addReservation(ReservationRequest request, Long memberId) { - validateIsReservationExist(request.themeId, request.timeId, request.date); - ReservationEntity reservation = getReservationForSave(request.timeId, request.themeId, request.date, memberId, - ReservationStatus.CONFIRMED); - return reservationRepository.save(reservation); - } + fun addReservationByAdmin(request: AdminReservationRequest): ReservationResponse { + validateIsReservationExist(request.themeId, request.timeId, request.date) - 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); - } + 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); - } + 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 ReservationResponse addReservationWithoutPayment(Long themeId, Long timeId, LocalDate date, Long memberId, - ReservationStatus status) { - ReservationEntity reservation = getReservationForSave(timeId, themeId, date, memberId, status); - ReservationEntity saved = reservationRepository.save(reservation); - return ReservationResponse.from(saved); - } + 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 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 fun validateMemberAlreadyReserve(themeId: Long?, timeId: Long?, date: LocalDate?, memberId: Long?) { + val spec: Specification = ReservationSearchSpecification() + .sameMemberId(memberId) + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() - 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 RoomescapeException(ErrorType.HAS_RESERVATION_OR_WAITING, HttpStatus.BAD_REQUEST) + } + } - if (reservationRepository.exists(spec)) { - throw new RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT); - } - } + private fun validateIsReservationExist(themeId: Long, timeId: Long, date: LocalDate) { + val spec: Specification = ReservationSearchSpecification() + .confirmed() + .sameThemeId(themeId) + .sameTimeId(timeId) + .sameDate(date) + .build() - 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 - ); - } - } + if (reservationRepository.exists(spec)) { + throw RoomescapeException(ErrorType.RESERVATION_DUPLICATED, HttpStatus.CONFLICT) + } + } - private ReservationEntity 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); + private fun validateDateAndTime( + requestDate: LocalDate, + requestReservationTime: ReservationTimeEntity + ) { + val now = LocalDateTime.now() + val request = LocalDateTime.of(requestDate, requestReservationTime.startAt) - validateDateAndTime(date, time); - return new ReservationEntity(null, date, time, theme, member, status); - } + if (request.isBefore(now)) { + throw RoomescapeException( + ErrorType.RESERVATION_PERIOD_IN_PAST, + "[now: $now | request: $request]", + HttpStatus.BAD_REQUEST + ) + } + } - @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(); + private fun getReservationForSave( + timeId: Long, + themeId: Long, + date: LocalDate, + memberId: Long, + status: ReservationStatus + ): ReservationEntity { + val time = reservationTimeService.findTimeById(timeId) + val theme = themeService.findThemeById(themeId) + val member = memberService.findById(memberId) - List response = reservationRepository.findAll(spec) - .stream() - .map(ReservationResponse::from) - .toList(); + validateDateAndTime(date, time) - return new ReservationsResponse(response); - } + return ReservationEntity( + date = date, + reservationTime = time, + theme = theme, + member = member, + reservationStatus = status + ) + } - 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) + 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() - @Transactional(readOnly = true) - public MyReservationsResponse findMemberReservations(Long memberId) { - return new MyReservationsResponse(reservationRepository.findMyReservations(memberId)); - } + return ReservationsResponse(findAllReservationByStatus(spec)) + } - 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); - } + 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 + ) + } + } - public void cancelWaiting(Long reservationId, Long memberId) { - ReservationEntity waiting = reservationRepository.findById(reservationId) - .filter(ReservationEntity::isWaiting) - .filter(r -> r.isSameMember(memberId)) - .orElseThrow(() -> throwReservationNotFound(reservationId)); - reservationRepository.delete(waiting); - } + @Transactional(readOnly = true) + fun findMemberReservations(memberId: Long): MyReservationsResponse { + return MyReservationsResponse(reservationRepository.findMyReservations(memberId)) + } - public void denyWaiting(Long reservationId, Long memberId) { - validateIsMemberAdmin(memberId); - ReservationEntity waiting = reservationRepository.findById(reservationId) - .filter(ReservationEntity::isWaiting) - .orElseThrow(() -> throwReservationNotFound(reservationId)); - reservationRepository.delete(waiting); - } + 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) + } - private void validateIsMemberAdmin(Long memberId) { - MemberEntity member = memberService.findById(memberId); - if (member.isAdmin()) { - return; - } - throw new RoomescapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, HttpStatus.FORBIDDEN); - } + fun cancelWaiting(reservationId: Long, memberId: Long) { + reservationRepository.findByIdOrNull(reservationId)?.takeIf { + it.isWaiting() && it.isSameMember(memberId) + }?.let { + reservationRepository.delete(it) + } ?: throw throwReservationNotFound(reservationId) + } - private RoomescapeException throwReservationNotFound(Long reservationId) { - return new RoomescapeException(ErrorType.RESERVATION_NOT_FOUND, - String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND); - } + 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 + ) + } } diff --git a/src/main/java/roomescape/reservation/web/ReservationResponse.kt b/src/main/java/roomescape/reservation/web/ReservationResponse.kt index 0503367b..35e6eb36 100644 --- a/src/main/java/roomescape/reservation/web/ReservationResponse.kt +++ b/src/main/java/roomescape/reservation/web/ReservationResponse.kt @@ -4,9 +4,11 @@ import com.fasterxml.jackson.annotation.JsonProperty import io.swagger.v3.oas.annotations.media.Schema import roomescape.member.web.MemberResponse import roomescape.member.web.MemberResponse.Companion.fromEntity +import roomescape.member.web.toResponse import roomescape.reservation.infrastructure.persistence.ReservationEntity import roomescape.reservation.infrastructure.persistence.ReservationStatus import roomescape.theme.web.ThemeResponse +import roomescape.theme.web.toResponse import java.time.LocalDate import java.time.LocalTime @@ -85,10 +87,18 @@ data class ReservationResponse( } } +fun ReservationEntity.toResponse(): ReservationResponse = ReservationResponse( + id = this.id!!, + date = this.date, + member = this.member.toResponse(), + time = this.reservationTime.toResponse(), + theme = this.theme.toResponse(), + status = this.reservationStatus +) + @Schema(name = "예약 목록 조회 응답", description = "모든 예약 정보 조회 응답시 사용됩니다.") @JvmRecord data class ReservationsResponse( @field:Schema(description = "모든 예약 및 대기 목록") val reservations: List ) -