diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java index f0dd97f..600f13e 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -1,15 +1,18 @@ package com.Podzilla.analytics.api.controllers; import org.springframework.http.ResponseEntity; +// import org.springframework.web.bind.MethodArgumentNotValidException; +// import org.springframework.web.bind.MissingServletRequestParameterException; +// import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +// import org.springframework.web.method.annotation. +// MethodArgumentTypeMismatchException; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; @@ -27,34 +30,37 @@ public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; - @Operation(summary = "Get average time from order placement to shipping", - description = "Returns the average time (in hours) between when" + @Operation( + summary = "Get average time from order placement to shipping", + description = "Returns the average time (in hours) between when" + " an order was placed and when it was shipped, grouped" - + " by the specified dimension") + + " by the specified dimension" + ) @GetMapping("/place-to-ship-time") public ResponseEntity> getPlaceToShipTime( @Valid @ModelAttribute final FulfillmentPlaceToShipRequest req) { - List reportData = fulfillmentAnalyticsService - .getPlaceToShipTimeResponse( + final List reportData = + fulfillmentAnalyticsService.getPlaceToShipTimeResponse( req.getStartDate(), req.getEndDate(), req.getGroupBy()); return ResponseEntity.ok(reportData); } - @Operation(summary = "Get average time from shipping to delivery", - description = "Returns the average time (in hours) between when" + + @Operation( + summary = "Get average time from shipping to delivery", + description = "Returns the average time (in hours) between when" + " an order was shipped and when it was delivered, grouped" - + " by the specified dimension") + + " by the specified dimension" + ) @GetMapping("/ship-to-deliver-time") public ResponseEntity> getShipToDeliverTime( @Valid @ModelAttribute final FulfillmentShipToDeliverRequest req) { - log.debug(req.toString()); - - List reportData = fulfillmentAnalyticsService - .getShipToDeliverTimeResponse( + final List reportData = + fulfillmentAnalyticsService.getShipToDeliverTimeResponse( req.getStartDate(), req.getEndDate(), req.getGroupBy()); diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java index bf4f6d9..cf0a7ae 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -1,14 +1,48 @@ package com.Podzilla.analytics.api.controllers; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; import com.Podzilla.analytics.services.ProfitAnalyticsService; +import io.swagger.v3.oas.annotations.Operation; +// import io.swagger.v3.oas.annotations.responses.ApiResponse; +// import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +/** + * REST controller for profit analytics operations. + * Provides endpoints to analyze revenue, cost, and profit metrics. + */ +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/profit-analytics") public class ProfitReportController { private final ProfitAnalyticsService profitAnalyticsService; + + + @Operation( + summary = "Get profit by product category", + description = "Returns the revenue, cost, and profit metrics " + + "grouped by product category") + @GetMapping("/by-category") + public ResponseEntity> getProfitByCategory( + @Valid @ModelAttribute final DateRangeRequest request) { + + List profitData = + profitAnalyticsService.getProfitByCategory( + request.getStartDate(), + request.getEndDate()); + return ResponseEntity.ok(profitData); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java index 6af9a21..1a2bd8f 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java @@ -40,6 +40,6 @@ public enum PlaceToShipGroupBy { @NotNull(message = "groupBy is required") @Schema(description = "How to group the results (OVERALL, REGION, COURIER " + "depending on endpoint)", example = "OVERALL", required = true) - private PlaceToShipGroupBy groupBy = PlaceToShipGroupBy.OVERALL; + private PlaceToShipGroupBy groupBy; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java index bb368bc..c87b2aa 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java @@ -44,5 +44,5 @@ public enum ShipToDeliverGroupBy { @NotNull(message = "groupBy is required") @Schema(description = "How to group the results (OVERALL, REGION, COURIER " + "depending on endpoint)", example = "OVERALL", required = true) - private ShipToDeliverGroupBy groupBy = ShipToDeliverGroupBy.OVERALL; + private ShipToDeliverGroupBy groupBy; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java b/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java new file mode 100644 index 0000000..f38927f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java @@ -0,0 +1,35 @@ +package com.Podzilla.analytics.api.dtos.profit; + +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProfitByCategory { + + @Schema(description = "Product category name", example = "Electronics") + private String category; + + @Schema(description = "Total revenue for the category in the given period", + example = "15000.50") + private BigDecimal totalRevenue; + + @Schema(description = "Total cost for the category in the given period", + example = "10000.25") + private BigDecimal totalCost; + + @Schema(description = "Gross profit (revenue - cost)", + example = "5000.25") + private BigDecimal grossProfit; + + @Schema(description = "Gross profit margin percentage", + example = "33.33") + private BigDecimal grossProfitMargin; +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java new file mode 100644 index 0000000..23a8968 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java @@ -0,0 +1,12 @@ +package com.Podzilla.analytics.api.projections.profit; + +import java.math.BigDecimal; + +/** + * Projection interface for profit by category query results + */ +public interface ProfitByCategoryProjection { + String getCategory(); + BigDecimal getTotalRevenue(); + BigDecimal getTotalCost(); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java index 4793b6e..b4b9ac8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java @@ -1,9 +1,26 @@ package com.Podzilla.analytics.repositories; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; + +import java.time.LocalDateTime; +import java.util.List; public interface SalesLineItemRepository - extends JpaRepository { + extends JpaRepository { + @Query("SELECT sli.product.category as category, " + + "SUM(sli.quantity * sli.pricePerUnit) as totalRevenue, " + + "SUM(sli.quantity * sli.product.cost) as totalCost " + + "FROM SalesLineItem sli " + + "WHERE sli.order.orderPlacedTimestamp BETWEEN " + + ":startDate AND :endDate " + + "AND sli.order.status = 'COMPLETED' " + + "GROUP BY sli.product.category") + List findSalesByCategoryBetweenDates( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java index be2fa59..85d3fb3 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -2,9 +2,62 @@ import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; +import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; + import lombok.RequiredArgsConstructor; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Service public class ProfitAnalyticsService { + private final SalesLineItemRepository salesLineItemRepository; + // Precision constant for percentage calculations + private static final int PERCENTAGE_PRECISION = 4; + + public List getProfitByCategory( + final LocalDate startDate, + final LocalDate endDate) { + // Convert LocalDate to LocalDateTime for start of day and end of day + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List salesData = salesLineItemRepository + .findSalesByCategoryBetweenDates(startDateTime, endDateTime); + + return salesData.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + private ProfitByCategory convertToDTO( + final ProfitByCategoryProjection projection) { + BigDecimal totalRevenue = projection.getTotalRevenue(); + BigDecimal totalCost = projection.getTotalCost(); + BigDecimal grossProfit = totalRevenue.subtract(totalCost); + + BigDecimal grossProfitMargin = BigDecimal.ZERO; + if (totalRevenue.compareTo(BigDecimal.ZERO) > 0) { + grossProfitMargin = grossProfit + .divide(totalRevenue, PERCENTAGE_PRECISION, + RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + return ProfitByCategory.builder() + .category(projection.getCategory()) + .totalRevenue(totalRevenue) + .totalCost(totalCost) + .grossProfit(grossProfit) + .grossProfitMargin(grossProfitMargin) + .build(); + } } diff --git a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java index 079bd20..26fa7cb 100644 --- a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java +++ b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java @@ -1,19 +1,33 @@ package com.Podzilla.analytics.validation.validators; -import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import java.time.LocalDate; +import java.lang.reflect.Method; + import com.Podzilla.analytics.validation.annotations.ValidDateRange; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public final class DateRangeValidator implements - ConstraintValidator { + ConstraintValidator { @Override - public boolean isValid(final DateRangeRequest request, + public boolean isValid(final Object value, final ConstraintValidatorContext context) { - if (request.getStartDate() == null || request.getEndDate() == null) { + if (value == null) { return true; } - return request.getEndDate().isAfter(request.getStartDate()); + + try { + Method getStartDate = value.getClass().getMethod("getStartDate"); + Method getEndDate = value.getClass().getMethod("getEndDate"); + LocalDate startDate = (LocalDate) getStartDate.invoke(value); + LocalDate endDate = (LocalDate) getEndDate.invoke(value); + if (startDate == null || endDate == null) { + return true; // Let @NotNull handle this + } + return !endDate.isBefore(startDate); + } catch (Exception e) { + return false; + } } } diff --git a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java index 61e3254..e76925d 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java @@ -1,10 +1,7 @@ package com.Podzilla.analytics.controllers; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.math.BigDecimal; @@ -15,22 +12,33 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; -import com.Podzilla.analytics.api.controllers.FulfillmentReportController; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class FulfillmentReportControllerTest { - private FulfillmentReportController controller; + @Autowired + private TestRestTemplate restTemplate; + + @MockBean private FulfillmentAnalyticsService mockService; + private ObjectMapper objectMapper; private LocalDate startDate; private LocalDate endDate; private List overallTimeResponses; @@ -39,8 +47,8 @@ public class FulfillmentReportControllerTest { @BeforeEach public void setup() { - mockService = mock(FulfillmentAnalyticsService.class); - controller = new FulfillmentReportController(mockService); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); startDate = LocalDate.of(2024, 1, 1); endDate = LocalDate.of(2024, 1, 31); @@ -80,18 +88,26 @@ public void testGetPlaceToShipTime_Overall() { startDate, endDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); - assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(1); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); + assertThat(response.getBody().get(0).getAverageDuration()).isEqualTo(BigDecimal.valueOf(24.5)); } @Test @@ -101,18 +117,26 @@ public void testGetPlaceToShipTime_ByRegion() { startDate, endDate, PlaceToShipGroupBy.REGION)) .thenReturn(regionTimeResponses); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.REGION.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(2); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("RegionID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("RegionID_2"); } @Test @@ -122,17 +146,24 @@ public void testGetShipToDeliverTime_Overall() { startDate, endDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -142,18 +173,25 @@ public void testGetShipToDeliverTime_ByRegion() { startDate, endDate, ShipToDeliverGroupBy.REGION)) .thenReturn(regionTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.REGION.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("RegionID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("RegionID_2"); } @Test @@ -163,18 +201,25 @@ public void testGetShipToDeliverTime_ByCourier() { startDate, endDate, ShipToDeliverGroupBy.COURIER)) .thenReturn(courierTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.COURIER); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(courierTimeResponses, response.getBody()); - assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.COURIER.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("CourierID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("CourierID_2"); } // Edge case tests @@ -186,17 +231,24 @@ public void testGetPlaceToShipTime_EmptyResponse() { startDate, endDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } @Test @@ -206,47 +258,63 @@ public void testGetShipToDeliverTime_EmptyResponse() { startDate, endDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } - // @Test - // public void testGetPlaceToShipTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = - // controller.getPlaceToShipTime(request); - - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } - - // @Test - // public void testGetShipToDeliverTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentShipToDeliverRequest request = new - // FulfillmentShipToDeliverRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = - // controller.getShipToDeliverTime(request); + @Test + public void testGetPlaceToShipTime_InvalidGroupBy() { + // Build URL without groupBy param + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } + @Test + public void testGetShipToDeliverTime_InvalidGroupBy() { + // Build URL without groupBy param + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } @Test public void testGetPlaceToShipTime_SameDayRange() { @@ -258,16 +326,24 @@ public void testGetPlaceToShipTime_SameDayRange() { sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request with same start and end date - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - sameDate, sameDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -280,16 +356,24 @@ public void testGetShipToDeliverTime_SameDayRange() { sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request with same start and end date - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -303,17 +387,24 @@ public void testGetPlaceToShipTime_FutureDates() { futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request with future dates - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", futureStart.toString()) + .queryParam("endDate", futureEnd.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } @Test @@ -323,17 +414,21 @@ public void testGetShipToDeliverTime_ServiceException() { any(), any(), any())) .thenThrow(new RuntimeException("Service error")); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - controller should handle exception - // Note: Actual behavior depends on how controller handles exceptions - // This might need adjustment based on actual implementation - try { - controller.getShipToDeliverTime(request); - } catch (RuntimeException e) { - assertEquals("Service error", e.getMessage()); - } + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } } \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java new file mode 100644 index 0000000..6c1f50c --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java @@ -0,0 +1,282 @@ +package com.Podzilla.analytics.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; + +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; +import com.Podzilla.analytics.services.ProfitAnalyticsService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ProfitReportControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @MockBean + private ProfitAnalyticsService mockService; + + private ObjectMapper objectMapper; + private LocalDate startDate; + private LocalDate endDate; + private List profitData; + + @BeforeEach + public void setup() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + startDate = LocalDate.of(2024, 1, 1); + endDate = LocalDate.of(2024, 1, 31); + + // Setup test data + profitData = Arrays.asList( + ProfitByCategory.builder() + .category("Electronics") + .totalRevenue(BigDecimal.valueOf(10000.50)) + .totalCost(BigDecimal.valueOf(7500.25)) + .grossProfit(BigDecimal.valueOf(2500.25)) + .grossProfitMargin(BigDecimal.valueOf(25.00)) + .build(), + ProfitByCategory.builder() + .category("Clothing") + .totalRevenue(BigDecimal.valueOf(5500.75)) + .totalCost(BigDecimal.valueOf(3000.50)) + .grossProfit(BigDecimal.valueOf(2500.25)) + .grossProfitMargin(BigDecimal.valueOf(45.45)) + .build()); + } + + @Test + public void testGetProfitByCategory_Success() { + // Configure mock service + when(mockService.getProfitByCategory(startDate, endDate)) + .thenReturn(profitData); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(2); + assertThat(response.getBody().get(0).getCategory()).isEqualTo("Electronics"); + assertThat(response.getBody().get(0).getTotalRevenue()).isEqualTo(BigDecimal.valueOf(10000.50)); + assertThat(response.getBody().get(0).getGrossProfit()).isEqualTo(BigDecimal.valueOf(2500.25)); + assertThat(response.getBody().get(1).getCategory()).isEqualTo("Clothing"); + assertThat(response.getBody().get(1).getGrossProfitMargin()).isEqualTo(BigDecimal.valueOf(45.45)); + } + + @Test + public void testGetProfitByCategory_EmptyResult() { + // Configure mock service to return empty list + when(mockService.getProfitByCategory(startDate, endDate)) + .thenReturn(Collections.emptyList()); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); + } + + @Test + public void testGetProfitByCategory_MissingStartDate() { + // Build URL with missing required parameter + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_MissingEndDate() { + // Build URL with missing required parameter + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_InvalidDateFormat() { + // Build URL with invalid date format + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", "2024-01-01") + .queryParam("endDate", "invalid-date") + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_StartDateAfterEndDate() { + // Set up dates where start is after end + LocalDate invalidStart = LocalDate.of(2024, 2, 1); + LocalDate invalidEnd = LocalDate.of(2024, 1, 1); + + // Build URL with invalid date range + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", invalidStart.toString()) + .queryParam("endDate", invalidEnd.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_FutureDateRange() { + // Set up future dates + LocalDate futureStart = LocalDate.now().plusDays(1); + LocalDate futureEnd = LocalDate.now().plusDays(30); + + // Configure mock service - should return empty data for future dates + when(mockService.getProfitByCategory(futureStart, futureEnd)) + .thenReturn(Collections.emptyList()); + + // Build URL with future date range + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", futureStart.toString()) + .queryParam("endDate", futureEnd.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); + } + + @Test + public void testGetProfitByCategory_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getProfitByCategory(sameDate, sameDate)) + .thenReturn(profitData); + + // Build URL with same day for start and end + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getCategory()).isEqualTo("Electronics"); + } + + @Test + public void testGetProfitByCategory_ServiceException() { + // Configure mock service to throw exception + when(mockService.getProfitByCategory(any(), any())) + .thenThrow(new RuntimeException("Service error")); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index f03dd4c..0139a73 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,7 +3,6 @@ spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver - # JPA/Hibernate Configuration spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true