diff --git a/src/main/java/com/bazaar/orders/controller/ReviewsController.java b/src/main/java/com/bazaar/orders/controller/ReviewsController.java index 9693724..43a6162 100644 --- a/src/main/java/com/bazaar/orders/controller/ReviewsController.java +++ b/src/main/java/com/bazaar/orders/controller/ReviewsController.java @@ -4,7 +4,9 @@ import com.bazaar.orders.dto.reviews.CreateOrderReviewRequestDTO; import com.bazaar.orders.dto.reviews.OrderDetailedReviewDTO; import com.bazaar.orders.dto.reviews.OrderItemReviewDTO; +import com.bazaar.orders.dto.reviews.OrderItemReviewPageDTO; import com.bazaar.orders.dto.reviews.OrderReviewDTO; +import com.bazaar.orders.dto.reviews.OrderReviewPageDTO; import com.bazaar.orders.dto.reviews.SellerReputationDTO; import com.bazaar.orders.service.ReviewsService; import io.swagger.v3.oas.annotations.Operation; @@ -14,6 +16,7 @@ import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -21,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -180,10 +184,11 @@ public OrderItemReviewDTO getOrderItemReview(@PathVariable Long productId, @Path @ApiResponses( value = { @ApiResponse(responseCode = "200", description = "Product reviews retrieved successfully") }) @GetMapping("/products/{productId}") - public List getProductReviews(@PathVariable Long productId) { + public OrderItemReviewPageDTO getProductReviews(@PathVariable Long productId, @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { LOGGER.info("Received request to get reviews for product {}", productId); - return reviewsService.getProductReviews(productId); + return reviewsService.getProductReviews(productId, PageRequest.of(page, size)); } /** @@ -194,10 +199,11 @@ public List getProductReviews(@PathVariable Long productId) @Operation(summary = "Get seller reviews", description = "Retrieves all reviews associated with a seller.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Seller reviews retrieved successfully") }) @GetMapping("/seller/{sellerId}") - public List getSellerReviews(@PathVariable Long sellerId) { + public OrderReviewPageDTO getSellerReviews(@PathVariable Long sellerId, @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { LOGGER.info("Received request to get reviews for seller {}", sellerId); - return reviewsService.getSellerReviews(sellerId); + return reviewsService.getSellerReviews(sellerId, PageRequest.of(page, size)); } /** diff --git a/src/main/java/com/bazaar/orders/dto/reviews/OrderItemReviewPageDTO.java b/src/main/java/com/bazaar/orders/dto/reviews/OrderItemReviewPageDTO.java new file mode 100644 index 0000000..cc53bc1 --- /dev/null +++ b/src/main/java/com/bazaar/orders/dto/reviews/OrderItemReviewPageDTO.java @@ -0,0 +1,19 @@ +package com.bazaar.orders.dto.reviews; + +import java.util.List; + +public record OrderItemReviewPageDTO( + List reviews, int page, int size, + long totalPages +) { + + /** + * Creates a new instance of OrderItemReviewPageDTO from a Page of OrderItemReviewDTO. + * @param page page + * @return reviews OrderItemReviewPageDTO + */ + public static OrderItemReviewPageDTO fromPage(org.springframework.data.domain.Page page) { + return new OrderItemReviewPageDTO(page.getContent(), page.getNumber(), page.getSize(), + page.getTotalPages()); + } +} diff --git a/src/main/java/com/bazaar/orders/dto/reviews/OrderReviewPageDTO.java b/src/main/java/com/bazaar/orders/dto/reviews/OrderReviewPageDTO.java new file mode 100644 index 0000000..31d15e1 --- /dev/null +++ b/src/main/java/com/bazaar/orders/dto/reviews/OrderReviewPageDTO.java @@ -0,0 +1,19 @@ +package com.bazaar.orders.dto.reviews; + +import java.util.List; + +public record OrderReviewPageDTO( + List reviews, int page, int size, + long totalPages +) { + + /** + * Creates a new instance of OrderReviewPageDTO from a Page of OrderReviewDTO. + * @param page + * @return reviews OrderReviewPageDTO + */ + public static OrderReviewPageDTO fromPage(org.springframework.data.domain.Page page) { + return new OrderReviewPageDTO(page.getContent(), page.getNumber(), page.getSize(), + page.getTotalPages()); + } +} diff --git a/src/main/java/com/bazaar/orders/repository/OrderItemReviewRepository.java b/src/main/java/com/bazaar/orders/repository/OrderItemReviewRepository.java index 83a9a65..8bee1f9 100644 --- a/src/main/java/com/bazaar/orders/repository/OrderItemReviewRepository.java +++ b/src/main/java/com/bazaar/orders/repository/OrderItemReviewRepository.java @@ -1,6 +1,8 @@ package com.bazaar.orders.repository; import com.bazaar.orders.model.OrderItemReview; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; @@ -19,25 +21,15 @@ public interface OrderItemReviewRepository extends JpaRepository findByOrderItemId(Long orderItemId); - /** - * Finds a review associated with a specific order item and created by a specific - * user. - * @param orderItemId identifier of the reviewed order item. - * @param userId identifier of the user who created the review. - * @return an {@link Optional} containing the matching {@link OrderItemReview} if - * found; otherwise, {@link Optional#empty()}. - */ - Optional findByOrderItemIdAndUserId(Long orderItemId, Long userId); - /** * Retrieves all reviews associated with the provided list of order item identifiers. * @param orderItemIds list of order item identifiers. - * @return list of matching {@link OrderItemReview} entities. + * @return page of matching {@link OrderItemReview} entities. */ - List findAllByOrderItemIdIn(List orderItemIds); + Page findAllByOrderItemIdIn(List orderItemIds, Pageable pageable); /** - * Retrieves all reviews associated with the provided orderId. + * Retrieves a list of all reviews associated with the provided orderId. * @param orderId orderId. */ List findAllByOrderId(Long orderId); diff --git a/src/main/java/com/bazaar/orders/repository/OrderReviewRepository.java b/src/main/java/com/bazaar/orders/repository/OrderReviewRepository.java index 459326f..870e45d 100644 --- a/src/main/java/com/bazaar/orders/repository/OrderReviewRepository.java +++ b/src/main/java/com/bazaar/orders/repository/OrderReviewRepository.java @@ -2,6 +2,8 @@ import com.bazaar.orders.dto.reviews.SellerReputationDTO; import com.bazaar.orders.model.OrderReview; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -26,9 +28,9 @@ public interface OrderReviewRepository extends JpaRepository /** * Finds reviews associated with a specific seller. * @param sellerId identifier of the seller. - * @return an {@link List} containing the matching {@link OrderReview} + * @return an {@link Page} containing the matching {@link OrderReview} */ - List findBySellerId(Long sellerId); + Page findBySellerId(Long sellerId, Pageable pageable); /** * Gets reputation associated with a specific seller. diff --git a/src/main/java/com/bazaar/orders/service/ReviewsService.java b/src/main/java/com/bazaar/orders/service/ReviewsService.java index 3292f40..c728775 100644 --- a/src/main/java/com/bazaar/orders/service/ReviewsService.java +++ b/src/main/java/com/bazaar/orders/service/ReviewsService.java @@ -7,7 +7,9 @@ import com.bazaar.orders.dto.reviews.CreateOrderReviewRequestDTO; import com.bazaar.orders.dto.reviews.OrderDetailedReviewDTO; import com.bazaar.orders.dto.reviews.OrderItemReviewDTO; +import com.bazaar.orders.dto.reviews.OrderItemReviewPageDTO; import com.bazaar.orders.dto.reviews.OrderReviewDTO; +import com.bazaar.orders.dto.reviews.OrderReviewPageDTO; import com.bazaar.orders.dto.reviews.SellerReputationDTO; import com.bazaar.orders.dto.user.UserProfileResponseDTO; import com.bazaar.orders.dto.user.UserStatusResponseDTO; @@ -25,6 +27,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.context.MessageSource; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.time.LocalDateTime; @@ -224,16 +227,16 @@ public OrderItemReviewDTO getOrderItemReview(Long productId, Long orderId, Long * @param productId identifier of the reviewed product. * @return list of {@link OrderItemReviewDTO}. */ - public List getProductReviews(Long productId) { + public OrderItemReviewPageDTO getProductReviews(Long productId, Pageable pageable) { List orderItems = orderItemRepository.findByProductId(productId); List orderItemIds = orderItems.stream().map(OrderItem::getId).toList(); - return orderItemReviewRepository.findAllByOrderItemIdIn(orderItemIds).stream().map(review -> { + return OrderItemReviewPageDTO.fromPage(orderItemReviewRepository.findAllByOrderItemIdIn(orderItemIds, pageable).map(review -> { var userProfile = getUserProfile(review.getUserId()); return OrderItemReviewDTO.fromOrderItemReview(review, userProfile); - }).toList(); + })); } /** @@ -241,11 +244,9 @@ public List getProductReviews(Long productId) { * @param sellerId identifier of the reviewed product. * @return list of {@link OrderReviewDTO}. */ - public List getSellerReviews(final Long sellerId) { - return orderReviewRepository.findBySellerId(sellerId) - .stream() - .map(review -> OrderReviewDTO.fromOrderReview(review, getUserProfile(review.getUserId()))) - .toList(); + public OrderReviewPageDTO getSellerReviews(final Long sellerId, Pageable pageable) { + return OrderReviewPageDTO.fromPage(orderReviewRepository.findBySellerId(sellerId, pageable) + .map(review -> OrderReviewDTO.fromOrderReview(review, getUserProfile(review.getUserId())))); } /** diff --git a/src/test/java/com/bazaar/orders/controller/ReviewsControllerTest.java b/src/test/java/com/bazaar/orders/controller/ReviewsControllerTest.java index 0e8e9b2..90ce1f5 100644 --- a/src/test/java/com/bazaar/orders/controller/ReviewsControllerTest.java +++ b/src/test/java/com/bazaar/orders/controller/ReviewsControllerTest.java @@ -19,6 +19,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -283,7 +284,7 @@ void testCreateOrderItemsReviewSuccess() { when(userClient.getUserProfile(1L)) .thenReturn(new ApiResponse<>(new UserProfileResponseDTO("juan", "perez", null, null, null))); - mockOrderItem(1L, 6L, 7L); + mockOrderItem(1L, 6L, 88L); var response = postOrderItemsReview(1L, 1L, orderItemReviewJson(6L, 5, "Amazing product")); @@ -365,23 +366,73 @@ void testGetProductReviewsSuccess() { when(userClient.getUserProfile(1L)) .thenReturn(new ApiResponse<>(new UserProfileResponseDTO("juan", "perez", null, null, null))); - var orderItem = mockOrderItem(1L, 2L, 8L); + var orderItem = mockOrderItem(1L, 3L, 890L); - mockProductReviews(2L, List.of(orderItem)); + mockProductReviews(3L, List.of(orderItem)); - postOrderItemsReview(1L, 1L, orderItemReviewJson(2L, 5, "Perfect")); + postOrderItemsReview(1L, 1L, orderItemReviewJson(3L, 5, "Perfect")); - var response = getProductReviews(2L, 1L); + var response = getProductReviews(3L, 1L, 0, 20); assertEquals(HttpStatus.OK, response.getStatusCode()); String body = response.getBody(); - assertEquals(1, ((Number) JsonPath.read(body, "$.data.length()")).intValue()); + assertEquals(1, ((Number) JsonPath.read(body, "$.data.reviews.length()")).intValue()); - assertEquals(5, ((Number) JsonPath.read(body, "$.data[0].rating")).intValue()); + assertEquals(5, ((Number) JsonPath.read(body, "$.data.reviews[0].rating")).intValue()); + + assertEquals("Perfect", JsonPath.read(body, "$.data.reviews[0].comment")); + + assertPageResponse(body, 0, 20, null, 1); + } + + @Test + void testGetProductReviewsMultiplePages() { + when(userClient.getUserProfile(1L)) + .thenReturn(new ApiResponse<>( + new UserProfileResponseDTO("juan", "perez", null, null, null) + )); + + Long productId = 2L; + List items = new ArrayList<>(); + + for (long i = 1; i <= 25; i++) { + Long orderId = i + 150; + mockArrivedOrder(orderId); + + OrderItem item = mockOrderItem(orderId, productId, i); + items.add(item); - assertEquals("Perfect", JsonPath.read(body, "$.data[0].comment")); + postOrderItemsReview(orderId, 1L, + orderItemReviewJson(productId, (int) (i % 5 + 1), "comment " + i)); + } + + mockProductReviews(productId, items); + + var page0 = getProductReviews(productId, 1L, 0, 20); + assertEquals(HttpStatus.OK, page0.getStatusCode()); + String body0 = page0.getBody(); + + assertEquals(20, + ((Number) JsonPath.read(body0, "$.data.reviews.length()")).intValue()); + + assertEquals(2, + ((Number) JsonPath.read(body0, "$.data.totalPages")).intValue()); + + assertPageResponse(body0, 0, 20, null, 2); + + var page1 = getProductReviews(productId, 1L, 1, 20); + assertEquals(HttpStatus.OK, page1.getStatusCode()); + String body1 = page1.getBody(); + + assertEquals(5, + ((Number) JsonPath.read(body1, "$.data.reviews.length()")).intValue()); + + assertEquals(2, + ((Number) JsonPath.read(body1, "$.data.totalPages")).intValue()); + + assertPageResponse(body1, 1, 20, null, 2); } @Test @@ -393,31 +444,35 @@ void testGetSellerReviewsSuccess() { postOrderReview(25L, 1L, sellerReviewJson(9L, 5, "Excellent seller")); - var response = getSellerReviews(9L, 1L); + var response = getSellerReviews(9L, 1L, 0, 20); assertEquals(HttpStatus.OK, response.getStatusCode()); String body = response.getBody(); - assertEquals(1, ((Number) JsonPath.read(body, "$.data.length()")).intValue()); + assertEquals(1, ((Number) JsonPath.read(body, "$.data.reviews.length()")).intValue()); - assertEquals(5, ((Number) JsonPath.read(body, "$.data[0].rating")).intValue()); + assertEquals(5, ((Number) JsonPath.read(body, "$.data.reviews[0].rating")).intValue()); - assertEquals("Excellent seller", JsonPath.read(body, "$.data[0].comment")); + assertEquals("Excellent seller", JsonPath.read(body, "$.data.reviews[0].comment")); - assertEquals(9L, ((Number) JsonPath.read(body, "$.data[0].sellerId")).longValue()); + assertEquals(9L, ((Number) JsonPath.read(body, "$.data.reviews[0].sellerId")).longValue()); + + assertPageResponse(body, 0, 20, null, 1); } @Test void testGetSellerReviewsEmpty() { - var response = getSellerReviews(999L, 1L); + var response = getSellerReviews(999L, 1L, 0, 20); assertEquals(HttpStatus.OK, response.getStatusCode()); String body = response.getBody(); - assertEquals(0, ((Number) JsonPath.read(body, "$.data.length()")).intValue()); + assertEquals(0, ((Number) JsonPath.read(body, "$.data.reviews.length()")).intValue()); + + assertPageResponse(body, 0, 20, null, 0); } @Test @@ -435,23 +490,59 @@ void testGetSellerReviewsMultipleUsers() { postOrderReview(200L, 2L, sellerReviewJson(8L, 4, "Very good seller")); - var response = getSellerReviews(8L, 1L); + var response = getSellerReviews(8L, 1L, 0, 20); assertEquals(HttpStatus.OK, response.getStatusCode()); String body = response.getBody(); - assertEquals(2, ((Number) JsonPath.read(body, "$.data.length()")).intValue()); + assertEquals(2, ((Number) JsonPath.read(body, "$.data.reviews.length()")).intValue()); + + List ratings = JsonPath.read(body, "$.data.reviews[*].rating"); + + List comments = JsonPath.read(body, "$.data.reviews[*].comment"); + + assertPageResponse(body, 0, 20, null, 1); - List ratings = JsonPath.read(body, "$.data[*].rating"); assertTrue(ratings.contains(5)); assertTrue(ratings.contains(4)); - List comments = JsonPath.read(body, "$.data[*].comment"); assertTrue(comments.contains("Excellent seller")); assertTrue(comments.contains("Very good seller")); } + @Test + void testGetSellerReviewsMultiplePages() { + when(userClient.getUserProfile(1L)) + .thenReturn(new ApiResponse<>(new UserProfileResponseDTO("juan", "perez", null, null, null))); + + Long sellerId = 15L; + + for (long i = 1; i <= 21; i++) { + Long orderId = i + 600; + mockArrivedOrder(orderId); + + postOrderReview(orderId, i, sellerReviewJson(sellerId, 5, "review " + i)); + } + + var page0 = getSellerReviews(sellerId, 1L, 0, 20); + String body0 = page0.getBody(); + + assertEquals(20, ((Number) JsonPath.read(body0, "$.data.reviews.length()")).intValue()); + assertEquals(2, ((Number) JsonPath.read(body0, "$.data.totalPages")).intValue()); + + assertPageResponse(body0, 0, 20, null, 2); + + var page1 = getSellerReviews(sellerId, 1L, 1, 20); + + String body1 = page1.getBody(); + + assertEquals(1, ((Number) JsonPath.read(body1, "$.data.reviews.length()")).intValue()); + assertEquals(2, ((Number) JsonPath.read(body1, "$.data.totalPages")).intValue()); + + assertPageResponse(body1, 1, 20, null, 2); + } + @Test void testGetSellerReputationSuccess() { when(userClient.getUserProfile(1L)) diff --git a/src/test/java/com/bazaar/orders/testConfig/AbstractTest.java b/src/test/java/com/bazaar/orders/testConfig/AbstractTest.java index 528b314..4b43d9b 100644 --- a/src/test/java/com/bazaar/orders/testConfig/AbstractTest.java +++ b/src/test/java/com/bazaar/orders/testConfig/AbstractTest.java @@ -430,10 +430,14 @@ protected ResponseEntity getOrderItemReview(Long orderId, Long productId .body(result.getResponseBody()); } - protected ResponseEntity getProductReviews(Long productId, Long userId) { + protected ResponseEntity getProductReviews(Long productId, Long userId, Integer page, Integer size) { var result = webTestClient.get() - .uri("reviews/products/%d".formatted(productId)) + .uri(uriBuilder -> uriBuilder + .path("/reviews/products/{productId}") + .queryParam("page", page) + .queryParam("size", size) + .build(productId)) .header("X-User-Id", userId.toString()) .exchange() .expectBody(String.class) @@ -444,10 +448,14 @@ protected ResponseEntity getProductReviews(Long productId, Long userId) .body(result.getResponseBody()); } - protected ResponseEntity getSellerReviews(Long sellerId, Long userId) { + protected ResponseEntity getSellerReviews(Long sellerId, Long userId, Integer page, Integer size) { var result = webTestClient.get() - .uri("/reviews/seller/%d".formatted(sellerId)) + .uri(uriBuilder -> uriBuilder + .path("/reviews/seller/{sellerId}") + .queryParam("page", page) + .queryParam("size", size) + .build(sellerId)) .header("X-User-Id", userId.toString()) .exchange() .expectBody(String.class) @@ -474,7 +482,9 @@ protected ResponseEntity getSellerReputation(Long sellerId) { public void assertPageResponse(String body, Integer page, Integer size, Integer totalElements, Integer totalPages) { assertEquals(page, ((Number) JsonPath.read(body, "$.data.page")).intValue()); assertEquals(size, ((Number) JsonPath.read(body, "$.data.size")).intValue()); - assertEquals(totalElements, ((Number) JsonPath.read(body, "$.data.totalElements")).intValue()); + if (totalElements != null) { + assertEquals(totalElements, ((Number) JsonPath.read(body, "$.data.totalElements")).intValue()); + } assertEquals(totalPages, ((Number) JsonPath.read(body, "$.data.totalPages")).intValue()); }