diff --git a/src/main/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleService.java b/src/main/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleService.java index 751d6e67..826bcdcb 100644 --- a/src/main/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleService.java +++ b/src/main/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleService.java @@ -12,8 +12,10 @@ import com.back.b2st.domain.performanceschedule.dto.response.PerformanceScheduleDetailRes; import com.back.b2st.domain.performanceschedule.dto.response.PerformanceScheduleListRes; import com.back.b2st.domain.performanceschedule.entity.PerformanceSchedule; +import com.back.b2st.domain.performanceschedule.entity.BookingType; import com.back.b2st.domain.performanceschedule.error.PerformanceScheduleErrorCode; import com.back.b2st.domain.performanceschedule.repository.PerformanceScheduleRepository; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.scheduleseat.entity.ScheduleSeat; import com.back.b2st.domain.scheduleseat.error.ScheduleSeatErrorCode; import com.back.b2st.domain.scheduleseat.repository.ScheduleSeatRepository; @@ -33,6 +35,7 @@ public class PerformanceScheduleService { private final SeatRepository seatRepository; private final ScheduleSeatRepository scheduleSeatRepository; + private final PrereservationTimeTableService prereservationTimeTableService; /** * 회차 생성 (관리자) @@ -63,6 +66,10 @@ public PerformanceScheduleCreateRes createSchedule(Long performanceId, Performan PerformanceSchedule saved = performanceScheduleRepository.save(schedule); + if (saved.getBookingType() == BookingType.PRERESERVE) { + prereservationTimeTableService.ensureDefaultTimeTablesIfMissing(saved.getPerformanceScheduleId()); + } + createScheduleSeats(saved.getPerformanceScheduleId(), performance.getVenue().getVenueId()); return PerformanceScheduleCreateRes.from(saved); diff --git a/src/main/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldService.java b/src/main/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldService.java index 4bd3bfa1..ad1eda60 100644 --- a/src/main/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldService.java +++ b/src/main/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldService.java @@ -2,8 +2,8 @@ import java.time.LocalDateTime; -import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.back.b2st.domain.performanceschedule.entity.BookingType; @@ -11,12 +11,13 @@ import com.back.b2st.domain.performanceschedule.repository.PerformanceScheduleRepository; import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; +import com.back.b2st.domain.scheduleseat.error.ScheduleSeatErrorCode; import com.back.b2st.domain.seat.seat.entity.Seat; import com.back.b2st.domain.seat.seat.repository.SeatRepository; import com.back.b2st.domain.venue.section.entity.Section; import com.back.b2st.domain.venue.section.repository.SectionRepository; -import com.back.b2st.domain.scheduleseat.error.ScheduleSeatErrorCode; import com.back.b2st.global.error.exception.BusinessException; import lombok.RequiredArgsConstructor; @@ -30,6 +31,7 @@ public class PrereservationHoldService { private final SectionRepository sectionRepository; private final PrereservationRepository prereservationRepository; private final PrereservationSlotService prereservationSlotService; + private final PrereservationTimeTableService prereservationTimeTableService; @Value("${prereservation.booking.strict:true}") private boolean bookingStrict = true; @@ -37,7 +39,7 @@ public class PrereservationHoldService { @Value("${prereservation.slot.strict:true}") private boolean slotStrict = true; - @Transactional(readOnly = true) + @Transactional public void validateSeatHoldAllowed(Long memberId, Long scheduleId, Long seatId) { PerformanceSchedule schedule = performanceScheduleRepository.findById(scheduleId) .orElseThrow(() -> new BusinessException(PrereservationErrorCode.SCHEDULE_NOT_FOUND)); @@ -78,6 +80,8 @@ public void validateSeatHoldAllowed(Long memberId, Long scheduleId, Long seatId) return; } + prereservationTimeTableService.ensureDefaultTimeTablesIfMissing(scheduleId); + Section section = sectionRepository.findById(seatSectionId) .orElseThrow(() -> new BusinessException(PrereservationErrorCode.SECTION_NOT_FOUND)); diff --git a/src/main/java/com/back/b2st/domain/prereservation/entry/controller/PrereservationController.java b/src/main/java/com/back/b2st/domain/prereservation/entry/controller/PrereservationController.java index 704ba1a8..502829ef 100644 --- a/src/main/java/com/back/b2st/domain/prereservation/entry/controller/PrereservationController.java +++ b/src/main/java/com/back/b2st/domain/prereservation/entry/controller/PrereservationController.java @@ -40,8 +40,8 @@ public class PrereservationController { summary = "사전 구역 신청", description = """ BookingType이 PRERESERVE(신청 예매)인 회차에 대해 예매 가능한 구역을 사전에 신청합니다. - - 예매 오픈 시간(bookingOpenAt) 24시간 전부터 신청 가능 - - 예매 오픈 시간(bookingOpenAt) 이후 신청 불가 + - 예매 오픈 날짜 기준 전날 00:00부터 신청 가능 + - 예매 오픈 날짜 기준 당일 00:00 이후 신청 불가 - 동일 회차/구역 중복 신청 불가 """ ) diff --git a/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyService.java b/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyService.java index 1892f572..aec2f0da 100644 --- a/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyService.java +++ b/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyService.java @@ -1,6 +1,7 @@ package com.back.b2st.domain.prereservation.entry.service; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; @@ -8,8 +9,8 @@ import java.util.TreeMap; import java.util.TreeSet; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,6 +22,7 @@ import com.back.b2st.domain.prereservation.entry.entity.Prereservation; import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; import com.back.b2st.domain.venue.section.entity.Section; import com.back.b2st.domain.venue.section.repository.SectionRepository; @@ -38,11 +40,15 @@ public class PrereservationApplyService { private final SectionRepository sectionRepository; private final PrereservationRepository prereservationRepository; private final PrereservationSlotService prereservationSlotService; + private final PrereservationTimeTableService prereservationTimeTableService; private final EmailSender emailSender; @Value("${prereservation.application.strict:true}") private boolean applicationStrict = true; + @Value("${prereservation.slot.strict:true}") + private boolean slotStrict = true; + @Value("${app.frontend.my-page-url:https://doncrytt.vercel.app/my-page}") private String myPageUrl = "https://doncrytt.vercel.app/my-page"; @@ -61,11 +67,12 @@ public void apply(Long scheduleId, Long memberId, String email, Long sectionId) if (applicationStrict) { LocalDateTime now = LocalDateTime.now(); - LocalDateTime applyOpenAt = bookingOpenAt.minusDays(1); + LocalDateTime applyOpenAt = bookingOpenAt.toLocalDate().minusDays(1).atStartOfDay(); + LocalDateTime applyCloseAt = bookingOpenAt.toLocalDate().atTime(LocalTime.MIDNIGHT); if (now.isBefore(applyOpenAt)) { throw new BusinessException(PrereservationErrorCode.APPLICATION_NOT_OPEN); } - if (!now.isBefore(bookingOpenAt)) { + if (!now.isBefore(applyCloseAt)) { throw new BusinessException(PrereservationErrorCode.APPLICATION_CLOSED); } } @@ -96,11 +103,27 @@ public void apply(Long scheduleId, Long memberId, String email, Long sectionId) } if (email != null && !email.isBlank()) { - var slot = prereservationSlotService.calculateSlotOrThrow(schedule, section); + var slot = slotStrict + ? calculateSlotOrEnsure(scheduleId, schedule, section) + : new PrereservationSlotService.Slot( + schedule.getBookingOpenAt(), + schedule.getBookingCloseAt() != null + ? schedule.getBookingCloseAt() + : schedule.getBookingOpenAt().plusDays(30) + ); sendAppliedEmail(email, section.getSectionName(), slot.startAt(), slot.endAt()); } } + private PrereservationSlotService.Slot calculateSlotOrEnsure( + Long scheduleId, + PerformanceSchedule schedule, + Section section + ) { + prereservationTimeTableService.ensureDefaultTimeTablesIfMissing(scheduleId); + return prereservationSlotService.calculateSlotOrThrow(schedule, section); + } + @Transactional(readOnly = true) public PrereservationRes getMyApplications(Long scheduleId, Long memberId) { PerformanceSchedule schedule = getScheduleOrThrow(scheduleId); @@ -159,10 +182,10 @@ private PerformanceSchedule getScheduleOrThrow(Long scheduleId) { private void sendAppliedEmail(String email, String sectionName, LocalDateTime startAt, LocalDateTime endAt) { String message = """ 신청 예매 사전 신청이 완료되었습니다. - + - 신청 구역: %s - 예매 가능 시간: %s ~ %s - + 해당 시간 외에는 예매가 불가능합니다. """.formatted( sectionName, diff --git a/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionService.java b/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionService.java index df7d87ac..5c647d66 100644 --- a/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionService.java +++ b/src/main/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionService.java @@ -16,6 +16,7 @@ import com.back.b2st.domain.prereservation.entry.entity.Prereservation; import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; import com.back.b2st.domain.venue.section.entity.Section; import com.back.b2st.domain.venue.section.repository.SectionRepository; @@ -31,11 +32,12 @@ public class PrereservationSectionService { private final SectionRepository sectionRepository; private final PrereservationRepository prereservationRepository; private final PrereservationSlotService prereservationSlotService; + private final PrereservationTimeTableService prereservationTimeTableService; @Value("${prereservation.slot.strict:true}") private boolean slotStrict = true; - @Transactional(readOnly = true) + @Transactional public List getSections(Long scheduleId, Long memberId) { PerformanceSchedule schedule = performanceScheduleRepository.findById(scheduleId) .orElseThrow(() -> new BusinessException(PrereservationErrorCode.SCHEDULE_NOT_FOUND)); @@ -56,16 +58,20 @@ public List getSections(Long scheduleId, Long memberId .map(Prereservation::getSectionId) .collect(Collectors.toSet()); + if (slotStrict) { + prereservationTimeTableService.ensureDefaultTimeTablesIfMissing(scheduleId); + } + return sections.stream() .map(section -> { var slot = slotStrict ? prereservationSlotService.calculateSlotOrThrow(schedule, section) : new PrereservationSlotService.Slot( - schedule.getBookingOpenAt(), - schedule.getBookingCloseAt() != null - ? schedule.getBookingCloseAt() - : schedule.getBookingOpenAt().plusDays(30) - ); + schedule.getBookingOpenAt(), + schedule.getBookingCloseAt() != null + ? schedule.getBookingCloseAt() + : schedule.getBookingOpenAt().plusDays(30) + ); return new PrereservationSectionRes( section.getId(), section.getSectionName(), diff --git a/src/main/java/com/back/b2st/domain/prereservation/policy/service/PrereservationTimeTableService.java b/src/main/java/com/back/b2st/domain/prereservation/policy/service/PrereservationTimeTableService.java index 16471891..1227f9c1 100644 --- a/src/main/java/com/back/b2st/domain/prereservation/policy/service/PrereservationTimeTableService.java +++ b/src/main/java/com/back/b2st/domain/prereservation/policy/service/PrereservationTimeTableService.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -64,6 +65,47 @@ public void upsert(Long scheduleId, List items } } + @Transactional + public void ensureDefaultTimeTablesIfMissing(Long scheduleId) { + PerformanceSchedule schedule = validatePrereserveScheduleOrThrow(scheduleId); + + Long venueId = schedule.getPerformance().getVenue().getVenueId(); + List
sections = sectionRepository.findByVenueId(venueId).stream() + .sorted(java.util.Comparator.comparing(Section::getSectionName)) + .toList(); + + if (sections.isEmpty()) { + throw new BusinessException(PrereservationErrorCode.SECTION_NOT_FOUND); + } + + LocalDateTime bookingOpenAt = schedule.getBookingOpenAt(); + + for (int idx = 0; idx < sections.size(); idx++) { + Section section = sections.get(idx); + + if (prereservationTimeTableRepository.findByPerformanceScheduleIdAndSectionId(scheduleId, section.getId()) + .isPresent()) { + continue; + } + + LocalDateTime startAt = bookingOpenAt.plusHours(idx); + LocalDateTime endAt = startAt.plusHours(1).minusSeconds(1); + + validateTimeRangeOrThrow(startAt, endAt); + validateWithinScheduleOrThrow(schedule, startAt, endAt); + + try { + prereservationTimeTableRepository.save(PrereservationTimeTable.builder() + .performanceScheduleId(scheduleId) + .sectionId(section.getId()) + .bookingStartAt(startAt) + .bookingEndAt(endAt) + .build()); + } catch (DataIntegrityViolationException ignored) { + } + } + } + private PerformanceSchedule validatePrereserveScheduleOrThrow(Long scheduleId) { PerformanceSchedule schedule = performanceScheduleRepository.findById(scheduleId) .orElseThrow(() -> new BusinessException(PrereservationErrorCode.SCHEDULE_NOT_FOUND)); diff --git a/src/test/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleServiceTest.java b/src/test/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleServiceTest.java index 33b26642..59975e08 100644 --- a/src/test/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleServiceTest.java +++ b/src/test/java/com/back/b2st/domain/performanceschedule/service/PerformanceScheduleServiceTest.java @@ -24,6 +24,7 @@ import com.back.b2st.domain.performanceschedule.entity.PerformanceSchedule; import com.back.b2st.domain.performanceschedule.error.PerformanceScheduleErrorCode; import com.back.b2st.domain.performanceschedule.repository.PerformanceScheduleRepository; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.scheduleseat.entity.ScheduleSeat; import com.back.b2st.domain.scheduleseat.repository.ScheduleSeatRepository; import com.back.b2st.domain.seat.seat.entity.Seat; @@ -46,6 +47,9 @@ class PerformanceScheduleServiceTest { @Mock private ScheduleSeatRepository scheduleSeatRepository; + @Mock + private PrereservationTimeTableService prereservationTimeTableService; + @InjectMocks private PerformanceScheduleService performanceScheduleService; diff --git a/src/test/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldServiceTest.java b/src/test/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldServiceTest.java index 350d4000..769d1e2b 100644 --- a/src/test/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldServiceTest.java +++ b/src/test/java/com/back/b2st/domain/prereservation/booking/service/PrereservationHoldServiceTest.java @@ -19,6 +19,7 @@ import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.seat.seat.entity.Seat; import com.back.b2st.domain.seat.seat.repository.SeatRepository; import com.back.b2st.domain.scheduleseat.error.ScheduleSeatErrorCode; @@ -44,6 +45,9 @@ class PrereservationHoldServiceTest { @Mock private PrereservationSlotService prereservationSlotService; + @Mock + private PrereservationTimeTableService prereservationTimeTableService; + @InjectMocks private PrereservationHoldService prereservationHoldService; diff --git a/src/test/java/com/back/b2st/domain/prereservation/entry/PrereservationInitDataFlowTest.java b/src/test/java/com/back/b2st/domain/prereservation/entry/PrereservationInitDataFlowTest.java index 128a343a..4a82ebd2 100644 --- a/src/test/java/com/back/b2st/domain/prereservation/entry/PrereservationInitDataFlowTest.java +++ b/src/test/java/com/back/b2st/domain/prereservation/entry/PrereservationInitDataFlowTest.java @@ -71,7 +71,7 @@ void prereservation_flow_success() { .status(PerformanceStatus.ACTIVE) .build()); - LocalDateTime bookingOpenAt = LocalDateTime.now().plusHours(1); + LocalDateTime bookingOpenAt = LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0); LocalDateTime bookingCloseAt = bookingOpenAt.plusHours(3); PerformanceSchedule schedule = performanceScheduleRepository.save(PerformanceSchedule.builder() @@ -129,4 +129,3 @@ void prereservation_flow_success() { .isFalse(); } } - diff --git a/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyServiceTest.java b/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyServiceTest.java index 3e1993e9..b2b83ae5 100644 --- a/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyServiceTest.java +++ b/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationApplyServiceTest.java @@ -13,6 +13,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.util.ReflectionTestUtils; import com.back.b2st.domain.email.service.EmailSender; import com.back.b2st.domain.performanceschedule.entity.BookingType; @@ -24,6 +25,7 @@ import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.venue.section.entity.Section; import com.back.b2st.domain.venue.section.repository.SectionRepository; import com.back.b2st.domain.venue.venue.entity.Venue; @@ -44,6 +46,9 @@ class PrereservationApplyServiceTest { @Mock private PrereservationSlotService prereservationSlotService; + @Mock + private PrereservationTimeTableService prereservationTimeTableService; + @Mock private EmailSender emailSender; @@ -110,12 +115,12 @@ void apply_beforeApplyOpen_throw() { } @Test - @DisplayName("apply(): 예매 오픈 시간 이후면 APPLICATION_CLOSED 예외") - void apply_afterBookingOpen_throw() { + @DisplayName("apply(): 예매 오픈 날짜 당일이면 APPLICATION_CLOSED 예외") + void apply_onBookingDate_throw() { // given PerformanceSchedule schedule = mock(PerformanceSchedule.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().minusMinutes(1)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().toLocalDate().atTime(23, 59)); given(performanceScheduleRepository.findById(SCHEDULE_ID)).willReturn(Optional.of(schedule)); // when & then @@ -130,7 +135,7 @@ void apply_sectionNotFound_throw() { // given PerformanceSchedule schedule = mock(PerformanceSchedule.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusMinutes(10)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0)); given(performanceScheduleRepository.findById(SCHEDULE_ID)).willReturn(Optional.of(schedule)); given(sectionRepository.findById(SECTION_ID)).willReturn(Optional.empty()); @@ -153,7 +158,7 @@ void apply_sectionNotInVenue_throw() { Section section = mock(Section.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusMinutes(10)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0)); given(schedule.getPerformance()).willReturn(performance); given(performance.getVenue()).willReturn(venue); given(venue.getVenueId()).willReturn(venueId); @@ -181,7 +186,7 @@ void apply_alreadyApplied_throw() { Section section = mock(Section.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusMinutes(10)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0)); given(schedule.getPerformance()).willReturn(performance); given(performance.getVenue()).willReturn(venue); given(venue.getVenueId()).willReturn(venueId); @@ -212,7 +217,7 @@ void apply_success() { Section section = mock(Section.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusMinutes(10)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0)); given(schedule.getPerformance()).willReturn(performance); given(performance.getVenue()).willReturn(venue); given(venue.getVenueId()).willReturn(venueId); @@ -240,7 +245,7 @@ void apply_successWithEmail_sendsEmail() { Long venueId = 100L; String email = "user@example.com"; - LocalDateTime bookingOpenAt = LocalDateTime.now().plusMinutes(10); + LocalDateTime bookingOpenAt = LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0); LocalDateTime slotStartAt = bookingOpenAt.plusHours(1); LocalDateTime slotEndAt = bookingOpenAt.plusHours(2); @@ -292,7 +297,7 @@ void apply_duplicateOnSave_throw() { Section section = mock(Section.class); given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); - given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusMinutes(10)); + given(schedule.getBookingOpenAt()).willReturn(LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0)); given(schedule.getPerformance()).willReturn(performance); given(performance.getVenue()).willReturn(venue); given(venue.getVenueId()).willReturn(venueId); @@ -437,4 +442,44 @@ void getMyApplicationList_duplicateSectionInSameSchedule() { assertThat(result.get(0).scheduleId()).isEqualTo(10L); assertThat(result.get(0).sectionIds()).containsExactly(1L); } + + @Test + @DisplayName("apply(): slotStrict=false이면 타임테이블 없이도 이메일 발송까지 성공한다") + void apply_slotStrictFalse_allowsWithoutTimeTable() { + // given + Long venueId = 100L; + String email = "user@example.com"; + + ReflectionTestUtils.setField(prereservationApplyService, "slotStrict", false); + + LocalDateTime bookingOpenAt = LocalDateTime.now().plusDays(1).withHour(13).withMinute(0).withSecond(0).withNano(0); + + PerformanceSchedule schedule = mock(PerformanceSchedule.class); + Performance performance = mock(Performance.class); + Venue venue = mock(Venue.class); + Section section = mock(Section.class); + + given(schedule.getBookingType()).willReturn(BookingType.PRERESERVE); + given(schedule.getBookingOpenAt()).willReturn(bookingOpenAt); + given(schedule.getBookingCloseAt()).willReturn(null); + given(schedule.getPerformance()).willReturn(performance); + given(performance.getVenue()).willReturn(venue); + given(venue.getVenueId()).willReturn(venueId); + + given(section.getVenueId()).willReturn(venueId); + given(section.getSectionName()).willReturn("A구역"); + + given(performanceScheduleRepository.findById(SCHEDULE_ID)).willReturn(Optional.of(schedule)); + given(sectionRepository.findById(SECTION_ID)).willReturn(Optional.of(section)); + given(prereservationRepository.existsByPerformanceScheduleIdAndMemberIdAndSectionId( + SCHEDULE_ID, MEMBER_ID, SECTION_ID + )).willReturn(false); + + // when & then + assertThatCode(() -> prereservationApplyService.apply(SCHEDULE_ID, MEMBER_ID, email, SECTION_ID)) + .doesNotThrowAnyException(); + + then(prereservationSlotService).shouldHaveNoInteractions(); + then(emailSender).should().sendNotificationEmail(eq(email), anyString(), anyString(), anyString(), anyString()); + } } diff --git a/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionServiceTest.java b/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionServiceTest.java index 1e4813c1..aadb9fc6 100644 --- a/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionServiceTest.java +++ b/src/test/java/com/back/b2st/domain/prereservation/entry/service/PrereservationSectionServiceTest.java @@ -23,6 +23,7 @@ import com.back.b2st.domain.prereservation.entry.error.PrereservationErrorCode; import com.back.b2st.domain.prereservation.entry.repository.PrereservationRepository; import com.back.b2st.domain.prereservation.policy.service.PrereservationSlotService; +import com.back.b2st.domain.prereservation.policy.service.PrereservationTimeTableService; import com.back.b2st.domain.venue.section.entity.Section; import com.back.b2st.domain.venue.section.repository.SectionRepository; import com.back.b2st.domain.venue.venue.entity.Venue; @@ -43,6 +44,9 @@ class PrereservationSectionServiceTest { @Mock private PrereservationSlotService prereservationSlotService; + @Mock + private PrereservationTimeTableService prereservationTimeTableService; + @InjectMocks private PrereservationSectionService prereservationSectionService; @@ -166,4 +170,4 @@ private Section createSection(Long id, String name) { given(section.getSectionName()).willReturn(name); return section; } -} \ No newline at end of file +}