diff --git a/core/src/main/java/greencity/config/SecurityConfig.java b/core/src/main/java/greencity/config/SecurityConfig.java index 86ee16c5e4..6e6c7755af 100644 --- a/core/src/main/java/greencity/config/SecurityConfig.java +++ b/core/src/main/java/greencity/config/SecurityConfig.java @@ -332,6 +332,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/place/{placeId}/comments", "/place/propose", "/place/save/favorite/", + "/place/filter/api", USER_CUSTOM_TO_DO_LIST_ITEMS, USER_TO_DO_LIST, "/user/{userId}/habit", diff --git a/core/src/main/java/greencity/controller/PlaceController.java b/core/src/main/java/greencity/controller/PlaceController.java index f350df4411..77c4795c08 100644 --- a/core/src/main/java/greencity/controller/PlaceController.java +++ b/core/src/main/java/greencity/controller/PlaceController.java @@ -5,6 +5,7 @@ import greencity.constant.HttpStatuses; import greencity.dto.PageableDto; import greencity.dto.favoriteplace.FavoritePlaceDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.filter.FilterPlaceDto; import greencity.dto.place.PlaceAddDto; import greencity.dto.place.PlaceInfoDto; @@ -255,8 +256,9 @@ public ResponseEntity> getPlacesByStatus( } /** - * The method which return a list {@code PlaceByBoundsDto} filtered by values + * The method which return a list of {@link PlaceByBoundsDto} filtered by values * contained in the incoming {@link FilterPlaceDto} object. + * {@link PlaceByBoundsDto} are retrieved from the database * * @param filterDto contains all information about the filtering of the list. * @return a list of {@code PlaceByBoundsDto} @@ -278,7 +280,38 @@ public ResponseEntity> getPlacesByStatus( @PostMapping("/filter") public ResponseEntity> getFilteredPlaces( @Valid @RequestBody FilterPlaceDto filterDto, - @CurrentUser UserVO userVO) { + @Parameter(hidden = true) @CurrentUser UserVO userVO) { + return ResponseEntity.ok().body(placeService.getPlacesByFilter(filterDto, userVO)); + } + + /** + * The method which return a list of {@link PlaceByBoundsDto} filtered by values + * contained in the incoming {@link FilterPlacesApiDto} object. + * {@link PlaceByBoundsDto} are retrieved from Google Places API + * + * @param filterDto contains all information about the filtering of the list. + * @return a list of {@code PlaceByBoundsDto} + */ + @Operation(summary = "Return a list places from Google Geocoding API filtered by values contained " + + "in the incoming FilterPlaceDto object") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = HttpStatuses.OK, + content = @Content(schema = @Schema(example = FilterPlacesApiDto.defaultJson))), + @ApiResponse(responseCode = "400", description = HttpStatuses.BAD_REQUEST, + content = @Content(examples = @ExampleObject(HttpStatuses.BAD_REQUEST))), + @ApiResponse(responseCode = "401", description = HttpStatuses.UNAUTHORIZED, + content = @Content(examples = @ExampleObject(HttpStatuses.UNAUTHORIZED))), + @ApiResponse(responseCode = "404", description = HttpStatuses.NOT_FOUND, + content = @Content(examples = @ExampleObject(HttpStatuses.NOT_FOUND))) + }) + @PostMapping("/filter/api") + public ResponseEntity> getFilteredPlacesFromApi( + @Schema( + description = "Filters for places from API", + name = "FilterPlacesApiDto", + type = "object", + example = FilterPlacesApiDto.defaultJson) @RequestBody FilterPlacesApiDto filterDto, + @CurrentUser @Parameter(hidden = true) UserVO userVO) { return ResponseEntity.ok().body(placeService.getPlacesByFilter(filterDto, userVO)); } diff --git a/core/src/main/java/greencity/exception/handler/CustomExceptionHandler.java b/core/src/main/java/greencity/exception/handler/CustomExceptionHandler.java index c71db04a99..bde07b9c9c 100644 --- a/core/src/main/java/greencity/exception/handler/CustomExceptionHandler.java +++ b/core/src/main/java/greencity/exception/handler/CustomExceptionHandler.java @@ -11,6 +11,7 @@ import greencity.exception.exceptions.EventDtoValidationException; import greencity.exception.exceptions.InvalidStatusException; import greencity.exception.exceptions.InvalidURLException; +import greencity.exception.exceptions.LowRoleLevelException; import greencity.exception.exceptions.MultipartXSSProcessingException; import greencity.exception.exceptions.NotCurrentUserException; import greencity.exception.exceptions.NotDeletedException; @@ -20,13 +21,14 @@ import greencity.exception.exceptions.ToDoListItemNotFoundException; import greencity.exception.exceptions.TagNotFoundException; import greencity.exception.exceptions.UnsupportedSortException; +import greencity.exception.exceptions.UserBlockedException; import greencity.exception.exceptions.UserHasNoFriendWithIdException; import greencity.exception.exceptions.UserHasNoPermissionToAccessException; import greencity.exception.exceptions.UserHasNoToDoListItemsException; import greencity.exception.exceptions.UserToDoListItemStatusNotUpdatedException; -import greencity.exception.exceptions.WrongIdException; import greencity.exception.exceptions.ResourceNotFoundException; -import greencity.exception.exceptions.*; +import greencity.exception.exceptions.WrongIdException; +import greencity.exception.exceptions.PlaceAlreadyExistsException; import jakarta.validation.ConstraintDeclarationException; import jakarta.validation.ValidationException; import lombok.AllArgsConstructor; @@ -264,6 +266,14 @@ public final ResponseEntity handleStatusException(InvalidStatusException return ResponseEntity.status(HttpStatus.CONFLICT).body(exceptionResponse); } + @ExceptionHandler(PlaceAlreadyExistsException.class) + public final ResponseEntity handlePlaceAlreadyExistsException(PlaceAlreadyExistsException ex, + WebRequest request) { + log.warn(ex.getMessage()); + ExceptionResponse exceptionResponse = new ExceptionResponse(getErrorAttributes(request)); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exceptionResponse); + } + /** * Method interceptor for exceptions related to unsuccessful operations such as * {@link NotDeletedException}, {@link NotUpdatedException}, diff --git a/core/src/main/resources/messages.properties b/core/src/main/resources/messages.properties index 9dcf2cc108..6e1158dd70 100644 --- a/core/src/main/resources/messages.properties +++ b/core/src/main/resources/messages.properties @@ -25,7 +25,7 @@ greenCity.validation.min.lng=Has to be greater or equals -180 greenCity.validation.max.lng=Has to be lower or equals 180 greenCity.validation.min.rate=The rate must be at least {value} -greenCity.validation.max.rate=The rate must bigger than {min} and less then {max} +greenCity.validation.max.rate=The rate must be greater than {min} and less than {max} greenCity.validation.habit.complexity=The habit's complexity must be between 1 and 3 greenCity.validation.invalid.discount.value=Min discount value is {min}, max discount value is {max} diff --git a/core/src/test/java/greencity/ModelUtils.java b/core/src/test/java/greencity/ModelUtils.java index 35c7ff65c8..c883f6db26 100644 --- a/core/src/test/java/greencity/ModelUtils.java +++ b/core/src/test/java/greencity/ModelUtils.java @@ -1,5 +1,8 @@ package greencity; +import com.google.maps.model.LatLng; +import com.google.maps.model.PriceLevel; +import com.google.maps.model.RankBy; import greencity.dto.PageableAdvancedDto; import greencity.dto.PageableDetailedDto; import greencity.dto.PageableDto; @@ -24,6 +27,7 @@ import greencity.dto.filter.FilterDistanceDto; import greencity.dto.filter.FilterEventDto; import greencity.dto.filter.FilterPlaceDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.friends.UserAsFriendDto; import greencity.dto.habit.CustomHabitDtoRequest; import greencity.dto.habit.HabitAssignCustomPropertiesDto; @@ -32,7 +36,9 @@ import greencity.dto.habittranslation.HabitTranslationDto; import greencity.dto.language.LanguageDTO; import greencity.dto.language.LanguageTranslationDTO; +import greencity.dto.location.LocationDto; import greencity.dto.location.MapBoundsDto; +import greencity.dto.place.PlaceByBoundsDto; import greencity.dto.todolistitem.CustomToDoListItemResponseDto; import greencity.dto.todolistitem.ToDoListItemPostDto; import greencity.dto.todolistitem.ToDoListItemRequestDto; @@ -562,4 +568,24 @@ public static Map getUserRoleBody() { body.put("role", ROLE_ADMIN); return body; } + + public static FilterPlacesApiDto getFilterPlacesApiDto() { + return FilterPlacesApiDto.builder() + .location(new LatLng(0d, 0d)) + .radius(10000) + .keyword("test") + .rankBy(RankBy.PROMINENCE) + .openNow(true) + .minPrice(PriceLevel.FREE) + .maxPrice(PriceLevel.VERY_EXPENSIVE) + .build(); + } + + public static List getPlaceByBoundsDto() { + return List.of(PlaceByBoundsDto.builder() + .id(1L) + .name("testx") + .location(new LocationDto()) + .build()); + } } \ No newline at end of file diff --git a/core/src/test/java/greencity/controller/PlaceControllerTest.java b/core/src/test/java/greencity/controller/PlaceControllerTest.java index 33a17533cd..753af65dd0 100644 --- a/core/src/test/java/greencity/controller/PlaceControllerTest.java +++ b/core/src/test/java/greencity/controller/PlaceControllerTest.java @@ -1,6 +1,7 @@ package greencity.controller; import greencity.converters.UserArgumentResolver; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.place.PlaceAddDto; import greencity.dto.place.PlaceVO; import greencity.dto.place.AddPlaceDto; @@ -52,6 +53,8 @@ import greencity.service.PlaceService; import static greencity.ModelUtils.getFilterPlaceDto; import static greencity.ModelUtils.getUserVO; +import static greencity.ModelUtils.getFilterPlacesApiDto; +import static greencity.ModelUtils.getPlaceByBoundsDto; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.times; @@ -342,6 +345,36 @@ void getFilteredPlaces() throws Exception { verify(placeService).getPlacesByFilter(filterPlaceDto, userVO); } + @Test + void getFilteredPlacesFromApi() throws Exception { + UserVO userVO = getUserVO(); + FilterPlacesApiDto filterDto = getFilterPlacesApiDto(); + String json = """ + { + "location": { + "lat": 0, + "lng": 0 + }, + "radius": 10000, + "rankBy": "PROMINENCE", + "keyword": "test", + "minPrice": "0", + "maxPrice": "4", + "openNow": true + } + """; + when(userService.findByEmail(anyString())).thenReturn(userVO); + when(placeService.getPlacesByFilter(filterDto, userVO)).thenReturn(getPlaceByBoundsDto()); + + this.mockMvc.perform(post(placeLink + "/filter/api") + .content(json) + .principal(principal) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + verify(placeService).getPlacesByFilter(filterDto, userVO); + } + @Test void filterPlaceBySearchPredicate() throws Exception { int pageNumber = 5; diff --git a/dao/src/main/java/greencity/repository/LocationRepo.java b/dao/src/main/java/greencity/repository/LocationRepo.java index 63d8af814f..273f24a543 100644 --- a/dao/src/main/java/greencity/repository/LocationRepo.java +++ b/dao/src/main/java/greencity/repository/LocationRepo.java @@ -3,6 +3,8 @@ import greencity.entity.Location; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; /** @@ -19,4 +21,25 @@ public interface LocationRepo extends JpaRepository { * @author Kateryna Horokh. */ Optional findByLatAndLng(Double lat, Double lng); + + /** + * Method checks if {@code Location} with such {@code lat} and {@code lng} + * exist. Only first 4 decimal places of {@code lat} and {@code lng} are taken + * into account + * + * @param lat latitude of point of the map + * @param lng longitude of point of the map + * @return {@code true} if {@code Location} with such coordinates exist, or else + * - {@code false} + * @author Hrenevych Ivan. + */ + @Query(value = """ + SELECT EXISTS ( + SELECT 1 + FROM locations l + WHERE ROUND(CAST(l.lat AS numeric), 4) = ROUND(CAST(:lat AS numeric), 4) + AND ROUND(CAST(l.lng AS numeric), 4) = ROUND(CAST(:lng AS numeric), 4) + ) + """, nativeQuery = true) + boolean existsByLatAndLng(@Param("lat") double lat, @Param("lng") double lng); } diff --git a/service-api/pom.xml b/service-api/pom.xml index 8025f8b4a0..58f583dfa3 100644 --- a/service-api/pom.xml +++ b/service-api/pom.xml @@ -108,5 +108,11 @@ 1.18.1 compile + + com.google.maps + google-maps-services + 2.2.0 + compile + diff --git a/service-api/src/main/java/greencity/constant/ErrorMessage.java b/service-api/src/main/java/greencity/constant/ErrorMessage.java index 9da07fd63f..00c4cdc8d4 100644 --- a/service-api/src/main/java/greencity/constant/ErrorMessage.java +++ b/service-api/src/main/java/greencity/constant/ErrorMessage.java @@ -51,6 +51,7 @@ public class ErrorMessage { "Subscriber with this email address and subscription type is exists."; public static final String UBSCRIPTION_BY_TOKEN_NOT_FOUND = "Subscriber with this token not found."; public static final String LOCATION_NOT_FOUND_BY_ID = "The location does not exist by this id: "; + public static final String LOCATION_NOT_FOUND = "Location must be provided either in filterDto or userVO"; public static final String HABIT_HAS_BEEN_ALREADY_ENROLLED = "You can enroll habit only once a day"; public static final String HABIT_ALREADY_ACQUIRED = "You have already acquired habit with id: "; public static final String HABIT_IS_NOT_ENROLLED_ON_CURRENT_DATE = "Habit is not enrolled on "; @@ -127,6 +128,7 @@ public class ErrorMessage { public static final String COMMENT_PROPERTY_TYPE_NOT_FOUND = "For type comment not found this property :"; public static final String CANNOT_REPLY_THE_REPLY = "You can't reply on reply"; public static final String NOT_A_CURRENT_USER = "You can't perform actions with the data of other user"; + public static final String PLACE_ALREADY_EXISTS = "Place with lat: %.4f and lng: %.4f already exists"; public static final String FAVORITE_PLACE_ALREADY_EXISTS = "Favorite place already exist for this placeId: %d and user with email: %s"; public static final String FAVORITE_PLACE_NOT_FOUND = "The favorite place does not exist "; @@ -227,6 +229,7 @@ public class ErrorMessage { public static final String GIT_REPOSITORY_NOT_INITIALIZED = "Git repository not initialized. Commit info is unavailable."; public static final String FAILED_TO_FETCH_COMMIT_INFO = "Failed to fetch commit info due to I/O error: "; + public static final String GEOCODING_RESULT_IS_EMPTY = "No geocoding results found for given location"; public static final String MAX_PAGE_SIZE_EXCEPTION = "Page size must be less than or equal to 100"; public static final String INVALID_VALUE_EXCEPTION = "Invalid value for %s: must be an integer"; public static final String NEGATIVE_VALUE_EXCEPTION = "%s must be a positive number"; diff --git a/service-api/src/main/java/greencity/dto/filter/FilterPlacesApiDto.java b/service-api/src/main/java/greencity/dto/filter/FilterPlacesApiDto.java new file mode 100644 index 0000000000..4b5b76be33 --- /dev/null +++ b/service-api/src/main/java/greencity/dto/filter/FilterPlacesApiDto.java @@ -0,0 +1,40 @@ +package greencity.dto.filter; + +import com.google.maps.model.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class FilterPlacesApiDto { + private LatLng location; + private int radius; + private RankBy rankBy; + private String keyword; + private PriceLevel minPrice; + private PriceLevel maxPrice; + private String name; + private boolean openNow; + private PlaceType type; + + public static final String defaultJson = """ + { + "location": { + "lat": 50.4500, + "lng": 30.5234 + }, + "radius": 0, + "rankBy": "PROMINENCE", + "keyword": "string", + "minPrice": "0", + "maxPrice": "4", + "name": "string", + "openNow": true, + "type": "restaurant" + } + """; +} diff --git a/service-api/src/main/java/greencity/exception/exceptions/PlaceAlreadyExistsException.java b/service-api/src/main/java/greencity/exception/exceptions/PlaceAlreadyExistsException.java new file mode 100644 index 0000000000..d731dbb95f --- /dev/null +++ b/service-api/src/main/java/greencity/exception/exceptions/PlaceAlreadyExistsException.java @@ -0,0 +1,7 @@ +package greencity.exception.exceptions; + +import lombok.experimental.StandardException; + +@StandardException +public class PlaceAlreadyExistsException extends RuntimeException { +} diff --git a/service-api/src/main/java/greencity/service/LocationService.java b/service-api/src/main/java/greencity/service/LocationService.java index c3eab5c94f..3d71fcc6a4 100644 --- a/service-api/src/main/java/greencity/service/LocationService.java +++ b/service-api/src/main/java/greencity/service/LocationService.java @@ -57,4 +57,17 @@ public interface LocationService { * @author Kateryna Horokh. */ Optional findByLatAndLng(Double lat, Double lng); + + /** + * Method checks if {@code Location} with such {@code lat} and {@code lng} + * exist. Only first 4 decimal places of {@code lat} and {@code lng} are taken + * into account + * + * @param lat latitude of point of the map + * @param lng longitude of point of the map + * @return {@code true} if {@code Location} with such coordinates exist, or else + * - {@code false} + * @author Hrenevych Ivan. + */ + boolean existsByLatAndLng(Double lat, Double lng); } diff --git a/service-api/src/main/java/greencity/service/PlaceService.java b/service-api/src/main/java/greencity/service/PlaceService.java index 15cf89a6d7..eee7abc232 100644 --- a/service-api/src/main/java/greencity/service/PlaceService.java +++ b/service-api/src/main/java/greencity/service/PlaceService.java @@ -1,6 +1,8 @@ package greencity.service; +import com.google.maps.model.GeocodingResult; import greencity.dto.PageableDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.filter.FilterPlaceDto; import greencity.dto.place.AddPlaceDto; import greencity.dto.place.AdminPlaceDto; @@ -193,7 +195,20 @@ public interface PlaceService { * @return a list of {@link PlaceByBoundsDto} * @author Roman Zahouri */ - List getPlacesByFilter(FilterPlaceDto filterDto, UserVO userVO); + List getPlacesByFilter(FilterPlaceDto filterDto, + UserVO userVO); + + /** + * The method finds all {@link GeocodingResult}'s from {@link GoogleApiService} + * filtered by the parameters contained in {@param filterDto} object. + * + * @param filterDto contains objects whose values determine the filter + * parameters of the returned list. + * @return a list of {@link PlaceByBoundsDto} + * @author Hrenevych Ivan + */ + List getPlacesByFilter(FilterPlacesApiDto filterDto, + UserVO userVO); /** * The method finds all {@link PlaceVO}'s filtered by the parameters contained @@ -250,7 +265,7 @@ public interface PlaceService { List getAllPlaceCategories(); /** - * Method for create new place From UI. + * Method to create new place From UI. */ PlaceResponse addPlaceFromUi(AddPlaceDto dto, String email, MultipartFile[] images); @@ -272,4 +287,4 @@ public interface PlaceService { * @throws NotFoundException If the place or user is not found. */ UpdatePlaceStatusWithUserEmailDto updatePlaceStatus(UpdatePlaceStatusWithUserEmailDto dto); -} \ No newline at end of file +} diff --git a/service/src/main/java/greencity/service/GoogleApiService.java b/service/src/main/java/greencity/service/GoogleApiService.java index 21b743131b..e7886f1577 100644 --- a/service/src/main/java/greencity/service/GoogleApiService.java +++ b/service/src/main/java/greencity/service/GoogleApiService.java @@ -2,15 +2,22 @@ import com.google.maps.GeoApiContext; import com.google.maps.GeocodingApi; +import com.google.maps.NearbySearchRequest; +import com.google.maps.PlacesApi; import com.google.maps.errors.ApiException; -import com.google.maps.model.AddressComponent; -import com.google.maps.model.AddressComponentType; import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; +import com.google.maps.model.AddressComponent; +import com.google.maps.model.AddressComponentType; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.model.PlacesSearchResponse; import greencity.constant.ErrorMessage; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.geocoding.AddressResponse; import greencity.dto.geocoding.AddressLatLngResponse; +import greencity.dto.user.UserVO; import greencity.exception.exceptions.BadRequestException; +import greencity.exception.exceptions.NotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -41,7 +48,9 @@ public List getResultFromGeoCode(String searchRequest) { LOCALES.forEach(locale -> { try { GeocodingResult[] results = GeocodingApi.newRequest(context) - .address(searchRequest).language(locale.getLanguage()).await(); + .address(searchRequest) + .language(locale.getLanguage()) + .await(); Collections.addAll(geocodingResults, results); } catch (IOException | InterruptedException | ApiException e) { log.error("Occurred error during the call on google API, reason: {}", e.getMessage()); @@ -51,6 +60,96 @@ public List getResultFromGeoCode(String searchRequest) { return geocodingResults; } + /** + * Sends a request to the Google Places API using the given FilterPlacesApiDto. + * + * @param filterDto DTO containing filter parameters for the Places API request. + * @param userVO contains location of the user. location from userVO is used + * if location in filterDto is null + * @return List of PlacesSearchResult containing the search results. + * + * @author Hrenevych Ivan + */ + public List getResultFromPlacesApi(FilterPlacesApiDto filterDto, UserVO userVO) { + List placesResults = new ArrayList<>(); + LOCALES.forEach(locale -> { + try { + LatLng location = + filterDto.getLocation() != null ? filterDto.getLocation() : getLocationFromUserVO(userVO); + NearbySearchRequest request = PlacesApi.nearbySearchQuery( + context, + location) + .radius(filterDto.getRadius()) + .language(locale.getLanguage()); + + applyFiltersToRequest(filterDto, request); + + PlacesSearchResponse response = request.await(); + Collections.addAll(placesResults, response.results); + } catch (IOException | InterruptedException | ApiException e) { + log.error("Error during Google Places API call, reason: {}", e.getMessage()); + Thread.currentThread().interrupt(); + } + }); + + return placesResults; + } + + /** + * Applies filters to a request from {@link FilterPlacesApiDto}. + * + * @param filterDto DTO containing filter parameters for the Places API request. + * @param request API request filters are applied to + * + * @author Hrenevych Ivan + */ + private static void applyFiltersToRequest(FilterPlacesApiDto filterDto, NearbySearchRequest request) { + if (filterDto.getKeyword() != null) { + request.keyword(filterDto.getKeyword()); + } + + if (filterDto.getType() != null) { + request.type(filterDto.getType()); + } + + if (filterDto.getRankBy() != null) { + request.rankby(filterDto.getRankBy()); + } + + if (filterDto.getMinPrice() != null) { + request.minPrice(filterDto.getMinPrice()); + } + + if (filterDto.getMaxPrice() != null) { + request.maxPrice(filterDto.getMaxPrice()); + } + + if (filterDto.isOpenNow()) { + request.openNow(true); + } + + if (filterDto.getName() != null) { + request.name(filterDto.getName()); + } + } + + /** + * Parses a location string (latitude,longitude) into a LatLng object. + * + * @param location - A string in the format "latitude,longitude". + * @return LatLng object representing the location. + */ + private com.google.maps.model.LatLng getLocationFromUserVO(UserVO userVO) { + if (userVO == null + || userVO.getUserLocationDto() == null + || userVO.getUserLocationDto().getLatitude() == null + || userVO.getUserLocationDto().getLongitude() == null) { + throw new NotFoundException(ErrorMessage.LOCATION_NOT_FOUND); + } + return new com.google.maps.model.LatLng(userVO.getUserLocationDto().getLatitude(), + userVO.getUserLocationDto().getLongitude()); + } + /** * Send request to the Google and receive response with geocoding. * diff --git a/service/src/main/java/greencity/service/LocationServiceImpl.java b/service/src/main/java/greencity/service/LocationServiceImpl.java index 8113537b97..465bb18c6a 100644 --- a/service/src/main/java/greencity/service/LocationServiceImpl.java +++ b/service/src/main/java/greencity/service/LocationServiceImpl.java @@ -116,4 +116,14 @@ public Optional findByLatAndLng(Double lat, Double lng) { } return Optional.empty(); } + + /** + * {@inheritDoc} + * + * @author Hrenevych Ivan + */ + @Override + public boolean existsByLatAndLng(Double lat, Double lng) { + return locationRepo.existsByLatAndLng(lat, lng); + } } diff --git a/service/src/main/java/greencity/service/PlaceServiceImpl.java b/service/src/main/java/greencity/service/PlaceServiceImpl.java index 023c451e4e..f3c30eb22a 100644 --- a/service/src/main/java/greencity/service/PlaceServiceImpl.java +++ b/service/src/main/java/greencity/service/PlaceServiceImpl.java @@ -1,6 +1,7 @@ package greencity.service; import com.google.maps.model.GeocodingResult; +import com.google.maps.model.PlacesSearchResult; import greencity.client.RestClient; import greencity.constant.ErrorMessage; import greencity.constant.LogMessage; @@ -8,8 +9,10 @@ import greencity.dto.discount.DiscountValueDto; import greencity.dto.discount.DiscountValueVO; import greencity.dto.filter.FilterDistanceDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.filter.FilterPlaceDto; import greencity.dto.location.AddPlaceLocation; +import greencity.dto.location.LocationDto; import greencity.dto.location.LocationAddressAndGeoForUpdateDto; import greencity.dto.location.LocationVO; import greencity.dto.openhours.OpenHoursDto; @@ -44,9 +47,7 @@ import greencity.enums.PlaceStatus; import greencity.enums.Role; import greencity.enums.UserStatus; -import greencity.exception.exceptions.NotFoundException; -import greencity.exception.exceptions.PlaceStatusException; -import greencity.exception.exceptions.UserBlockedException; +import greencity.exception.exceptions.*; import greencity.repository.CategoryRepo; import greencity.repository.FavoritePlaceRepo; import greencity.repository.PhotoRepo; @@ -432,7 +433,22 @@ public List getPlacesByFilter(FilterPlaceDto filterDto, UserVO } /** - * Method that filtering places by distance. + * {@inheritDoc} + */ + @Override + public List getPlacesByFilter(FilterPlacesApiDto filterDto, UserVO userVO) { + List fromPlacesApi = googleApiService.getResultFromPlacesApi(filterDto, userVO); + return fromPlacesApi.stream() + .map(el -> new PlaceByBoundsDto( + System.currentTimeMillis(), + el.name, + new LocationDto(System.currentTimeMillis(), el.geometry.location.lat, el.geometry.location.lng, + el.vicinity))) + .toList(); + } + + /** + * Method that filters places by distance. * * @param filterDto - {@link FilterPlaceDto} DTO. * @param placeList - {@link List} of {@link Place} that will be filtered. @@ -530,18 +546,27 @@ public List getAllPlaceCategories() { }.getType()); } + /** + * {@inheritDoc} + */ @Override public PlaceResponse addPlaceFromUi(AddPlaceDto dto, String email, MultipartFile[] images) { - PlaceResponse placeResponse = modelMapper.map(dto, PlaceResponse.class); User user = userRepo.findByEmail(email) .orElseThrow(() -> new NotFoundException(ErrorMessage.USER_NOT_FOUND_BY_EMAIL + email)); if (user.getUserStatus().equals(UserStatus.BLOCKED)) { throw new UserBlockedException(ErrorMessage.USER_HAS_BLOCKED_STATUS); } - - AddPlaceLocation geoDetails = getLocationDetailsFromGeocode(dto.getLocationName()); - placeResponse.setLocationAddressAndGeoDto(geoDetails); - + PlaceResponse placeResponse = modelMapper.map(dto, PlaceResponse.class); + List geocodingResults = googleApiService.getResultFromGeoCode(dto.getLocationName()); + if (geocodingResults.isEmpty()) { + throw new NotFoundException(ErrorMessage.GEOCODING_RESULT_IS_EMPTY); + } + double lat = geocodingResults.getFirst().geometry.location.lat; + double lng = geocodingResults.getFirst().geometry.location.lng; + if (locationService.existsByLatAndLng(lat, lng)) { + throw new PlaceAlreadyExistsException(ErrorMessage.PLACE_ALREADY_EXISTS.formatted(lat, lng)); + } + placeResponse.setLocationAddressAndGeoDto(initializeGeoCodingResults(geocodingResults)); Place place = modelMapper.map(placeResponse, Place.class); place.setCategory(categoryRepo.findCategoryByName(dto.getCategoryName())); place.setAuthor(user); diff --git a/service/src/test/java/greencity/ModelUtils.java b/service/src/test/java/greencity/ModelUtils.java index 3bd31c9499..053af00f65 100644 --- a/service/src/test/java/greencity/ModelUtils.java +++ b/service/src/test/java/greencity/ModelUtils.java @@ -1,10 +1,14 @@ package greencity; -import com.google.maps.model.AddressComponent; -import com.google.maps.model.AddressComponentType; import com.google.maps.model.GeocodingResult; import com.google.maps.model.Geometry; import com.google.maps.model.LatLng; +import com.google.maps.model.AddressComponent; +import com.google.maps.model.AddressComponentType; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.model.PlacesSearchResponse; +import com.google.maps.model.RankBy; +import com.google.maps.model.PriceLevel; import greencity.constant.AppConstant; import greencity.dto.PageableAdvancedDto; import greencity.dto.achievement.AchievementManagementDto; @@ -54,6 +58,7 @@ import greencity.dto.favoriteplace.FavoritePlaceDto; import greencity.dto.favoriteplace.FavoritePlaceVO; import greencity.dto.filter.FilterEventDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.friends.UserAsFriendDto; import greencity.dto.friends.UserFriendDto; import greencity.dto.geocoding.AddressLatLngResponse; @@ -540,6 +545,11 @@ public static UserVO getUserVO() { .verifyEmail(new VerifyEmailVO()) .dateOfRegistration(localDateTime) .languageVO(getLanguageVO()) + .userLocationDto( + UserLocationDto.builder() + .latitude(1d) + .longitude(1d) + .build()) .build(); } @@ -3327,6 +3337,77 @@ public static List getUserFriendInviteHabitDtoTuple2() { return List.of(tuple1, tuple2); } + public static FilterPlacesApiDto getFilterPlacesApiDto() { + return FilterPlacesApiDto.builder() + .location(new LatLng(0d, 0d)) + .radius(10000) + .keyword("test") + .rankBy(RankBy.PROMINENCE) + .openNow(true) + .minPrice(PriceLevel.FREE) + .maxPrice(PriceLevel.VERY_EXPENSIVE) + .build(); + } + + public static List getPlaceByBoundsDto() { + return List.of(PlaceByBoundsDto.builder() + .id(1L) + .name("testx") + .location(new LocationDto()) + .build()); + } + + public static List getPlaceByBoundsDtoFromApi() { + return List.of(PlaceByBoundsDto.builder() + .id(1L) + .name("testName") + .location(new LocationDto(1L, 1d, 1d, "testVicinity")) + .build()); + } + + public static List getPlacesSearchResultUk() { + PlacesSearchResult placesSearchResult = new PlacesSearchResult(); + placesSearchResult.name = "тестІмя"; + placesSearchResult.vicinity = "тестАдреса"; + placesSearchResult.geometry = new Geometry(); + placesSearchResult.geometry.location = new LatLng(1d, 1d); + return List.of(placesSearchResult); + } + + public static List getPlacesSearchResultEn() { + PlacesSearchResult placesSearchResult = new PlacesSearchResult(); + placesSearchResult.name = "testName"; + placesSearchResult.vicinity = "testVicinity"; + placesSearchResult.geometry = new Geometry(); + placesSearchResult.geometry.location = new LatLng(1d, 1d); + return List.of(placesSearchResult); + } + + public static PlacesSearchResponse getPlacesSearchResponse() { + PlacesSearchResponse placesSearchResponse = new PlacesSearchResponse(); + List results = new ArrayList<>(); + Collections.addAll(results, getPlacesSearchResultEn().toArray(new PlacesSearchResult[0])); + Collections.addAll(results, getPlacesSearchResultUk().toArray(new PlacesSearchResult[0])); + placesSearchResponse.results = results.toArray(new PlacesSearchResult[0]); + return placesSearchResponse; + } + + public static PlacesSearchResponse getPlacesSearchResponseEn() { + PlacesSearchResponse placesSearchResponse = new PlacesSearchResponse(); + List results = new ArrayList<>(); + Collections.addAll(results, getPlacesSearchResultEn().toArray(new PlacesSearchResult[0])); + placesSearchResponse.results = results.toArray(new PlacesSearchResult[0]); + return placesSearchResponse; + } + + public static PlacesSearchResponse getPlacesSearchResponseUk() { + PlacesSearchResponse placesSearchResponse = new PlacesSearchResponse(); + List results = new ArrayList<>(); + Collections.addAll(results, getPlacesSearchResultUk().toArray(new PlacesSearchResult[0])); + placesSearchResponse.results = results.toArray(new PlacesSearchResult[0]); + return placesSearchResponse; + } + public static PlaceUpdateDto getPlaceUpdateDto() { return PlaceUpdateDto.builder() .id(1L) diff --git a/service/src/test/java/greencity/service/GoogleApiServiceTest.java b/service/src/test/java/greencity/service/GoogleApiServiceTest.java index 98ab9f1ab7..652bba68d0 100644 --- a/service/src/test/java/greencity/service/GoogleApiServiceTest.java +++ b/service/src/test/java/greencity/service/GoogleApiServiceTest.java @@ -3,12 +3,18 @@ import com.google.maps.GeoApiContext; import com.google.maps.GeocodingApi; import com.google.maps.GeocodingApiRequest; -import com.google.maps.errors.ApiException; +import com.google.maps.PlacesApi; +import com.google.maps.NearbySearchRequest; import com.google.maps.model.GeocodingResult; import com.google.maps.model.LatLng; +import com.google.maps.model.PlacesSearchResult; +import com.google.maps.errors.ApiException; import greencity.ModelUtils; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.geocoding.AddressLatLngResponse; +import greencity.dto.user.UserVO; import greencity.exception.exceptions.BadRequestException; +import greencity.exception.exceptions.NotFoundException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -17,8 +23,10 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import java.io.IOException; +import java.util.List; import java.util.Locale; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; @@ -107,4 +115,198 @@ void getResultFromGeoCodeByCoordinatesWithNullResultsTest() verify(request).await(); } } + + @Test + void getResultsFromGeocodeTest() throws IOException, InterruptedException, ApiException { + Locale localeUk = Locale.forLanguageTag("uk"); + Locale localeEn = Locale.forLanguageTag("en"); + try (MockedStatic geocodingApiMockedStatic = mockStatic(GeocodingApi.class)) { + + GeocodingApiRequest requestUk = mock(GeocodingApiRequest.class); + GeocodingApiRequest requestEn = mock(GeocodingApiRequest.class); + + String searchRequest = "testSearchRequest"; + + when(GeocodingApi.newRequest(context)).thenReturn(requestUk, requestEn); + + when(requestUk.address(searchRequest)).thenReturn(requestUk); + when(requestUk.language(localeUk.getLanguage())).thenReturn(requestUk); + when(requestUk.await()).thenReturn(ModelUtils.getGeocodingResultUk()); + + when(requestEn.address(searchRequest)).thenReturn(requestEn); + when(requestEn.language(localeEn.getLanguage())).thenReturn(requestEn); + when(requestEn.await()).thenReturn(ModelUtils.getGeocodingResultEn()); + + List actual = googleApiService.getResultFromGeoCode(searchRequest); + + assertEquals(ModelUtils.getGeocodingResultUk().length + ModelUtils.getGeocodingResultEn().length, + actual.size()); + verify(requestUk, times(1)).await(); + verify(requestEn, times(1)).await(); + } + } + + @Test + void getResultsFromGeocodeThrowsTest() throws IOException, InterruptedException, ApiException { + String searchRequest = "testSearchRequest"; + + Locale localeUk = Locale.forLanguageTag("uk"); + Locale localeEn = Locale.forLanguageTag("en"); + try (MockedStatic geocodingApiMockedStatic = mockStatic(GeocodingApi.class)) { + + GeocodingApiRequest requestUk = mock(GeocodingApiRequest.class); + GeocodingApiRequest requestEn = mock(GeocodingApiRequest.class); + + when(GeocodingApi.newRequest(context)).thenReturn(requestUk, requestEn); + + when(requestUk.address(searchRequest)).thenReturn(requestUk); + when(requestUk.language(localeUk.getLanguage())).thenReturn(requestUk); + when(requestUk.await()).thenThrow(ApiException.class); + + when(requestEn.address(searchRequest)).thenReturn(requestEn); + when(requestEn.language(localeEn.getLanguage())).thenReturn(requestEn); + when(requestEn.await()).thenThrow(ApiException.class); + + assertDoesNotThrow(() -> googleApiService.getResultFromGeoCode(searchRequest)); + + verify(requestUk, times(1)).await(); + verify(requestEn, times(1)).await(); + } + } + + @Test + void getResultFromPlacesApiTest() throws IOException, InterruptedException, ApiException { + FilterPlacesApiDto filterDto = ModelUtils.getFilterPlacesApiDto(); + UserVO userVO = ModelUtils.getUserVO(); + + Locale localeUk = Locale.forLanguageTag("uk"); + Locale localeEn = Locale.forLanguageTag("en"); + try (MockedStatic placesApiMockedStatic = mockStatic(PlacesApi.class)) { + NearbySearchRequest requestUk = mock(NearbySearchRequest.class); + NearbySearchRequest requestEn = mock(NearbySearchRequest.class); + + when(PlacesApi.nearbySearchQuery(context, filterDto.getLocation())).thenReturn(requestUk, requestEn); + + when(requestUk.radius(filterDto.getRadius())).thenReturn(requestUk); + when(requestUk.language(localeUk.getLanguage())).thenReturn(requestUk); + when(requestUk.keyword(filterDto.getKeyword())).thenReturn(requestUk); + when(requestUk.type(filterDto.getType())).thenReturn(requestUk); + when(requestUk.rankby(filterDto.getRankBy())).thenReturn(requestUk); + when(requestUk.minPrice(filterDto.getMinPrice())).thenReturn(requestUk); + when(requestUk.maxPrice(filterDto.getMaxPrice())).thenReturn(requestUk); + when(requestUk.openNow(filterDto.isOpenNow())).thenReturn(requestUk); + when(requestUk.name(filterDto.getName())).thenReturn(requestUk); + + when(requestEn.radius(filterDto.getRadius())).thenReturn(requestEn); + when(requestEn.language(localeEn.getLanguage())).thenReturn(requestEn); + when(requestEn.keyword(filterDto.getKeyword())).thenReturn(requestEn); + when(requestEn.type(filterDto.getType())).thenReturn(requestEn); + when(requestEn.rankby(filterDto.getRankBy())).thenReturn(requestEn); + when(requestEn.minPrice(filterDto.getMinPrice())).thenReturn(requestEn); + when(requestEn.maxPrice(filterDto.getMaxPrice())).thenReturn(requestEn); + when(requestEn.openNow(filterDto.isOpenNow())).thenReturn(requestEn); + when(requestEn.name(filterDto.getName())).thenReturn(requestEn); + + when(requestUk.await()).thenReturn(ModelUtils.getPlacesSearchResponseUk()); + when(requestEn.await()).thenReturn(ModelUtils.getPlacesSearchResponseEn()); + + List actual = googleApiService.getResultFromPlacesApi(filterDto, userVO); + + List expected = List.of(ModelUtils.getPlacesSearchResultUk().getFirst(), + ModelUtils.getPlacesSearchResultEn().getFirst()); + + assertEquals(actual.size(), expected.size()); + + verify(requestUk, times(1)).await(); + verify(requestEn, times(1)).await(); + } + } + + @Test + void getResultFromPlacesApiNullLocationTest() throws IOException, InterruptedException, ApiException { + FilterPlacesApiDto filterDto = ModelUtils.getFilterPlacesApiDto(); + filterDto.setLocation(null); + UserVO userVO = ModelUtils.getUserVO(); + userVO.getUserLocationDto().setLongitude(null); + + Locale localeUk = Locale.forLanguageTag("uk"); + Locale localeEn = Locale.forLanguageTag("en"); + try (MockedStatic placesApiMockedStatic = mockStatic(PlacesApi.class)) { + NearbySearchRequest requestUk = mock(NearbySearchRequest.class); + NearbySearchRequest requestEn = mock(NearbySearchRequest.class); + + when(PlacesApi.nearbySearchQuery(context, filterDto.getLocation())).thenReturn(requestUk, requestEn); + + when(requestUk.radius(filterDto.getRadius())).thenReturn(requestUk); + when(requestUk.language(localeUk.getLanguage())).thenReturn(requestUk); + when(requestUk.keyword(filterDto.getKeyword())).thenReturn(requestUk); + when(requestUk.type(filterDto.getType())).thenReturn(requestUk); + when(requestUk.rankby(filterDto.getRankBy())).thenReturn(requestUk); + when(requestUk.minPrice(filterDto.getMinPrice())).thenReturn(requestUk); + when(requestUk.maxPrice(filterDto.getMaxPrice())).thenReturn(requestUk); + when(requestUk.openNow(filterDto.isOpenNow())).thenReturn(requestUk); + when(requestUk.name(filterDto.getName())).thenReturn(requestUk); + + when(requestEn.radius(filterDto.getRadius())).thenReturn(requestEn); + when(requestEn.language(localeEn.getLanguage())).thenReturn(requestEn); + when(requestEn.keyword(filterDto.getKeyword())).thenReturn(requestEn); + when(requestEn.type(filterDto.getType())).thenReturn(requestEn); + when(requestEn.rankby(filterDto.getRankBy())).thenReturn(requestEn); + when(requestEn.minPrice(filterDto.getMinPrice())).thenReturn(requestEn); + when(requestEn.maxPrice(filterDto.getMaxPrice())).thenReturn(requestEn); + when(requestEn.openNow(filterDto.isOpenNow())).thenReturn(requestEn); + when(requestEn.name(filterDto.getName())).thenReturn(requestEn); + + when(requestUk.await()).thenReturn(ModelUtils.getPlacesSearchResponseUk()); + when(requestEn.await()).thenReturn(ModelUtils.getPlacesSearchResponseEn()); + + assertThrows(NotFoundException.class, () -> googleApiService.getResultFromPlacesApi(filterDto, userVO)); + + verify(requestUk, times(0)).await(); + verify(requestEn, times(0)).await(); + } + } + + @Test + void getResultFromPlacesApiThrowsApiExceptionTest() throws IOException, InterruptedException, ApiException { + FilterPlacesApiDto filterDto = ModelUtils.getFilterPlacesApiDto(); + UserVO userVO = ModelUtils.getUserVO(); + + Locale localeUk = Locale.forLanguageTag("uk"); + Locale localeEn = Locale.forLanguageTag("en"); + try (MockedStatic placesApiMockedStatic = mockStatic(PlacesApi.class)) { + NearbySearchRequest requestUk = mock(NearbySearchRequest.class); + NearbySearchRequest requestEn = mock(NearbySearchRequest.class); + + when(PlacesApi.nearbySearchQuery(context, filterDto.getLocation())).thenReturn(requestUk, requestEn); + + when(requestUk.radius(filterDto.getRadius())).thenReturn(requestUk); + when(requestUk.language(localeUk.getLanguage())).thenReturn(requestUk); + when(requestUk.keyword(filterDto.getKeyword())).thenReturn(requestUk); + when(requestUk.type(filterDto.getType())).thenReturn(requestUk); + when(requestUk.rankby(filterDto.getRankBy())).thenReturn(requestUk); + when(requestUk.minPrice(filterDto.getMinPrice())).thenReturn(requestUk); + when(requestUk.maxPrice(filterDto.getMaxPrice())).thenReturn(requestUk); + when(requestUk.openNow(filterDto.isOpenNow())).thenReturn(requestUk); + when(requestUk.name(filterDto.getName())).thenReturn(requestUk); + + when(requestEn.radius(filterDto.getRadius())).thenReturn(requestEn); + when(requestEn.language(localeEn.getLanguage())).thenReturn(requestEn); + when(requestEn.keyword(filterDto.getKeyword())).thenReturn(requestEn); + when(requestEn.type(filterDto.getType())).thenReturn(requestEn); + when(requestEn.rankby(filterDto.getRankBy())).thenReturn(requestEn); + when(requestEn.minPrice(filterDto.getMinPrice())).thenReturn(requestEn); + when(requestEn.maxPrice(filterDto.getMaxPrice())).thenReturn(requestEn); + when(requestEn.openNow(filterDto.isOpenNow())).thenReturn(requestEn); + when(requestEn.name(filterDto.getName())).thenReturn(requestEn); + + when(requestUk.await()).thenReturn(ModelUtils.getPlacesSearchResponseUk()); + when(requestEn.await()).thenThrow(ApiException.class); + + assertDoesNotThrow(() -> googleApiService.getResultFromPlacesApi(filterDto, userVO)); + + verify(requestUk, times(1)).await(); + verify(requestEn, times(1)).await(); + } + } } diff --git a/service/src/test/java/greencity/service/PlaceServiceImplTest.java b/service/src/test/java/greencity/service/PlaceServiceImplTest.java index 66aaab4948..4d346e0642 100644 --- a/service/src/test/java/greencity/service/PlaceServiceImplTest.java +++ b/service/src/test/java/greencity/service/PlaceServiceImplTest.java @@ -12,6 +12,7 @@ import greencity.dto.discount.DiscountValueVO; import greencity.dto.filter.FilterDistanceDto; import greencity.dto.filter.FilterPlaceDto; +import greencity.dto.filter.FilterPlacesApiDto; import greencity.dto.language.LanguageVO; import greencity.dto.location.LocationAddressAndGeoForUpdateDto; import greencity.dto.location.LocationVO; @@ -44,6 +45,7 @@ import greencity.enums.Role; import greencity.enums.UserStatus; import greencity.exception.exceptions.NotFoundException; +import greencity.exception.exceptions.PlaceAlreadyExistsException; import greencity.exception.exceptions.PlaceStatusException; import greencity.exception.exceptions.UserBlockedException; import greencity.repository.CategoryRepo; @@ -74,6 +76,9 @@ import static greencity.ModelUtils.getPlace; import static greencity.ModelUtils.getPlaceUpdateDto; import static greencity.ModelUtils.getSearchPlacesDto; +import static greencity.ModelUtils.getFilterPlacesApiDto; +import static greencity.ModelUtils.getPlacesSearchResultEn; +import static greencity.ModelUtils.getPlaceByBoundsDtoFromApi; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -134,6 +139,7 @@ class PlaceServiceImplTest { .email("Nazar.stasyuk@gmail.com") .name("Nazar Stasyuk") .role(Role.ROLE_USER) + .userStatus(UserStatus.ACTIVATED) .lastActivityTime(LocalDateTime.now()) .dateOfRegistration(LocalDateTime.now()) .language(language) @@ -665,6 +671,25 @@ void getPlacesByFilterWithDistanceFromUserTest() { verify(modelMapper).map(genericEntity1, PlaceByBoundsDto.class); } + @Test + void getPlacesByFilterFromApiTest() { + FilterPlacesApiDto filterDto = getFilterPlacesApiDto(); + + when(googleApiService.getResultFromPlacesApi(filterDto, userVO)).thenReturn(getPlacesSearchResultEn()); + + var result = placeService.getPlacesByFilter(filterDto, userVO); + List updatedResult = result.stream().map(el -> { + el.setId(1L); + el.getLocation().setId(1L); + return el; + }).toList(); + List expectedResult = getPlaceByBoundsDtoFromApi(); + + Assertions.assertArrayEquals(updatedResult.toArray(), expectedResult.toArray()); + + verify(googleApiService).getResultFromPlacesApi(filterDto, userVO); + } + @Test void filterPlaceBySearchPredicateTest() { Place place = getPlace(); @@ -773,8 +798,32 @@ void addPlaceFromUiThrowsException() { assertThrows(UserBlockedException.class, () -> placeService.addPlaceFromUi(dto, email, null)); + verify(userRepo).findByEmail(user.getEmail()); + } + + @Test + void addPlaceFromUiSaveAlreadyExistingLocation() { + AddPlaceDto dto = ModelUtils.getAddPlaceDto(); + PlaceResponse placeResponse = ModelUtils.getPlaceResponse(); + String email = user.getEmail(); + + when(userRepo.findByEmail(email)).thenReturn(Optional.of(user)); + when(modelMapper.map(dto, PlaceResponse.class)).thenReturn(placeResponse); + when(googleApiService.getResultFromGeoCode(dto.getLocationName())).thenReturn(ModelUtils.getGeocodingResult()); + + double lat = ModelUtils.getGeocodingResult().getFirst().geometry.location.lat; + double lng = ModelUtils.getGeocodingResult().getFirst().geometry.location.lng; + + when(locationService.existsByLatAndLng(lat, lng)).thenReturn(true); + + assertThrows(PlaceAlreadyExistsException.class, () -> placeService.addPlaceFromUi(dto, email, null)); + verify(modelMapper).map(dto, PlaceResponse.class); verify(userRepo).findByEmail(user.getEmail()); + verify(googleApiService).getResultFromGeoCode(dto.getLocationName()); + verify(locationService).existsByLatAndLng(lat, lng); + + verify(modelMapper, times(0)).map(placeResponse, Place.class); } @Test