diff --git a/src/main/java/com/parkez/payment/dto/response/PaymentConfirmResponse.java b/src/main/java/com/parkez/payment/dto/response/PaymentConfirmResponse.java index 68349ef7..24afc6dc 100644 --- a/src/main/java/com/parkez/payment/dto/response/PaymentConfirmResponse.java +++ b/src/main/java/com/parkez/payment/dto/response/PaymentConfirmResponse.java @@ -1,5 +1,6 @@ package com.parkez.payment.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; import com.parkez.payment.domain.enums.PaymentType; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -13,6 +14,7 @@ public class PaymentConfirmResponse { private String orderId; + @JsonProperty("totalAmount") private Integer amount; private String approvedAt; diff --git a/src/main/java/com/parkez/payment/service/PaymentWriter.java b/src/main/java/com/parkez/payment/service/PaymentWriter.java index ec0590d1..46f4597d 100644 --- a/src/main/java/com/parkez/payment/service/PaymentWriter.java +++ b/src/main/java/com/parkez/payment/service/PaymentWriter.java @@ -35,10 +35,12 @@ public Payment createPayment(User user, Reservation reservation, String orderId) public void savePayment(Payment payment, PaymentConfirmResponse response) { payment.approvePaymentInfo(response.getPaymentKey(), response.getApprovedAt(), response.getType()); + paymentRepository.save(payment); } public void cancelPayment(Payment payment){ payment.cancel(LocalDateTime.now()); + paymentRepository.save(payment); } } diff --git a/src/main/java/com/parkez/queue/service/QueueService.java b/src/main/java/com/parkez/queue/service/QueueService.java index ab1ec85f..9ac24dd4 100644 --- a/src/main/java/com/parkez/queue/service/QueueService.java +++ b/src/main/java/com/parkez/queue/service/QueueService.java @@ -57,6 +57,11 @@ public JoinQueueResult joinWaitingQueue(Long userId, ReservationRequest request) public WaitingUserDto dequeueConvertToDto(String key) { Object obj = queueRedisRepository.dequeue(key); log.info("[대기열] dequeue 결과: {}", obj); + + if (obj == null) { + return null; + } + return convertToDto(obj); } diff --git a/src/main/java/com/parkez/reservation/domain/entity/Reservation.java b/src/main/java/com/parkez/reservation/domain/entity/Reservation.java index f09447cf..a9c997e9 100644 --- a/src/main/java/com/parkez/reservation/domain/entity/Reservation.java +++ b/src/main/java/com/parkez/reservation/domain/entity/Reservation.java @@ -129,7 +129,7 @@ public void writeReview() { } public boolean canBeCanceled() { - return this.status == ReservationStatus.PENDING || this.status == ReservationStatus.CONFIRMED; + return this.status == ReservationStatus.CONFIRMED; } public boolean isAfter(LocalDateTime cancelLimitHour, LocalDateTime now) { diff --git a/src/main/java/com/parkez/reservation/domain/repository/ReservationRepository.java b/src/main/java/com/parkez/reservation/domain/repository/ReservationRepository.java index 4ce55cf6..a9592847 100644 --- a/src/main/java/com/parkez/reservation/domain/repository/ReservationRepository.java +++ b/src/main/java/com/parkez/reservation/domain/repository/ReservationRepository.java @@ -103,4 +103,26 @@ List findExpiredReservations( WHERE r.id = :id """) Optional findByIdWithUserAndParkingZone(@Param("id") Long reservationId); + + @Query(""" + SELECT + CASE + WHEN COUNT(r) > 0 THEN true + ELSE false + END + FROM Reservation r + WHERE r.parkingZone = :parkingZone + AND r.user.id = :userId + AND r.status IN :statusList + AND ( + (:start < r.endDateTime AND :end > r.startDateTime) + ) +""") + boolean existsReservationByConditionsForUser( + @Param("parkingZone") ParkingZone parkingZone, + @Param("start") LocalDateTime start, + @Param("end") LocalDateTime end, + @Param("userId") Long userId, + @Param("statusList") List statusList + ); } \ No newline at end of file diff --git a/src/main/java/com/parkez/reservation/exception/ReservationErrorCode.java b/src/main/java/com/parkez/reservation/exception/ReservationErrorCode.java index aafa70d9..5d08d09b 100644 --- a/src/main/java/com/parkez/reservation/exception/ReservationErrorCode.java +++ b/src/main/java/com/parkez/reservation/exception/ReservationErrorCode.java @@ -12,14 +12,14 @@ public enum ReservationErrorCode implements ErrorCode { // BAD_REQUEST NOT_VALID_REQUEST_TIME(HttpStatus.BAD_REQUEST, "RESERVATION_001", "올바르지 않은 예약 시간 요청입니다."), CANT_MODIFY_RESERVATION_STATUS(HttpStatus.BAD_REQUEST, "RESERVATION_002", "사용 중인 예약이 아닙니다."), - CANT_CANCEL_RESERVATION(HttpStatus.BAD_REQUEST, "RESERVATION_003", "결제 대기 중 또는 확인 완료 된 예약만 취소할 수 있습니다."), + CANT_CANCEL_RESERVATION(HttpStatus.BAD_REQUEST, "RESERVATION_003", "결제 완료 된 예약만 취소할 수 있습니다."), CANT_CANCEL_WITHIN_ONE_HOUR(HttpStatus.BAD_REQUEST, "RESERVATION_004", "예약 시작 시간으로부터 1시간 이내일 경우 예약을 취소할 수 없습니다."), RESERVATION_ALREADY_USED(HttpStatus.BAD_REQUEST, "RESERVATION_010", "이미 사용 완료된 예약입니다."), RESERVATION_ALREADY_CANCELED(HttpStatus.BAD_REQUEST, "RESERVATION_011", "이미 취소된 예약입니다."), CANT_RESERVE_UNAVAILABLE_PARKING_ZONE(HttpStatus.BAD_REQUEST, "RESERVATION_012", "예약 가능한 주차 공간이 아닙니다."), CANT_RESERVE_AT_CLOSE_TIME(HttpStatus.BAD_REQUEST, "RESERVATION_013", "영업 시간 외에는 예약할 수 없습니다."), // CONFLICT - ALREADY_RESERVED(HttpStatus.CONFLICT, "RESERVATION_005", "이미 예약이 존재합니다."), + ALREADY_RESERVED_BY_YOURSELF(HttpStatus.CONFLICT, "RESERVATION_005", "본인이 이미 예약한 시간입니다."), // NOT_FOUND NOT_FOUND_RESERVATION(HttpStatus.NOT_FOUND, "RESERVATION_006", "예약이 존재하지 않습니다."), // UNAUTHORIZED diff --git a/src/main/java/com/parkez/reservation/service/ReservationReader.java b/src/main/java/com/parkez/reservation/service/ReservationReader.java index 6780b555..5d397d30 100644 --- a/src/main/java/com/parkez/reservation/service/ReservationReader.java +++ b/src/main/java/com/parkez/reservation/service/ReservationReader.java @@ -93,4 +93,10 @@ public Reservation findReservationByQueueKey(Long parkingZoneId, LocalDateTime s public Reservation findById(Long reservationId) { return reservationRepository.findById(reservationId).orElseThrow(()-> new ParkingEasyException(ReservationErrorCode.NOT_FOUND_RESERVATION)); } + + public boolean existsReservationByConditionsForUser(ParkingZone parkingZone, LocalDateTime startDateTime, LocalDateTime endDateTime, Long userId, List statusList) { + return reservationRepository.existsReservationByConditionsForUser( + parkingZone, startDateTime, endDateTime, userId, statusList + ); + } } diff --git a/src/main/java/com/parkez/reservation/service/ReservationService.java b/src/main/java/com/parkez/reservation/service/ReservationService.java index 6c7afd06..7efa8ae2 100644 --- a/src/main/java/com/parkez/reservation/service/ReservationService.java +++ b/src/main/java/com/parkez/reservation/service/ReservationService.java @@ -81,6 +81,11 @@ public ReservationResponse createReservation(AuthUser authUser, ReservationReque } List statusList = List.of(ReservationStatus.PENDING, ReservationStatus.CONFIRMED); + + if (reservationReader.existsReservationByConditionsForUser(parkingZone, request.getStartDateTime(), request.getEndDateTime(), user.getId(), statusList)) { + throw new ParkingEasyException(ReservationErrorCode.ALREADY_RESERVED_BY_YOURSELF); + } + boolean existed = reservationReader.existsReservationByConditions(parkingZone, request.getStartDateTime(), request.getEndDateTime(), statusList); if (existed) { @@ -190,7 +195,7 @@ public void cancelReservation(AuthUser authUser, Long reservationId, Reservation Reservation reservation = reservationReader.findMyReservation(authUser.getId(), reservationId); - // 결제 대기 중 또는 결제 완료 된 예약만 취소할 수 있음 + // 결제 완료 된 예약만 취소할 수 있음 if (!reservation.canBeCanceled()) { throw new ParkingEasyException(ReservationErrorCode.CANT_CANCEL_RESERVATION); } @@ -217,7 +222,11 @@ public void cancelReservation(AuthUser authUser, Long reservationId, Reservation public void expireReservation() { LocalDateTime expiredTime = LocalDateTime.now().minusMinutes(EXPIRATION_TIME); - reservationWriter.expire(expiredTime); + List expiredReservations = reservationWriter.expire(expiredTime); + + for (Reservation reservation : expiredReservations) { + handleNextInQueue(reservation); + } } public boolean validateRequestTime(ReservationRequest request) { @@ -263,14 +272,18 @@ private void handleNextInQueue(Reservation reservation) { public void createFromQueue(User user, ReservationRequest request) { ParkingZone parkingZone = parkingZoneReader.getActiveByParkingZoneId(request.getParkingZoneId()); + long hours = calculateUsedHour(request.getStartDateTime(), request.getEndDateTime()); + + BigDecimal originalPrice = parkingZone.getParkingLotPricePerHour().multiply(BigDecimal.valueOf(hours)); + reservationWriter.create( user, parkingZone, request.getStartDateTime(), request.getEndDateTime(), - null, - null, - null, + originalPrice, + BigDecimal.ZERO, + originalPrice, null ); diff --git a/src/main/java/com/parkez/reservation/service/ReservationWriter.java b/src/main/java/com/parkez/reservation/service/ReservationWriter.java index 02c4a25b..43cbe47e 100644 --- a/src/main/java/com/parkez/reservation/service/ReservationWriter.java +++ b/src/main/java/com/parkez/reservation/service/ReservationWriter.java @@ -46,23 +46,27 @@ public void complete(Reservation reservation) { public void cancel(Reservation reservation) { reservation.cancel(); + reservationRepository.save(reservation); } public void updateStatusConfirm(Reservation reservation) { reservation.confirm(); + reservationRepository.save(reservation); } public void expirePaymentTimeout(Reservation reservation) { reservation.expire(); } - public void expire(LocalDateTime expiredTime) { + public List expire(LocalDateTime expiredTime) { List expireToReservation = reservationRepository.findReservationsToExpire(expiredTime); if (!expireToReservation.isEmpty()) { expireToReservation.forEach(Reservation::expire); reservationRepository.saveAll(expireToReservation); } + + return expireToReservation; } } diff --git a/src/main/java/com/parkez/settlement/web/SettlementController.java b/src/main/java/com/parkez/settlement/web/SettlementController.java index c6a3bd7c..f7cfd8f4 100644 --- a/src/main/java/com/parkez/settlement/web/SettlementController.java +++ b/src/main/java/com/parkez/settlement/web/SettlementController.java @@ -68,7 +68,7 @@ public Response complete(@PathVariable Long settlementId) { @Secured(UserRole.Authority.OWNER) @Operation(summary = "확정 정산 내역 조회", description = "지정한 연월 기준으로 정산 정보를 확인합니다.") public Response getConfirmedSettlement( - @AuthenticatedUser AuthUser authUser, + @Parameter(hidden = true) @AuthenticatedUser AuthUser authUser, @RequestParam int year, @RequestParam int month ) { diff --git a/src/test/java/com/parkez/reservation/service/ReservationReaderTest.java b/src/test/java/com/parkez/reservation/service/ReservationReaderTest.java index a20c418f..87446265 100644 --- a/src/test/java/com/parkez/reservation/service/ReservationReaderTest.java +++ b/src/test/java/com/parkez/reservation/service/ReservationReaderTest.java @@ -373,4 +373,55 @@ class FindByIdTest { .hasFieldOrPropertyWithValue("errorCode", ReservationErrorCode.NOT_FOUND_RESERVATION); } } + + + @Nested + class ExistsReservationByConditionsForUser { + + @Test + void 예약_존재함() { + // given + ParkingZone parkingZone = mock(ParkingZone.class); + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + Long userId = 1L; + List statusList = List.of(ReservationStatus.PENDING, ReservationStatus.CONFIRMED); + + given(reservationRepository.existsReservationByConditionsForUser( + parkingZone, startDateTime, endDateTime, userId, statusList + )).willReturn(true); + + // when + boolean result = reservationReader.existsReservationByConditionsForUser( + parkingZone, startDateTime, endDateTime, userId, statusList + ); + + // then + assertThat(result).isTrue(); + verify(reservationRepository).existsReservationByConditionsForUser(parkingZone, startDateTime, endDateTime, userId, statusList); + } + + @Test + void 예약_존재하지_않음() { + // given + ParkingZone parkingZone = mock(ParkingZone.class); + LocalDateTime startDateTime = LocalDateTime.now(); + LocalDateTime endDateTime = startDateTime.plusHours(1); + Long userId = 1L; + List statusList = List.of(ReservationStatus.PENDING, ReservationStatus.CONFIRMED); + + given(reservationRepository.existsReservationByConditionsForUser( + parkingZone, startDateTime, endDateTime, userId, statusList + )).willReturn(false); + + // when + boolean result = reservationReader.existsReservationByConditionsForUser( + parkingZone, startDateTime, endDateTime, userId, statusList + ); + + // then + assertThat(result).isFalse(); + verify(reservationRepository).existsReservationByConditionsForUser(parkingZone, startDateTime, endDateTime, userId, statusList); + } + } } \ No newline at end of file diff --git a/src/test/java/com/parkez/reservation/service/ReservationServiceTest.java b/src/test/java/com/parkez/reservation/service/ReservationServiceTest.java index 7b076153..c9153922 100644 --- a/src/test/java/com/parkez/reservation/service/ReservationServiceTest.java +++ b/src/test/java/com/parkez/reservation/service/ReservationServiceTest.java @@ -857,6 +857,42 @@ class CreateReservation { // then assertThat(result).isTrue(); } + + @Test + void 본인이_이미_예약한_경우_예외발생() { + // given + Long ownerId = 1L; + Long userId = 2L; + Long parkingLotId = 1L; + Long parkingZoneId = 1L; + + AuthUser authUser = createAuthUser(userId); + + ReservationRequest request = createRequest(parkingZoneId, null); + + User owner = createOwner(ownerId); + User user = createUser(authUser.getId()); + + ParkingLot parkingLot = createParkingLot(parkingLotId, owner); + ParkingZone parkingZone = createParkingZone(parkingZoneId, parkingLot); + + List statusList = List.of(ReservationStatus.PENDING, ReservationStatus.CONFIRMED); + + given(distributedLockManager.executeWithLock(anyLong(), any())).willAnswer(invocation -> { + Callable task = invocation.getArgument(1); + return task.call(); + }); + given(userReader.getActiveUserById(userId)).willReturn(user); + given(parkingZoneReader.getActiveByParkingZoneId(parkingZoneId)).willReturn(parkingZone); + given(reservationReader.existsReservationByConditionsForUser(parkingZone, request.getStartDateTime(), request.getEndDateTime(), userId, statusList)) + .willReturn(true); + + // when & then + ParkingEasyException exception = assertThrows(ParkingEasyException.class, + () -> reservationService.createReservation(authUser, request, LocalDateTime.now())); + + assertThat(exception.getErrorCode()).isEqualTo(ReservationErrorCode.ALREADY_RESERVED_BY_YOURSELF); + } } @Nested @@ -1163,42 +1199,6 @@ class CancelReservation { verify(promotionIssueWriter).cancelUsage(promotionIssue); } - @Test - void 특정_예약_취소_시_PENDING_상태의_특정_예약_취소_테스트() { - // given - Long ownerId = 1L; - Long userId = 2L; - Long reservationId = 1L; - LocalDateTime startDateTime = LocalDateTime.now().plusHours(3); - LocalDateTime endDateTime = LocalDateTime.now().plusHours(4); - Long parkingLotId = 1L; - Long parkingZoneId = 1L; - - AuthUser authUser = createAuthUser(userId); - User user = createUser(authUser.getId()); - User owner = createOwner(ownerId); - - ParkingLot parkingLot = createParkingLot(parkingLotId, owner); - - ParkingZone parkingZone = createParkingZone(parkingZoneId, parkingLot); - - Reservation reservation = getReservation(parkingZoneId, user, parkingZone); - ReflectionTestUtils.setField(reservation, "status", ReservationStatus.PENDING); - ReflectionTestUtils.setField(reservation, "startDateTime", startDateTime); - ReflectionTestUtils.setField(reservation, "endDateTime", endDateTime); - - given(reservationReader.findMyReservation(anyLong(), any(Long.class))).willReturn(reservation); - doNothing().when(reservationWriter).cancel(reservation); - - // when - ReservationCancelRequest request = new ReservationCancelRequest(); - reservationService.cancelReservation(authUser, reservationId, request, LocalDateTime.now()); - - - // then - verify(reservationWriter, times(1)).cancel(reservation); - } - @Test void 특정_예약_취소_시_CONFIRMED_상태의_특정_예약_취소_테스트() { // given @@ -1303,7 +1303,14 @@ class ExpireReservation { @Test void 예약_생성_후_10분_이내_결제_요청_생성하지_않을_경우_예약_만료() { // given - doNothing().when(reservationWriter).expire(any(LocalDateTime.class)); + Reservation expiredReservation = mock(Reservation.class); + given(reservationWriter.expire(any(LocalDateTime.class))) + .willReturn(List.of(expiredReservation)); // << 여기! List 반환! + + given(expiredReservation.getParkingZoneId()).willReturn(1L); + given(expiredReservation.getStartDateTime()).willReturn(LocalDateTime.now().plusHours(1)); + given(expiredReservation.getEndDateTime()).willReturn(LocalDateTime.now().plusHours(2)); + given(queueService.dequeueConvertToDto(anyString())).willReturn(null); // 대기열 비어있게 // when reservationService.expireReservation(); @@ -1423,6 +1430,7 @@ class HandleNextInQueueTest { ); User user = mock(User.class); + ParkingZone parkingZone = mock(ParkingZone.class); given(reservation.getParkingZoneId()).willReturn(1L); given(reservation.getStartDateTime()).willReturn(waitingUserDto.getReservationStartDateTime()); @@ -1430,6 +1438,8 @@ class HandleNextInQueueTest { given(queueService.dequeueConvertToDto(anyString())).willReturn(waitingUserDto); given(userReader.getActiveUserById(waitingUserDto.getUserId())).willReturn(user); + given(parkingZoneReader.getActiveByParkingZoneId(waitingUserDto.getParkingZoneId())).willReturn(parkingZone); + given(parkingZone.getParkingLotPricePerHour()).willReturn(BigDecimal.valueOf(1000)); // when ReflectionTestUtils.invokeMethod(reservationService, "handleNextInQueue", reservation); @@ -1437,7 +1447,18 @@ class HandleNextInQueueTest { // then verify(userReader).getActiveUserById(waitingUserDto.getUserId()); verify(parkingZoneReader).getActiveByParkingZoneId(waitingUserDto.getParkingZoneId()); + verify(reservationWriter).create( + eq(user), + eq(parkingZone), + eq(waitingUserDto.getReservationStartDateTime()), + eq(waitingUserDto.getReservationEndDateTime()), + any(BigDecimal.class), + eq(BigDecimal.ZERO), + any(BigDecimal.class), + isNull() + ); } + }