Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,6 +14,7 @@ public class PaymentConfirmResponse {

private String orderId;

@JsonProperty("totalAmount")
private Integer amount;

private String approvedAt;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/parkez/payment/service/PaymentWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
5 changes: 5 additions & 0 deletions src/main/java/com/parkez/queue/service/QueueService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,26 @@ List<Reservation> findExpiredReservations(
WHERE r.id = :id
""")
Optional<Reservation> 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<ReservationStatus> statusList
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReservationStatus> statusList) {
return reservationRepository.existsReservationByConditionsForUser(
parkingZone, startDateTime, endDateTime, userId, statusList
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ public ReservationResponse createReservation(AuthUser authUser, ReservationReque
}

List<ReservationStatus> 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) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<Reservation> expiredReservations = reservationWriter.expire(expiredTime);

for (Reservation reservation : expiredReservations) {
handleNextInQueue(reservation);
}
}

public boolean validateRequestTime(ReservationRequest request) {
Expand Down Expand Up @@ -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
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Reservation> expire(LocalDateTime expiredTime) {
List<Reservation> expireToReservation = reservationRepository.findReservationsToExpire(expiredTime);

if (!expireToReservation.isEmpty()) {
expireToReservation.forEach(Reservation::expire);
reservationRepository.saveAll(expireToReservation);
}

return expireToReservation;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public Response<Void> complete(@PathVariable Long settlementId) {
@Secured(UserRole.Authority.OWNER)
@Operation(summary = "확정 정산 내역 조회", description = "지정한 연월 기준으로 정산 정보를 확인합니다.")
public Response<SettlementResponse> getConfirmedSettlement(
@AuthenticatedUser AuthUser authUser,
@Parameter(hidden = true) @AuthenticatedUser AuthUser authUser,
@RequestParam int year,
@RequestParam int month
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReservationStatus> 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<ReservationStatus> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReservationStatus> statusList = List.of(ReservationStatus.PENDING, ReservationStatus.CONFIRMED);

given(distributedLockManager.executeWithLock(anyLong(), any())).willAnswer(invocation -> {
Callable<ReservationResponse> 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -1423,21 +1430,35 @@ class HandleNextInQueueTest {
);

User user = mock(User.class);
ParkingZone parkingZone = mock(ParkingZone.class);

given(reservation.getParkingZoneId()).willReturn(1L);
given(reservation.getStartDateTime()).willReturn(waitingUserDto.getReservationStartDateTime());
given(reservation.getEndDateTime()).willReturn(waitingUserDto.getReservationEndDateTime());

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

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

}


Expand Down