generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #13 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> theme 패키지 내 코드 및 테스트 코틀린 전환 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> 다른 테스트는 코틀린으로 전환 시 크게 문제가 없었으나, GET /themes/most-reserved-last-week API의 경우 쿼리에 크게 의존하여 mocking을 사용하는 기존 테스트로 처리하기 애매한 부분이 있었음. 따라서, API 테스트는 mocking이 아닌 RestAssured를 이용한 실제 테스트로 진행하였고 \@RequestParam, 날짜 등 실제 비즈니스와 관련된 부분을 위주로 처리하고 쿼리 자체는 Repository 테스트에서 상세하게 검증하였음. ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> 패키지를 reservation 안에 넣는 것은 고민이 조금 더 필요할 것 같음. 현재는 단일 매장에 대한 서비스지만 매장별로 분리하는 것을 고민중인 만큼 코틀린 마이그레이션이 끝난 이후 생각해볼 예정 Reviewed-on: #15 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
228 lines
8.5 KiB
Java
228 lines
8.5 KiB
Java
package roomescape.reservation.service;
|
|
|
|
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.Member;
|
|
import roomescape.reservation.domain.Reservation;
|
|
import roomescape.reservation.domain.ReservationStatus;
|
|
import roomescape.reservation.domain.ReservationTime;
|
|
import roomescape.reservation.domain.repository.ReservationRepository;
|
|
import roomescape.reservation.domain.repository.ReservationSearchSpecification;
|
|
import roomescape.reservation.dto.request.AdminReservationRequest;
|
|
import roomescape.reservation.dto.request.ReservationRequest;
|
|
import roomescape.reservation.dto.request.WaitingRequest;
|
|
import roomescape.reservation.dto.response.MyReservationsResponse;
|
|
import roomescape.reservation.dto.response.ReservationResponse;
|
|
import roomescape.reservation.dto.response.ReservationsResponse;
|
|
import roomescape.theme.infrastructure.persistence.ThemeEntity;
|
|
import roomescape.theme.business.ThemeService;
|
|
|
|
@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<Reservation> spec = new ReservationSearchSpecification().confirmed().build();
|
|
List<ReservationResponse> response = findAllReservationByStatus(spec);
|
|
|
|
return new ReservationsResponse(response);
|
|
}
|
|
|
|
@Transactional(readOnly = true)
|
|
public ReservationsResponse findAllWaiting() {
|
|
Specification<Reservation> spec = new ReservationSearchSpecification().waiting().build();
|
|
List<ReservationResponse> response = findAllReservationByStatus(spec);
|
|
|
|
return new ReservationsResponse(response);
|
|
}
|
|
|
|
private List<ReservationResponse> findAllReservationByStatus(Specification<Reservation> 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<Reservation> 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<Reservation> 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,
|
|
ReservationTime 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) {
|
|
ReservationTime time = reservationTimeService.findTimeById(timeId);
|
|
ThemeEntity theme = themeService.findThemeById(themeId);
|
|
Member 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<Reservation> spec = new ReservationSearchSpecification()
|
|
.confirmed()
|
|
.sameThemeId(themeId)
|
|
.sameMemberId(memberId)
|
|
.dateStartFrom(dateFrom)
|
|
.dateEndAt(dateTo)
|
|
.build();
|
|
|
|
List<ReservationResponse> 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) {
|
|
Member 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);
|
|
}
|
|
}
|