Skip to content

Commit

Permalink
Task 31 : Add StoreReentryTooSoonException with revision codes by add…
Browse files Browse the repository at this point in the history
…ing its tests
  • Loading branch information
Rapter1990 committed Sep 29, 2024
1 parent cd76e9c commit 40c9a08
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
package com.casestudy.migroscouriertracking.common.exception;

import com.casestudy.migroscouriertracking.common.model.CustomError;
import com.casestudy.migroscouriertracking.courier.exception.CourierNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.StoreFarAwayException;
import com.casestudy.migroscouriertracking.courier.exception.StoreNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.TimestampBeforeStoreCreateException;
import com.casestudy.migroscouriertracking.courier.exception.*;
import jakarta.validation.ConstraintViolationException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -181,4 +178,22 @@ protected ResponseEntity<CustomError> handleTimestampBeforeStoreCreate(final Tim
return new ResponseEntity<>(customError, HttpStatus.BAD_REQUEST);
}

/**
* Handles StoreReentryTooSoonException thrown when a courier attempts to reenter the
* circumference of a store within a restricted time frame.
*
* @param ex the StoreReentryTooSoonException thrown
* @return ResponseEntity containing the custom error response with the exception message
*/
@ExceptionHandler(StoreReentryTooSoonException.class)
protected ResponseEntity<CustomError> handleStoreReentryTooSoon(final StoreReentryTooSoonException ex) {
CustomError customError = CustomError.builder()
.httpStatus(HttpStatus.CONFLICT) // Use CONFLICT status for reentry issues
.header(CustomError.Header.API_ERROR.getName())
.message(ex.getMessage())
.build();

return new ResponseEntity<>(customError, HttpStatus.CONFLICT);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.casestudy.migroscouriertracking.courier.exception;

/**
* Exception thrown when a courier attempts to reenter the circumference of a store
* within a restricted time frame.
*/
public class StoreReentryTooSoonException extends RuntimeException {

/**
* Constructs a new StoreReentryTooSoonException with the specified detail message.
*
* @param message the detail message explaining the reason for the exception
*/
public StoreReentryTooSoonException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ public interface CourierRepository extends JpaRepository<CourierEntity, String>

/**
* Finds a list of CourierEntities associated with the specified courier ID, store name,
* and within the provided timestamp range.
* and within the provided timestamp range, ordered by timestamp in descending order.
*
* @param courierId the unique identifier of the courier
* @param storeName the name of the store
* @param start the start timestamp of the range
* @param end the end timestamp of the range
* @return a list of CourierEntities that match the given criteria
* @return a list of CourierEntities that match the given criteria, ordered by timestamp descending
*/
List<CourierEntity> findByCourierIdAndStoreNameAndTimestampBetween(String courierId, String storeName, LocalDateTime start, LocalDateTime end);
List<CourierEntity> findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(String courierId, String storeName, LocalDateTime start, LocalDateTime end);

/**
* Finds a list of CourierEntities associated with the specified courier ID and orders them by timestamp in ascending order.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.casestudy.migroscouriertracking.courier.service;

import com.casestudy.migroscouriertracking.courier.exception.CourierNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.StoreFarAwayException;
import com.casestudy.migroscouriertracking.courier.exception.StoreNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.TimestampBeforeStoreCreateException;
import com.casestudy.migroscouriertracking.courier.exception.*;
import com.casestudy.migroscouriertracking.courier.model.Courier;
import com.casestudy.migroscouriertracking.courier.model.dto.request.LogCourierLocationRequest;
import com.casestudy.migroscouriertracking.courier.model.dto.request.TravelQueryRequest;
Expand Down Expand Up @@ -57,31 +54,32 @@ public void logCourierLocation(LogCourierLocationRequest logRequest) {
Optional.ofNullable(stores)
.orElseThrow(() -> new StoreNotFoundException("No stores found in the database."));

boolean travelEntrySaved = stores.stream().anyMatch(store -> {
if (DistanceUtils.isWithinRadius(lat, lng, store.getLat(), store.getLng(), 100.0)) {
if (timestamp.isBefore(store.getCreatedAt())) {
throw new TimestampBeforeStoreCreateException("Timestamp is before store's creation time.");
}

CourierEntity lastTravel = findLastTravelEntry(courierId, store.getName(), timestamp);
if (lastTravel == null || DistanceUtils.isMoreThanOneMinuteAgo(lastTravel.getTimestamp(), timestamp)) {
CourierEntity courier = CourierEntity.builder()
.courierId(courierId)
.lat(lat)
.lng(lng)
.storeName(store.getName())
.timestamp(timestamp)
.build();
courierRepository.save(courier);
return true;
}
}
return false;
});

if (!travelEntrySaved) {
throw new StoreFarAwayException("Courier is far away from all stores.");
}
stores.stream()
.filter(store -> DistanceUtils.isWithinRadius(lat, lng, store.getLat(), store.getLng(), 100.0))
.findFirst()
.ifPresentOrElse(store -> {
if (timestamp.isBefore(store.getCreatedAt())) {
throw new TimestampBeforeStoreCreateException("Timestamp is before store's creation time.");
}

// Find the last travel entry for the courier at this store
CourierEntity lastTravel = findLastTravelEntry(courierId, store.getName(), timestamp);
if (lastTravel == null || DistanceUtils.isMoreThanOneMinuteAgo(lastTravel.getTimestamp(), timestamp)) {
CourierEntity courier = CourierEntity.builder()
.courierId(courierId)
.lat(lat)
.lng(lng)
.storeName(store.getName())
.timestamp(timestamp)
.build();
courierRepository.save(courier); // Exit after saving the first valid entry
} else {
throw new StoreReentryTooSoonException("Reentry to the same store's circumference is too soon. Please wait before logging again.");
}
}, () -> {
throw new StoreFarAwayException("Courier is far away from all stores.");
});

}

/**
Expand All @@ -94,9 +92,9 @@ public void logCourierLocation(LogCourierLocationRequest logRequest) {
*/
private CourierEntity findLastTravelEntry(String courierId, String storeName, LocalDateTime currentTimestamp) {
LocalDateTime oneMinuteAgo = currentTimestamp.minusMinutes(1);
return courierRepository.findByCourierIdAndStoreNameAndTimestampBetween(courierId, storeName, oneMinuteAgo, currentTimestamp)
return courierRepository.findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(courierId, storeName, oneMinuteAgo, currentTimestamp)
.stream()
.max(Comparator.comparing(CourierEntity::getTimestamp))
.findFirst()
.orElse(null);
}

Expand Down Expand Up @@ -128,7 +126,7 @@ public List<Courier> getTravelsByCourierIdStoreNameAndTimeRange(TravelQueryReque
LocalDateTime start = request.getStart();
LocalDateTime end = request.getEnd();

List<CourierEntity> entities = courierRepository.findByCourierIdAndStoreNameAndTimestampBetween(courierId, storeName, start, end);
List<CourierEntity> entities = courierRepository.findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(courierId, storeName, start, end);
Optional.ofNullable(entities)
.filter(e -> !e.isEmpty())
.orElseThrow(() -> new CourierNotFoundException("No travels found for Courier ID " + courierId + " in store " + storeName + " between " + start + " and " + end + "."));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

import com.casestudy.migroscouriertracking.base.AbstractRestControllerTest;
import com.casestudy.migroscouriertracking.common.model.CustomError;
import com.casestudy.migroscouriertracking.courier.exception.CourierNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.StoreFarAwayException;
import com.casestudy.migroscouriertracking.courier.exception.StoreNotFoundException;
import com.casestudy.migroscouriertracking.courier.exception.TimestampBeforeStoreCreateException;
import com.casestudy.migroscouriertracking.courier.exception.*;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Path;
Expand Down Expand Up @@ -261,7 +258,28 @@ void givenTimestampBeforeStoreCreateException_whenHandleTimestampBeforeStoreCrea
checkCustomError(expectedError, actualError);
}

@Test
@DisplayName("Given StoreReentryTooSoonException - When HandleStoreReentryTooSoon - Then Return RespondWithConflict")
void givenStoreReentryTooSoonException_whenHandleStoreReentryTooSoon_thenReturnRespondWithConflict() {

// Given
StoreReentryTooSoonException ex = new StoreReentryTooSoonException("Reentry to the store is too soon");

CustomError expectedError = CustomError.builder()
.httpStatus(HttpStatus.CONFLICT)
.header(CustomError.Header.API_ERROR.getName())
.message("Reentry to the store is too soon")
.isSuccess(false)
.build();

// When
ResponseEntity<CustomError> responseEntity = globalExceptionHandler.handleStoreReentryTooSoon(ex);

// Then
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
CustomError actualError = responseEntity.getBody();
checkCustomError(expectedError, actualError);
}


private void checkCustomError(CustomError expectedError, CustomError actualError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.casestudy.migroscouriertracking.base.AbstractBaseServiceTest;
import com.casestudy.migroscouriertracking.courier.exception.StoreFarAwayException;
import com.casestudy.migroscouriertracking.courier.exception.StoreReentryTooSoonException;
import com.casestudy.migroscouriertracking.courier.exception.TimestampBeforeStoreCreateException;
import com.casestudy.migroscouriertracking.courier.model.Courier;
import com.casestudy.migroscouriertracking.courier.model.dto.request.LogCourierLocationRequest;
Expand All @@ -23,6 +24,7 @@

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

Expand Down Expand Up @@ -156,6 +158,63 @@ void logCourierLocation_shouldThrowStoreFarAwayException_ifCourierIsFarAwayFromA

}

@Test
void logCourierLocation_shouldThrowStoreReentryTooSoonException_ifReenteringSameStoreTooSoon() {

// Given
String courierId = UUID.randomUUID().toString();
double lat = 37.7749;
double lng = -122.4194;
LocalDateTime now = LocalDateTime.now();
LocalDateTime lastEntryTimestamp = now.minusSeconds(30); // Last entry within 30 seconds

LogCourierLocationRequest logRequest = LogCourierLocationRequest.builder()
.courierId(courierId)
.lat(lat)
.lng(lng)
.timestamp(now)
.build();

StoreEntity store = StoreEntity.builder()
.id(UUID.randomUUID().toString())
.name("store1")
.lat(37.7750)
.lng(-122.4183)
.createdAt(now.minusMinutes(10))
.build();

CourierEntity lastTravelEntry = CourierEntity.builder()
.id(UUID.randomUUID().toString())
.courierId(courierId)
.lat(lat)
.lng(lng)
.storeName(store.getName())
.timestamp(lastEntryTimestamp)
.build();

// When
when(storeRepository.findAll()).thenReturn(List.of(store));
when(courierRepository.findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(
eq(courierId),
eq(store.getName()),
any(LocalDateTime.class),
any(LocalDateTime.class)
)).thenReturn(List.of(lastTravelEntry));

// Then
assertThrows(StoreReentryTooSoonException.class, () -> courierService.logCourierLocation(logRequest));

// Verify
verify(storeRepository).findAll();
verify(courierRepository).findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(
eq(courierId),
eq(store.getName()),
any(LocalDateTime.class),
any(LocalDateTime.class)
);

}

@Test
void getPastTravelsByCourierId_shouldReturnTravelsForGivenCourierId() {

Expand Down Expand Up @@ -212,7 +271,7 @@ void getTravelsByCourierIdStoreNameAndTimeRange_shouldReturnTravelsWithinTimeRan
List<Courier> couriers = courierEntityToCourierMapper.map(courierEntities);

// When
when(courierRepository.findByCourierIdAndStoreNameAndTimestampBetween(request.getCourierId(), request.getStoreName(), request.getStart(), request.getEnd())).thenReturn(courierEntities);
when(courierRepository.findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(request.getCourierId(), request.getStoreName(), request.getStart(), request.getEnd())).thenReturn(courierEntities);

// Then
List<Courier> result = courierService.getTravelsByCourierIdStoreNameAndTimeRange(request);
Expand All @@ -221,7 +280,7 @@ void getTravelsByCourierIdStoreNameAndTimeRange_shouldReturnTravelsWithinTimeRan
assertEquals(couriers.get(0).getCourierId(), result.get(0).getCourierId());

// Verify
verify(courierRepository).findByCourierIdAndStoreNameAndTimestampBetween(request.getCourierId(), request.getStoreName(), request.getStart(), request.getEnd());
verify(courierRepository).findByCourierIdAndStoreNameAndTimestampBetweenOrderByTimestampDesc(request.getCourierId(), request.getStoreName(), request.getStart(), request.getEnd());

}

Expand Down

0 comments on commit 40c9a08

Please sign in to comment.