pricelees 8a4f71be39 [#13] Theme 도메인 코드 코틀린 마이그레이션 (#15)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#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>
2025-07-17 16:37:27 +00:00

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);
}
}