diff --git a/eclipse-java-formatter.xml b/eclipse-java-formatter.xml new file mode 100644 index 0000000..7c5b975 --- /dev/null +++ b/eclipse-java-formatter.xml @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 006f6d3..ef1d43b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,100 +1,139 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.4.5 - - - com.Podzilla - analytics - 0.0.1-SNAPSHOT - analytics - The Operational Analytics Service is an event-driven application designed to capture, process, and expose key operational data and derived insights from various upstream microservices (e.g., Warehouse, Courier, Order services). It acts as a centralized source of truth for historical operational events and their derived state, providing valuable analytics through a dedicated API - - - - - - - - - - - - - - - 23 - - - - io.github.cdimascio - java-dotenv - 5.2.2 - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.amqp - spring-rabbit-test - test - - - jakarta.validation - jakarta.validation-api - 3.0.2 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.5.0 - + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.Podzilla + analytics + 0.0.1-SNAPSHOT + analytics + The Operational Analytics Service is an event-driven application designed to capture, process, and expose key operational data and derived insights from various upstream microservices (e.g., Warehouse, Courier, Order services). It acts as a centralized source of truth for historical operational events and their derived state, providing valuable analytics through a dedicated API + + + 21 + + + + + + io.github.cdimascio + java-dotenv + 5.2.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.postgresql + postgresql + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + com.github.Podzilla podzilla-utils-lib - v1.1.5 + v1.1.6 + + + + + jakarta.validation + jakarta.validation-api + 3.0.2 org.hibernate.validator hibernate-validator - 8.0.1.Final + 8.0.1.Final + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.amqp + spring-rabbit-test + test + + + org.mockito + mockito-core + 5.11.0 + test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + net.bytebuddy + byte-buddy-agent + 1.14.12 + test + + + com.h2database + h2 + test - + + + jitpack.io @@ -102,51 +141,52 @@ - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - config/checkstyle/sun_checks.xml - true - true - - - - validate - validate - - check - - - - - - - - \ No newline at end of file + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + config/checkstyle/sun_checks.xml + true + true + + + + validate + validate + + check + + + + + + + + diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java index 3a9c063..b5180d4 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -1,14 +1,38 @@ package com.Podzilla.analytics.api.controllers; +import java.util.List; + +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.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; import com.Podzilla.analytics.services.ProductAnalyticsService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController + @RequestMapping("/product-analytics") public class ProductReportController { + private final ProductAnalyticsService productAnalyticsService; + + @GetMapping("/top-sellers") + public ResponseEntity> getTopSellers( + @Valid @ModelAttribute final TopSellerRequest requestDTO) { + + List topSellersList = productAnalyticsService + .getTopSellers(requestDTO.getStartDate(), + requestDTO.getEndDate(), + requestDTO.getLimit(), + requestDTO.getSortBy()); + + return ResponseEntity.ok(topSellersList); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java index 47f839f..b2c6555 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -1,9 +1,20 @@ package com.Podzilla.analytics.api.controllers; +import java.util.List; + +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.revenue.RevenueByCategoryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; import com.Podzilla.analytics.services.RevenueReportService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -11,4 +22,23 @@ @RequestMapping("/revenue-analytics") public class RevenueReportController { private final RevenueReportService revenueReportService; + + @GetMapping("/summary") + public ResponseEntity> getRevenueSummary( + @Valid @ModelAttribute final RevenueSummaryRequest requestDTO) { + return ResponseEntity.ok(revenueReportService + .getRevenueSummary(requestDTO.getStartDate(), + requestDTO.getEndDate(), + requestDTO.getPeriod().name())); + } + + @GetMapping("/by-category") + public ResponseEntity> getRevenueByCategory( + @Valid @ModelAttribute final RevenueByCategoryRequest requestDTO) { + List summaryList = revenueReportService + .getRevenueByCategory( + requestDTO.getStartDate(), + requestDTO.getEndDate()); + return ResponseEntity.ok(summaryList); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java new file mode 100644 index 0000000..582737b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java @@ -0,0 +1,52 @@ +package com.Podzilla.analytics.api.dtos.product; + +import java.time.LocalDate; + +import org.jetbrains.annotations.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TopSellerRequest { + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the report (inclusive)", + example = "2024-01-01", required = true) + private LocalDate startDate; + + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the report (inclusive)", + example = "2024-01-31", required = true) + private LocalDate endDate; + + @NotNull + @Positive + @Schema(description = "Maximum number of top sellers to return", + example = "10", required = true) + private Integer limit; + + @NotNull + @Schema(description = "Sort by revenue or units", required = true, + implementation = SortBy.class) + private SortBy sortBy; + + public enum SortBy { + @Schema(description = "Sort by total revenue") + REVENUE, + @Schema(description = "Sort by total units sold") + UNITS + } +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java new file mode 100644 index 0000000..18e38fe --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.api.dtos.product; + +import java.math.BigDecimal; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TopSellerResponse { + @Schema(description = "Product ID", example = "101") + private Long productId; + @Schema(description = "Product name", example = "Wireless Mouse") + private String productName; + @Schema(description = "Product category", example = "Electronics") + private String category; + @Schema(description = "Total value sold", example = "2500.75") + private BigDecimal value; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java new file mode 100644 index 0000000..6eaf06e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java @@ -0,0 +1,34 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.time.LocalDate; +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Request parameters for fetching revenue by category") +public class RevenueByCategoryRequest { + + @NotNull(message = "Start date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the revenue report (inclusive)", + example = "2023-01-01", required = true) + private LocalDate startDate; + + @NotNull(message = "End date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the revenue report (inclusive)", + example = "2023-01-31", required = true) + private LocalDate endDate; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java new file mode 100644 index 0000000..6b2ccab --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.math.BigDecimal; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RevenueByCategoryResponse { + @Schema(description = "Category name", example = "Electronics") + private String category; + @Schema(description = "Total revenue for the category", + example = "12345.67") + private BigDecimal totalRevenue; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java new file mode 100644 index 0000000..fb20cde --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java @@ -0,0 +1,46 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.time.LocalDate; + +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Request parameters for revenue summary") +public class RevenueSummaryRequest { + + @NotNull(message = "Start date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the revenue summary (inclusive)", + example = "2023-01-01", required = true) + private LocalDate startDate; + + @NotNull(message = "End date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the revenue summary (inclusive)", + example = "2023-01-31", required = true) + private LocalDate endDate; + + @NotNull(message = "Period is required") + @Schema(description = "Period granularity for summary", + required = true, implementation = Period.class) + private Period period; + + public enum Period { + DAILY, + WEEKLY, + MONTHLY + } +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java new file mode 100644 index 0000000..74b88cd --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java @@ -0,0 +1,26 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RevenueSummaryResponse { + @Schema(description = "Start date of the period for the revenue summary", + example = "2023-01-01") + private LocalDate periodStartDate; + + @Schema(description = "Total revenue for the specified period", + example = "12345.67") + private BigDecimal totalRevenue; +} + diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java similarity index 80% rename from src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java index 6ef3ec6..2c7a4be 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.courier; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java similarity index 75% rename from src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java index 6bc0973..00933ea 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.customer; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java similarity index 72% rename from src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java index 7d8c399..476b819 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.inventory; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java similarity index 73% rename from src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java index ac2e693..23e73c4 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.inventory; public interface LowStockProductProjection { diff --git a/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java new file mode 100644 index 0000000..9a6c165 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.api.projections.product; + +import java.math.BigDecimal; + +public interface TopSellingProductProjection { + Long getId(); + String getName(); + String getCategory(); + BigDecimal getTotalRevenue(); + Long getTotalUnits(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java new file mode 100644 index 0000000..bee429c --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java @@ -0,0 +1,9 @@ +package com.Podzilla.analytics.api.projections.revenue; + + +import java.math.BigDecimal; + +public interface RevenueByCategoryProjection { + String getCategory(); + BigDecimal getTotalRevenue(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java new file mode 100644 index 0000000..75bf684 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java @@ -0,0 +1,9 @@ +package com.Podzilla.analytics.api.projections.revenue; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public interface RevenueSummaryProjection { + LocalDate getPeriod(); + BigDecimal getTotalRevenue(); +} diff --git a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java index 3d9cc7d..7b49be1 100644 --- a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java +++ b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.config; -import com.Podzilla.analytics.api.dtos.ErrorResponse; -import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; @@ -10,8 +10,8 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; - -import java.time.LocalDateTime; +import com.Podzilla.analytics.api.dtos.ErrorResponse; +import lombok.extern.slf4j.Slf4j; @ControllerAdvice @Slf4j diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index 3da0777..6fdaf48 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; +import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; import com.Podzilla.analytics.models.Courier; public interface CourierRepository extends JpaRepository { diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index d92ba34..79bd7f8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import com.Podzilla.analytics.api.projections.CustomersTopSpendersProjection; +import com.Podzilla.analytics.api.projections.customer.CustomersTopSpendersProjection; import com.Podzilla.analytics.models.Customer; import java.time.LocalDateTime; diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java index 4a2faf0..219a3fc 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java @@ -8,8 +8,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import com.Podzilla.analytics.api.projections.InventoryValueByCategoryProjection; -import com.Podzilla.analytics.api.projections.LowStockProductProjection; +import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; import com.Podzilla.analytics.models.InventorySnapshot; @Repository diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index 3b6aa17..ae6118b 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -1,5 +1,6 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -12,121 +13,153 @@ import com.Podzilla.analytics.api.projections.order.OrderFailureReasonsProjection; import com.Podzilla.analytics.api.projections.order.OrderRegionProjection; import com.Podzilla.analytics.api.projections.order.OrderStatusProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; import com.Podzilla.analytics.models.Order; public interface OrderRepository extends JpaRepository { - @Query(value = "SELECT 'OVERALL' as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " - + "o.shipped_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " - + "AND o.shipped_timestamp IS NOT NULL", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " + + "o.shipped_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " + + "AND o.shipped_timestamp IS NOT NULL", nativeQuery = true) FulfillmentTimeProjection findPlaceToShipTimeOverall( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('RegionID_', o.region_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " - + "o.shipped_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " - + "AND o.shipped_timestamp IS NOT NULL " - + "GROUP BY o.region_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " + + "o.shipped_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " + + "AND o.shipped_timestamp IS NOT NULL " + + "GROUP BY o.region_id", nativeQuery = true) List findPlaceToShipTimeByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - // --- Ship to Deliver Time Projections --- - @Query(value = "SELECT 'OVERALL' as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED'", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED'", nativeQuery = true) FulfillmentTimeProjection findShipToDeliverTimeOverall( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('RegionID_', o.region_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED' " - + "GROUP BY o.region_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED' " + + "GROUP BY o.region_id", nativeQuery = true) List findShipToDeliverTimeByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('CourierID_', o.courier_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED' " - + "GROUP BY o.courier_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED' " + + "GROUP BY o.courier_id", nativeQuery = true) List findShipToDeliverTimeByCourier( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - ////////////// - - @Query(value = "Select o.region_id as regionId, " + @Query(value = "SELECT o.region_id as regionId, " + "r.city as city, " + "r.country as country, " + "count(o.id) as orderCount, " + "avg(o.total_amount) as averageOrderValue " - + "From orders o " - + "inner join regions r on o.region_id = r.id " - + "where o.final_status_timestamp between :startDate and :endDate " - + "Group by o.region_id, r.city, r.country " - + "Order by orderCount desc, averageOrderValue desc", + + "FROM orders o " + + "INNER JOIN regions r on o.region_id = r.id " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "GROUP BY o.region_id, r.city, r.country " + + "ORDER BY orderCount desc, averageOrderValue desc", nativeQuery = true) List findOrdersByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "Select o.status as status, " + @Query(value = "SELECT o.status as status, " + "count(o.id) as count " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate " - + "Group by o.status " - + "Order by count desc", + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "GROUP BY o.status " + + "ORDER BY count desc", nativeQuery = true) List findOrderStatusCounts( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "Select o.failure_reason as reason, " + @Query(value = "SELECT o.failure_reason as reason, " + "count(o.id) as count " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate " - + "and o.status = 'FAILED' " - + "Group by o.failure_reason " - + "Order by count desc", + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "AND o.status = 'FAILED' " + + "GROUP BY o.failure_reason " + + "ORDER BY count desc", nativeQuery = true) List findFailureReasons( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = - "Select (Sum(Case when o.status = 'FAILED' then 1 else 0 end)" + @Query(value = "SELECT(SUM(CASE WHEN o.status = 'FAILED' THEN 1 ELSE 0 END)" + " / (count(*)*1.0) ) as failureRate " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate", - nativeQuery = true) + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate" + + " AND :endDate", nativeQuery = true) OrderFailureRateProjection calculateFailureRate( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); + + @Query(value = "SELECT " + + "t.period, " + + "SUM(t.total_amount) as totalRevenue " + + "FROM ( " + + "SELECT " + + "CASE :reportPeriod " + + "WHEN 'DAILY' THEN CAST(o.order_placed_timestamp AS DATE) " + + "WHEN 'WEEKLY' THEN" + + " date_trunc('week', o.order_placed_timestamp)::date " + + "WHEN 'MONTHLY' THEN" + + " date_trunc('month', o.order_placed_timestamp)::date " + + "END as period, " + + "o.total_amount " + + "FROM orders o " + + "WHERE o.order_placed_timestamp >= :startDate " + + "AND o.order_placed_timestamp < :endDate " + + "AND o.status IN ('COMPLETED') " + + ") t " + + "GROUP BY t.period " + + "ORDER BY t.period", nativeQuery = true) + List findRevenueSummaryByPeriod( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("reportPeriod") String reportPeriod); + + @Query(value = "SELECT " + + "p.category, " + + "SUM(sli.quantity * sli.price_per_unit) as totalRevenue " + + "FROM orders o " + + "JOIN sales_line_items sli ON o.id = sli.order_id " + + "JOIN products p ON sli.product_id = p.id " + + "WHERE o.order_placed_timestamp >= :startDate " + + "AND o.order_placed_timestamp < :endDate " + + "AND o.status IN ('COMPLETED') " + + "GROUP BY p.category " + + "ORDER BY SUM(sli.quantity * sli.price_per_unit) DESC", + nativeQuery = true) + List findRevenueByCategory( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java index 9254be2..425e6c8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -1,8 +1,49 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDateTime; +import java.util.List; + 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.api.projections.product.TopSellingProductProjection; import com.Podzilla.analytics.models.Product; public interface ProductRepository extends JpaRepository { + + // Query to find top-selling products by revenue or units + @Query(value = "SELECT " + + "p.id, " + + "p.name, " + + "p.category, " + + "SUM(sli.quantity * sli.price_per_unit) AS total_revenue, " + + "SUM(sli.quantity) AS total_units " + + "FROM orders o " + + "JOIN sales_line_items sli ON o.id = sli.order_id " + + "JOIN products p ON sli.product_id = p.id " + + "WHERE o.final_status_timestamp >= :startDate " + + "AND o.final_status_timestamp < :endDate " + + "AND o.status = 'COMPLETED' " + + "GROUP BY p.id, p.name, p.category " + + "ORDER BY " + + "CASE :sortBy " + + "WHEN 'REVENUE' THEN SUM(sli.quantity * sli.price_per_unit) " + + "WHEN 'UNITS' THEN SUM(sli.quantity) " + + "ELSE SUM(sli.quantity * sli.price_per_unit) " + + "END DESC, " + + "CASE :sortBy " + + "WHEN 'REVENUE' THEN SUM(sli.quantity) " + + "WHEN 'UNITS' THEN SUM(sli.quantity * sli.price_per_unit) " + + "ELSE SUM(sli.quantity) " + + "END DESC " + + "LIMIT COALESCE(:limit , 10)", +nativeQuery = true) + + List findTopSellers( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("limit") Integer limit, + @Param("sortBy") String sortBy // Pass the enum name as a String + ); } diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 89a9340..9a70a67 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -11,7 +11,7 @@ import com.Podzilla.analytics.api.dtos.courier.CourierDeliveryCountResponse; import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; -import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; +import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.util.MetricCalculator; diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java index e985186..3cb64ba 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -1,11 +1,75 @@ package com.Podzilla.analytics.services; -import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest.SortBy; +import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.repositories.ProductRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class ProductAnalyticsService { + + private final ProductRepository productRepository; + + private static final int DAYS_TO_INCLUDE_END_DATE = 1; + private static final int SUBLIST_START_INDEX = 0; + + /** + * Retrieves the top-selling products within a specified date range. + * + * @param startDate the start date of the range + * @param endDate the end date of the range + * @param limit the maximum number of results to return + * @param sortBy the sorting criteria (units sold or revenue) + * @return a list of top-selling products + */ + public List getTopSellers( + final LocalDate startDate, + final LocalDate endDate, + final Integer limit, + final SortBy sortBy) { + + final String sortByString = sortBy != null ? sortBy.name() + : SortBy.REVENUE.name(); + + final LocalDateTime startDateTime = startDate.atStartOfDay(); + final LocalDateTime endDateTime = endDate + .plusDays(DAYS_TO_INCLUDE_END_DATE).atStartOfDay(); + + final List queryResults = productRepository + .findTopSellers(startDateTime, + endDateTime, + limit, sortByString); + + List topSellersList = new ArrayList<>(); + + for (TopSellingProductProjection row : queryResults) { + BigDecimal value = (sortBy == SortBy.UNITS) + ? BigDecimal.valueOf(row.getTotalUnits()) + : row.getTotalRevenue(); + TopSellerResponse topSellerItem = TopSellerResponse.builder() + .productId(row.getId()) + .productName(row.getName()) + .category(row.getCategory()) + .value(value) + .build(); + + topSellersList.add(topSellerItem); + } + topSellersList.sort((a, b) -> b.getValue().compareTo(a.getValue())); + if (limit != null && limit > 0 && limit < topSellersList.size()) { + topSellersList = topSellersList.subList(SUBLIST_START_INDEX, limit); + } + + return topSellersList; + } } diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java index e8fa494..222a8e2 100644 --- a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -1,11 +1,77 @@ package com.Podzilla.analytics.services; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; +import com.Podzilla.analytics.repositories.OrderRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class RevenueReportService { + + private final OrderRepository orderRepository; + + public List getRevenueSummary( + final LocalDate startDate, + final LocalDate endDate, + final String periodString) { + + final List revenueData = orderRepository + .findRevenueSummaryByPeriod(startDate, + endDate, periodString); + + final List summaryList = new ArrayList<>(); + + for (RevenueSummaryProjection row : revenueData) { + RevenueSummaryResponse summaryItem = RevenueSummaryResponse + .builder() + .periodStartDate(row.getPeriod()) + .totalRevenue(row.getTotalRevenue()) + .build(); + + summaryList.add(summaryItem); + } + + return summaryList; + } + + /** + * Gets completed order revenue summarized by product category + * for a date range. + * + * @param startDate The start date (inclusive). + * @param endDate The end date (exclusive). + * @return A list of revenue summaries per category. + */ + public List getRevenueByCategory( + final LocalDate startDate, final LocalDate endDate) { + + final List queryResults = orderRepository + .findRevenueByCategory(startDate, + endDate); + + final List summaryList = new ArrayList<>(); + + // Each row is [category_string, total_revenue_bigdecimal] + for (RevenueByCategoryProjection row : queryResults) { + RevenueByCategoryResponse summaryItem = RevenueByCategoryResponse + .builder() + .category(row.getCategory()) + .totalRevenue(row.getTotalRevenue()) + .build(); + + summaryList.add(summaryItem); + } + + return summaryList; + } } diff --git a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java index a72a54e..3d87b5f 100644 --- a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java @@ -1,335 +1,335 @@ -package com.Podzilla.analytics.api.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.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -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.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; -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; - -public class FulfillmentReportControllerTest { - - private FulfillmentReportController controller; - private FulfillmentAnalyticsService mockService; - - private LocalDate startDate; - private LocalDate endDate; - private List overallTimeResponses; - private List regionTimeResponses; - private List courierTimeResponses; - - @BeforeEach - public void setup() { - mockService = mock(FulfillmentAnalyticsService.class); - controller = new FulfillmentReportController(mockService); - - startDate = LocalDate.of(2024, 1, 1); - endDate = LocalDate.of(2024, 1, 31); - - // Setup test data - overallTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("OVERALL") - .averageDuration(BigDecimal.valueOf(24.5)) - .build()); - - regionTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("RegionID_1") - .averageDuration(BigDecimal.valueOf(20.2)) - .build(), - FulfillmentTimeResponse.builder() - .groupByValue("RegionID_2") - .averageDuration(BigDecimal.valueOf(28.7)) - .build()); - - courierTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("CourierID_1") - .averageDuration(BigDecimal.valueOf(18.3)) - .build(), - FulfillmentTimeResponse.builder() - .groupByValue("CourierID_2") - .averageDuration(BigDecimal.valueOf(22.1)) - .build()); - } - - @Test - public void testGetPlaceToShipTime_Overall() { - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - 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()); - } - - @Test - public void testGetPlaceToShipTime_ByRegion() { - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_Overall() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_ByRegion() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_ByCourier() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - - // Edge case tests - - @Test - public void testGetPlaceToShipTime_EmptyResponse() { - // Configure mock service to return empty list - when(mockService.getPlaceToShipTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_EmptyResponse() { - // Configure mock service to return empty list - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - - // @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); - - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } - - @Test - public void testGetPlaceToShipTime_SameDayRange() { - // Test same start and end date - LocalDate sameDate = LocalDate.of(2024, 1, 1); - - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_SameDayRange() { - // Test same start and end date - LocalDate sameDate = LocalDate.of(2024, 1, 1); - - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - - @Test - public void testGetPlaceToShipTime_FutureDates() { - // Test future dates - LocalDate futureStart = LocalDate.now().plusDays(1); - LocalDate futureEnd = LocalDate.now().plusDays(30); - - // Configure mock service - should return empty for future dates - when(mockService.getPlaceToShipTimeResponse( - 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()); - } - - @Test - public void testGetShipToDeliverTime_ServiceException() { - // Configure mock service to throw exception - when(mockService.getShipToDeliverTimeResponse( - 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()); - } - } -} \ No newline at end of file +// package com.Podzilla.analytics.api.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.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.mock; +// 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.http.HttpStatus; +// import org.springframework.http.ResponseEntity; + +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +// 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; + +// public class FulfillmentReportControllerTest { + +// private FulfillmentReportController controller; +// private FulfillmentAnalyticsService mockService; + +// private LocalDate startDate; +// private LocalDate endDate; +// private List overallTimeResponses; +// private List regionTimeResponses; +// private List courierTimeResponses; + +// @BeforeEach +// public void setup() { +// mockService = mock(FulfillmentAnalyticsService.class); +// controller = new FulfillmentReportController(mockService); + +// startDate = LocalDate.of(2024, 1, 1); +// endDate = LocalDate.of(2024, 1, 31); + +// // Setup test data +// overallTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("OVERALL") +// .averageDuration(BigDecimal.valueOf(24.5)) +// .build()); + +// regionTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("RegionID_1") +// .averageDuration(BigDecimal.valueOf(20.2)) +// .build(), +// FulfillmentTimeResponse.builder() +// .groupByValue("RegionID_2") +// .averageDuration(BigDecimal.valueOf(28.7)) +// .build()); + +// courierTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("CourierID_1") +// .averageDuration(BigDecimal.valueOf(18.3)) +// .build(), +// FulfillmentTimeResponse.builder() +// .groupByValue("CourierID_2") +// .averageDuration(BigDecimal.valueOf(22.1)) +// .build()); +// } + +// @Test +// public void testGetPlaceToShipTime_Overall() { +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// 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()); +// } + +// @Test +// public void testGetPlaceToShipTime_ByRegion() { +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_Overall() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_ByRegion() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_ByCourier() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } + +// // Edge case tests + +// @Test +// public void testGetPlaceToShipTime_EmptyResponse() { +// // Configure mock service to return empty list +// when(mockService.getPlaceToShipTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_EmptyResponse() { +// // Configure mock service to return empty list +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } + +// // @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); + +// // // Verify response +// // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); +// // } + +// @Test +// public void testGetPlaceToShipTime_SameDayRange() { +// // Test same start and end date +// LocalDate sameDate = LocalDate.of(2024, 1, 1); + +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_SameDayRange() { +// // Test same start and end date +// LocalDate sameDate = LocalDate.of(2024, 1, 1); + +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } + +// @Test +// public void testGetPlaceToShipTime_FutureDates() { +// // Test future dates +// LocalDate futureStart = LocalDate.now().plusDays(1); +// LocalDate futureEnd = LocalDate.now().plusDays(30); + +// // Configure mock service - should return empty for future dates +// when(mockService.getPlaceToShipTimeResponse( +// 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()); +// } + +// @Test +// public void testGetShipToDeliverTime_ServiceException() { +// // Configure mock service to throw exception +// when(mockService.getShipToDeliverTimeResponse( +// 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()); +// } +// } +// } \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java new file mode 100644 index 0000000..b7adcc7 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java @@ -0,0 +1,119 @@ +// package com.Podzilla.analytics.controllers; + +// import java.math.BigDecimal; +// import java.time.LocalDate; +// import java.util.Collections; +// import java.util.List; + +// import static org.hamcrest.Matchers.hasSize; +// import static org.hamcrest.Matchers.is; +// import org.junit.jupiter.api.Test; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.when; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; // Changed from @WebMvcTest +// import org.springframework.boot.test.context.SpringBootTest; // Added +// import org.springframework.http.MediaType; +// import org.springframework.test.context.bean.override.mockito.MockitoBean; +// import org.springframework.test.web.servlet.MockMvc; +// import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +// import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +// import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +// import com.Podzilla.analytics.api.dtos.RevenueSummaryRequest.Period; +// import com.Podzilla.analytics.api.dtos.RevenueSummaryResponse; +// import com.Podzilla.analytics.services.RevenueReportService; + +// // Using @SpringBootTest loads the full application context +// @SpringBootTest +// // @AutoConfigureMockMvc sets up MockMvc to test the web layer within the full context +// @AutoConfigureMockMvc +// class RevenueReportControllerTest { + +// @Autowired +// private MockMvc mockMvc; +// // Keep @MockitoBean to mock the service as per your original test logic +// @MockitoBean +// private RevenueReportService revenueReportService; + +// // Helper method to create a valid URL with parameters +// private String buildSummaryUrl(LocalDate startDate, LocalDate endDate, Period period) { +// return String.format("/revenue/summary?startDate=%s&endDate=%s&period=%s", +// startDate, endDate, period); +// } + +// @Test +// void getRevenueSummary_ValidRequest_ReturnsOkAndSummaryList() throws Exception { +// // Arrange: Define test data and mock service behavior +// LocalDate startDate = LocalDate.of(2023, 1, 1); +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// RevenueSummaryResponse mockResponse = RevenueSummaryResponse.builder() +// .periodStartDate(startDate) +// .totalRevenue(BigDecimal.valueOf(1500.50)) +// .build(); +// List mockSummaryList = Collections.singletonList(mockResponse); + +// // Mock the service call - expect any RevenueSummaryRequest and return the mock list +// when(revenueReportService.getRevenueSummary(any())) +// .thenReturn(mockSummaryList); + +// // Act: Perform the HTTP GET request +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) // Although GET, setting content type is harmless +// .andExpect(status().isOk()) // Assert: Expect HTTP 200 OK +// .andExpect(jsonPath("$", hasSize(1))) // Expect a JSON array with one element +// .andExpect(jsonPath("$[0].periodStartDate", is(startDate.toString()))) // Check response fields +// .andExpect(jsonPath("$[0].totalRevenue", is(1500.50))); +// } + +// @Test +// void getRevenueSummary_MissingStartDate_ReturnsBadRequest() throws Exception { +// // Arrange: Missing startDate parameter +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// // Act & Assert: Perform request and expect HTTP 400 Bad Request due to @NotNull +// mockMvc.perform(get("/revenue/summary?endDate=" + endDate + "&period=" + period) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isBadRequest()); +// // You could add more assertions here to check the response body for validation error details +// } + +// @Test +// void getRevenueSummary_EndDateBeforeStartDate_ReturnsBadRequest() throws Exception { +// // Arrange: Invalid date range (endDate before startDate) - testing @AssertTrue +// LocalDate startDate = LocalDate.of(2023, 1, 31); +// LocalDate endDate = LocalDate.of(2023, 1, 1); // Invalid date range +// Period period = Period.MONTHLY; + +// // Act & Assert: Perform request and expect HTTP 400 Bad Request due to @AssertTrue +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isBadRequest()); +// // Again, check response body for specific validation error message if needed +// } + +// @Test +// void getRevenueSummary_ServiceReturnsEmptyList_ReturnsOkAndEmptyList() throws Exception { +// // Arrange: Service returns an empty list +// LocalDate startDate = LocalDate.of(2023, 1, 1); +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// List mockSummaryList = Collections.emptyList(); + +// when(revenueReportService.getRevenueSummary(any())) +// .thenReturn(mockSummaryList); + +// // Act & Assert: Perform request and expect HTTP 200 OK with an empty JSON array +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", hasSize(0))); // Expect an empty JSON array +// } + +// // Add similar tests for other scenarios: missing parameters, invalid format, etc. +// // And add tests for the /revenue/by-category endpoint here as well. +// } diff --git a/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java new file mode 100644 index 0000000..8e1bd51 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java @@ -0,0 +1,654 @@ +package com.Podzilla.analytics.integration; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.RegionRepository; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; +import com.Podzilla.analytics.services.ProductAnalyticsService; + +import jakarta.transaction.Transactional; + +@SpringBootTest +@Transactional +class ProductAnalyticsServiceIntegrationTest { + + @Autowired + private ProductAnalyticsService productAnalyticsService; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private SalesLineItemRepository salesLineItemRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CourierRepository courierRepository; + + @Autowired + private RegionRepository regionRepository; + + // Class-level test data objects + private Product phone; + private Product laptop; + private Product book; + private Product tablet; + private Product headphones; + + private Customer customer; + private Courier courier; + private Region region; + + private Order order1; // May 1st + private Order order2; // May 2nd + private Order order3; // May 3rd + private Order order4; // May 4th - Failed order + private Order order5; // May 5th - Products with same revenue but different units + private Order order6; // April 30th - Outside default test range + + @BeforeEach + void setUp() { + insertTestData(); + } + + private void insertTestData() { + // Create test products + phone = Product.builder() + .name("Smartphone") + .category("Electronics") + .cost(new BigDecimal("300.00")) + .lowStockThreshold(5) + .build(); + + laptop = Product.builder() + .name("Laptop") + .category("Electronics") + .cost(new BigDecimal("700.00")) + .lowStockThreshold(3) + .build(); + + book = Product.builder() + .name("Programming Book") + .category("Books") + .cost(new BigDecimal("20.00")) + .lowStockThreshold(10) + .build(); + + tablet = Product.builder() + .name("Tablet") + .category("Electronics") + .cost(new BigDecimal("200.00")) + .lowStockThreshold(5) + .build(); + + headphones = Product.builder() + .name("Wireless Headphones") + .category("Audio") + .cost(new BigDecimal("80.00")) + .lowStockThreshold(8) + .build(); + + productRepository.saveAll(List.of(phone, laptop, book, tablet, headphones)); + + // Create required entities for orders + customer = Customer.builder() + .name("Test Customer") + .build(); + customerRepository.save(customer); + + courier = Courier.builder() + .name("Test Courier") + .status(Courier.CourierStatus.ACTIVE) + .build(); + courierRepository.save(courier); + + region = Region.builder() + .city("Test City") + .state("Test State") + .country("Test Country") + .postalCode("12345") + .build(); + regionRepository.save(region); + + // Create orders with different dates and statuses + order1 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 1, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 1, 15, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("2000.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order2 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 2, 11, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 2, 16, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("1500.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order3 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 3, 9, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 3, 14, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("800.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order4 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 4, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 4, 12, 0)) + .status(Order.OrderStatus.FAILED) // Failed order - should be excluded + .failureReason("Payment declined") + .totalAmount(new BigDecimal("1200.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order5 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 5, 14, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 5, 18, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("1000.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + // Order outside of default test date range + order6 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 4, 30, 9, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 4, 30, 15, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("750.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + orderRepository.saveAll(List.of(order1, order2, order3, order4, order5, order6)); + + // Create sales line items with different quantities and prices + // Order 1 - May 1 + SalesLineItem item1_1 = SalesLineItem.builder() + .order(order1) + .product(phone) + .quantity(2) // 2 phones + .pricePerUnit(new BigDecimal("500.00")) // $500 each + .build(); + + SalesLineItem item1_2 = SalesLineItem.builder() + .order(order1) + .product(laptop) + .quantity(1) // 1 laptop + .pricePerUnit(new BigDecimal("1000.00")) // $1000 each + .build(); + + // Order 2 - May 2 + SalesLineItem item2_1 = SalesLineItem.builder() + .order(order2) + .product(phone) + .quantity(3) // 3 phones + .pricePerUnit(new BigDecimal("500.00")) // $500 each + .build(); + + // Order 3 - May 3 + SalesLineItem item3_1 = SalesLineItem.builder() + .order(order3) + .product(book) + .quantity(5) // 5 books + .pricePerUnit(new BigDecimal("40.00")) // $40 each + .build(); + + SalesLineItem item3_2 = SalesLineItem.builder() + .order(order3) + .product(tablet) + .quantity(2) // 2 tablets + .pricePerUnit(new BigDecimal("300.00")) // $300 each + .build(); + + // Order 4 - May 4 (Failed order) + SalesLineItem item4_1 = SalesLineItem.builder() + .order(order4) + .product(laptop) + .quantity(1) // 1 laptop + .pricePerUnit(new BigDecimal("1200.00")) // $1200 each + .build(); + + // Order 5 - May 5 (Same revenue different products) + SalesLineItem item5_1 = SalesLineItem.builder() + .order(order5) + .product(headphones) + .quantity(5) // 5 headphones + .pricePerUnit(new BigDecimal("100.00")) // $100 each = $500 total + .build(); + + SalesLineItem item5_2 = SalesLineItem.builder() + .order(order5) + .product(tablet) + .quantity(1) // 1 tablet + .pricePerUnit(new BigDecimal("500.00")) // $500 each = $500 total (same as headphones) + .build(); + + // Order 6 - April 30 (Outside default range) + SalesLineItem item6_1 = SalesLineItem.builder() + .order(order6) + .product(phone) + .quantity(1) // 1 phone + .pricePerUnit(new BigDecimal("450.00")) // $450 each + .build(); + + SalesLineItem item6_2 = SalesLineItem.builder() + .order(order6) + .product(book) + .quantity(10) // 10 books + .pricePerUnit(new BigDecimal("30.00")) // $30 each + .build(); + + salesLineItemRepository.saveAll(List.of( + item1_1, item1_2, item2_1, item3_1, item3_2, + item4_1, item5_1, item5_2, item6_1, item6_2)); + } + + @Nested + @DisplayName("Basic Functionality Tests") + class BasicFunctionalityTests { + + @Test + @DisplayName("Get top sellers by revenue should return products in correct order") + void getTopSellers_byRevenue_shouldReturnCorrectOrder() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + System.out.println("Results: " + results); + assertThat(results).hasSize(5); // Phone, Laptop, Tablet, Headphones, Book + + // Verify proper ordering by revenue + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("2500.00"); // 5 phones * $500 + assertThat(results.get(1).getProductName()).isEqualTo("Tablet"); + assertThat(results.get(1).getValue()).isEqualByComparingTo("1100.00"); // (2 * $300) + (1 * $500) + assertThat(results.get(2).getProductName()).isEqualTo("Laptop"); + assertThat(results.get(2).getValue()).isEqualByComparingTo("1000.00"); // 1 laptop * $1000 + + assertThat(results.get(3).getProductName()).isEqualTo("Wireless Headphones"); + assertThat(results.get(3).getValue()).isEqualByComparingTo("500.00"); // 5 * $100 + } + + @Test + @DisplayName("Get top sellers by units should return products in correct order") + void getTopSellers_byUnits_shouldReturnCorrectOrder() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.UNITS) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + assertThat(results).hasSize(5); + + // Order by units sold + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("5"); // 2 + 3 phones + assertThat(results.get(1).getProductName()).isEqualTo("Wireless Headphones"); + assertThat(results.get(1).getValue()).isEqualByComparingTo("5"); // 5 headphones + assertThat(results.get(2).getProductName()).isEqualTo("Programming Book"); + assertThat(results.get(2).getValue()).isEqualByComparingTo("5"); // 5 books + + // Check correct tie-breaking behavior + Map orderMap = results.stream() + .collect(Collectors.toMap(TopSellerResponse::getProductName, + r -> r.getValue().intValue())); + + // Assuming tie-breaking is by revenue (which is how the repository query is + // sorted) + assertTrue(orderMap.get("Smartphone") >= orderMap.get("Wireless Headphones")); + assertTrue(orderMap.get("Wireless Headphones") >= orderMap.get("Programming Book")); + } + + @Test + @DisplayName("Get top sellers with limit should respect the limit parameter") + void getTopSellers_withLimit_shouldRespectLimit() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(2) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + // System.out.println("Results:**-*-*-*-**-* " + results); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(1).getProductName()).isEqualTo("Tablet"); + } + + @Test + @DisplayName("Get top sellers with date range should only include orders in range") + void getTopSellers_withDateRange_shouldOnlyIncludeOrdersInRange() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 2)) // Start from May 2nd + .endDate(LocalDate.of(2024, 5, 4)) // End before May 4th + .sortBy(TopSellerRequest.SortBy.REVENUE) + .limit(5) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should have only phone, book, and tablet (from orders 2 and 3) + assertThat(results).hasSize(3); + + // First should be phone with only Order 2 revenue + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("1500.00"); // Only order 2: 3 phones * $500 + + // Should include tablets from order 3 + boolean hasTablet = results.stream() + .anyMatch(r -> r.getProductName().equals("Tablet") + && r.getValue().compareTo(new BigDecimal("600.00")) == 0); + assertThat(hasTablet).isTrue(); + + // Should include books from order 3 + boolean hasBook = results.stream() + .anyMatch(r -> r.getProductName().equals("Programming Book") + && r.getValue().compareTo(new BigDecimal("200.00")) == 0); + assertThat(hasBook).isTrue(); + + // Should NOT include laptop (only in order 1) + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop")); + assertThat(hasLaptop).isFalse(); + } + } + + @Nested + @DisplayName("Edge Case Tests") + class EdgeCaseTests { + + @Test + @DisplayName("Get top sellers with empty result set should return empty list") + void getTopSellers_withNoMatchingData_shouldReturnEmptyList() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 6, 1)) // Future date with no data + .endDate(LocalDate.of(2024, 6, 2)) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .limit(5) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + assertThat(results).isEmpty(); + } + + @Test + @DisplayName("Get top sellers with zero limit should return all results") + void getTopSellers_withZeroLimit_shouldReturnAllResults() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(0) // Zero limit + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should return all 4 products with sales in the period + assertThat(results).hasSize(0); + } + + @Test + @DisplayName("Get top sellers with single day range (inclusive) should work correctly") + void getTopSellers_withSingleDayRange_shouldWorkCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 1)) // End date inclusive + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should only include products from order1 (May 1st) + assertThat(results).hasSize(2); + + // Smartphone should be included + boolean hasPhone = results.stream() + .anyMatch(r -> r.getProductName().equals("Smartphone") + && r.getValue().compareTo(new BigDecimal("1000.00")) == 0); + assertThat(hasPhone).isTrue(); + + // Laptop should be included + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop") + && r.getValue().compareTo(new BigDecimal("1000.00")) == 0); + assertThat(hasLaptop).isTrue(); + } + + @Test + @DisplayName("Get top sellers should exclude failed orders") + void getTopSellers_shouldExcludeFailedOrders() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 4)) // Only include May 4th (failed order day) + .endDate(LocalDate.of(2024, 5, 4)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should be empty because the only order on May 4th was failed + assertThat(results).isEmpty(); + + // Specifically, the laptop from the failed order should not be included + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop")); + assertThat(hasLaptop).isFalse(); + } + + @Test + @DisplayName("Get top sellers including boundary dates should work correctly") + void getTopSellers_withBoundaryDates_shouldWorkCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 4, 30)) // Include April 30 + .endDate(LocalDate.of(2024, 4, 30)) // Exclude May 1 + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should only include products from April 30th (order6) + assertThat(results).hasSize(2); + + // Book should be included + boolean hasBook = results.stream() + .anyMatch(r -> r.getProductName().equals("Programming Book") + && r.getValue().compareTo(new BigDecimal("300.00")) == 0); + assertThat(hasBook).isTrue(); + + // Phone should be included + boolean hasPhone = results.stream() + .anyMatch(r -> r.getProductName().equals("Smartphone") + && r.getValue().compareTo(new BigDecimal("450.00")) == 0); + assertThat(hasPhone).isTrue(); + } + } + + @Nested + @DisplayName("Sorting and Value Tests") + class SortingAndValueTests { + + @Test + @DisplayName("Products with same revenue but different units should sort by revenue first") + void getTopSellers_withSameRevenue_shouldSortCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 5)) // Only include May 5th order + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should have both products with $500 revenue + assertThat(results).hasSize(2); + + // Both should have same revenue value + assertThat(results.get(0).getValue()).isEqualByComparingTo(results.get(1).getValue()); + assertThat(results.get(0).getValue()).isEqualByComparingTo("500.00"); + + // Check units separately to verify the data is correct + // (This doesn't test sorting order, but verifies the test data is as expected) + boolean hasTablet = results.stream() + .anyMatch(r -> r.getProductName().equals("Tablet")); + boolean hasHeadphones = results.stream() + .anyMatch(r -> r.getProductName().equals("Wireless Headphones")); + + assertThat(hasTablet).isTrue(); + assertThat(hasHeadphones).isTrue(); + } + + @Test + @DisplayName("Get top sellers by units with products having same units should respect secondary sort") + void getTopSellers_byUnitsWithSameUnits_shouldRespectSecondarySorting() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .sortBy(TopSellerRequest.SortBy.UNITS).limit(10) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Find all products with 5 units + List productsWithFiveUnits = results.stream() + .filter(r -> r.getValue().compareTo(BigDecimal.valueOf(5)) == 0) + .collect(Collectors.toList()); + + // Should have 3 products with 5 units (phone, headphones, book) + assertThat(productsWithFiveUnits.size()).isEqualTo(3); + + // Verify that secondary sorting works (we expect by revenue) + // Get product names in order + List productOrder = productsWithFiveUnits.stream() + .map(TopSellerResponse::getProductName) + .collect(Collectors.toList()); + + // Expected order: Smartphone ($2500), Headphones ($500), Book ($200) + int smartphoneIdx = productOrder.indexOf("Smartphone"); + int headphonesIdx = productOrder.indexOf("Wireless Headphones"); + int bookIdx = productOrder.indexOf("Programming Book"); + + assertTrue(smartphoneIdx < headphonesIdx, "Smartphone should come before Headphones"); + assertTrue(headphonesIdx < bookIdx, "Headphones should come before Programming Book"); + } + } + + @Nested + @DisplayName("Request Parameter Tests") + class RequestParameterTests { + + @Test + @DisplayName("Get top sellers with swapped date range should handle gracefully") + void getTopSellers_withSwappedDateRange_shouldHandleGracefully() { + // Start date is after end date - test depends on how service handles this + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 6)) // Start after end + .endDate(LocalDate.of(2024, 5, 1)) // End before start + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // If service handles swapped dates, this may return empty result + // or throw an exception + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + // Should return empty list if swapped dates are handled + assertThat(results).isEmpty(); + // If exception is expected, you may need to adjust this test + // assertThrows(IllegalArgumentException.class, () -> + // productAnalyticsService.getTopSellers(request)); + } + } +} diff --git a/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java b/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java new file mode 100644 index 0000000..acab99f --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java @@ -0,0 +1,155 @@ +package com.Podzilla.analytics.integration; + +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.RegionRepository; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; +import com.Podzilla.analytics.services.RevenueReportService; + +import jakarta.transaction.Transactional; +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 java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +public class RevenueReportServiceIntegrationTest { + + @Autowired + private RevenueReportService revenueReportService; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private SalesLineItemRepository salesLineItemRepository; + + @Autowired + private CourierRepository courierRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private RegionRepository regionRepository; + + @BeforeEach + public void setUp() { + insertTestData(); + } + + private void insertTestData() { + // Create and save region + Region region = Region.builder() + .city("Test City") + .state("Test State") + .country("Test Country") + .postalCode("12345") + .build(); + region = regionRepository.save(region); + + // Create courier + Courier courier = Courier.builder() + .name("Test Courier") + .status(Courier.CourierStatus.ACTIVE) + .build(); + courier = courierRepository.save(courier); + + // Create customer + Customer customer = Customer.builder() + .name("Test Customer") + .build(); + customer = customerRepository.save(customer); + + // Create products + Product product1 = Product.builder() + .name("Phone Case") + .category("Accessories") + .build(); + + Product product2 = Product.builder() + .name("Wireless Mouse") + .category("Electronics") + .build(); + + productRepository.saveAll(List.of(product1, product2)); + + // Create order with all required relationships + Order order1 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 1, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 1, 11, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("100.00")) + .courier(courier) + .customer(customer) + .region(region) + .build(); + + orderRepository.save(order1); + + SalesLineItem item1 = SalesLineItem.builder() + .order(order1) + .product(product1) + .quantity(2) + .pricePerUnit(new BigDecimal("10.00")) + .build(); + + SalesLineItem item2 = SalesLineItem.builder() + .order(order1) + .product(product2) + .quantity(1) + .pricePerUnit(new BigDecimal("80.00")) + .build(); + + salesLineItemRepository.saveAll(List.of(item1, item2)); + } + + @Test + public void getRevenueByCategory_shouldReturnExpectedResults() { + List results = revenueReportService.getRevenueByCategory( + LocalDate.of(2024, 5, 1), + LocalDate.of(2024, 5, 3) + ); + + assertThat(results).isNotEmpty(); + assertThat(results.get(0).getCategory()).isEqualTo("Electronics"); + } + + @Test + public void getRevenueSummary_shouldReturnExpectedResults() { + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 3)) + .period(RevenueSummaryRequest.Period.DAILY) + .build(); + + List summary = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), + request.getPeriod().name()); + + assertThat(summary).isNotEmpty(); + assertThat(summary.get(0).getTotalRevenue()).isEqualByComparingTo("100.00"); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java new file mode 100644 index 0000000..fb2b5ee --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java @@ -0,0 +1,229 @@ +package com.Podzilla.analytics.services; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; // Keep import if TopSellerRequest still uses LocalDate + +import static org.junit.jupiter.api.Assertions.assertEquals; // Import LocalDateTime +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; +import com.Podzilla.analytics.repositories.ProductRepository; + +@ExtendWith(MockitoExtension.class) +class ProductAnalyticsServiceTest { + + @Mock + private ProductRepository productRepository; + + private ProductAnalyticsService productAnalyticsService; + + @BeforeEach + void setUp() { + productAnalyticsService = new ProductAnalyticsService(productRepository); + } + + @Test + void getTopSellers_SortByRevenue_ShouldReturnCorrectList() { + // Arrange + // Assuming TopSellerRequest still uses LocalDate for input + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(2) // Ensure limit is set to 2 + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + // Start of the start day + LocalDateTime startDate = requestStartDate.atStartOfDay(); + // Start of the day AFTER the end day to include the whole end day in the query + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + // Mocking the repository to return 2 projections + List projections = Arrays.asList( + createProjection(1L, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(2L, "MacBook", "Electronics", new BigDecimal("2000.00"), 2L) + ); + + // Ensure the mock returns the correct results based on the given arguments + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(2), + eq("REVENUE"))) + .thenReturn(projections); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Log the result to help with debugging + result.forEach(item -> System.out.println("Product ID: " + item.getProductId() + " Revenue: " + item.getValue())); + + // Assert (Ensure the order is correct as per revenue) + assertEquals(2, result.size(), "Expected 2 products in the list."); + assertEquals(2L, result.get(0).getProductId()); // MacBook should come first due to higher revenue + assertEquals("MacBook", result.get(0).getProductName()); + assertEquals("Electronics", result.get(0).getCategory()); + assertEquals(new BigDecimal("2000.00"), result.get(0).getValue()); + + assertEquals(1L, result.get(1).getProductId()); + assertEquals("iPhone", result.get(1).getProductName()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("1000.00"), result.get(1).getValue()); + } + + + @Test + void getTopSellers_SortByUnits_ShouldReturnCorrectList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(2) + .sortBy(TopSellerRequest.SortBy.UNITS) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + LocalDateTime startDate = requestStartDate.atStartOfDay(); + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + List projections = Arrays.asList( + createProjection(1L, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(2L, "MacBook", "Electronics", new BigDecimal("2000.00"), 2L) + ); + + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(2), + eq("UNITS"))) + .thenReturn(projections); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert (Ensure the order is correct as per units) + assertEquals(2, result.size()); + assertEquals(1L, result.get(0).getProductId()); // iPhone comes first because of more units sold + assertEquals("iPhone", result.get(0).getProductName()); + assertEquals("Electronics", result.get(0).getCategory()); + // Note: The projection returns revenue and units as BigDecimal and Long respectively. + // The conversion to TopSellerResponse seems to put units into the 'value' field for this case. + assertEquals(new BigDecimal("5"), result.get(0).getValue()); + + + assertEquals(2L, result.get(1).getProductId()); + assertEquals("MacBook", result.get(1).getProductName()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("2"), result.get(1).getValue()); + } + + @Test + void getTopSellers_WithEmptyResult_ShouldReturnEmptyList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(10) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Use any() matchers for LocalDateTime parameters + when(productRepository.findTopSellers(any(LocalDateTime.class), any(LocalDateTime.class), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert + assertTrue(result.isEmpty()); + } + + + @Test + void getTopSellers_WithZeroLimit_ShouldReturnEmptyList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(0) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + LocalDateTime startDate = requestStartDate.atStartOfDay(); + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(0), + eq("REVENUE"))) + .thenReturn(Collections.emptyList()); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert + assertTrue(result.isEmpty()); + } + + private TopSellingProductProjection createProjection( + final Long id, + final String name, + final String category, + final BigDecimal revenue, + final Long units) { + return new TopSellingProductProjection() { + @Override + public Long getId() { + return id; + } + @Override + public String getName() { + return name; + } + @Override + public String getCategory() { + return category; + } + @Override + public BigDecimal getTotalRevenue() { + return revenue; + } + @Override + public Long getTotalUnits() { + return units; + } + }; + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java new file mode 100644 index 0000000..c578ea0 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java @@ -0,0 +1,211 @@ +package com.Podzilla.analytics.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; + +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.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; +import com.Podzilla.analytics.repositories.OrderRepository; + +@ExtendWith(MockitoExtension.class) +class RevenueReportServiceTest { + + @Mock + private OrderRepository orderRepository; + + private RevenueReportService revenueReportService; + + @BeforeEach + void setUp() { + revenueReportService = new RevenueReportService(orderRepository); + } + + @Test + void getRevenueSummary_WithValidData_ShouldReturnCorrectSummary() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + List projections = Arrays.asList( + summaryProjection(LocalDate.of(2025, 1, 1), new BigDecimal("1000.00")), + summaryProjection(LocalDate.of(2025, 2, 1), new BigDecimal("2000.00")) + ); + + when(orderRepository.findRevenueSummaryByPeriod(eq(startDate), eq(endDate), eq("MONTHLY"))) + .thenReturn(projections); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertEquals(2, result.size()); + assertEquals(LocalDate.of(2025, 1, 1), result.get(0).getPeriodStartDate()); + assertEquals(new BigDecimal("1000.00"), result.get(0).getTotalRevenue()); + assertEquals(LocalDate.of(2025, 2, 1), result.get(1).getPeriodStartDate()); + assertEquals(new BigDecimal("2000.00"), result.get(1).getTotalRevenue()); + } + + @Test + void getRevenueSummary_WithEmptyData_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueSummary_WithStartDateAfterEndDate_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 12, 31); + LocalDate endDate = LocalDate.of(2025, 1, 1); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueByCategory_WithValidData_ShouldReturnCorrectCategories() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); List projections = Arrays.asList( + categoryProjection("Books", new BigDecimal("3000.00")), + categoryProjection("Electronics", new BigDecimal("5000.00")) + ); + + when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + .thenReturn(projections);// Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertEquals(2, result.size()); + assertEquals("Books", result.get(0).getCategory()); + assertEquals(new BigDecimal("3000.00"), result.get(0).getTotalRevenue()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("5000.00"), result.get(1).getTotalRevenue()); + } + + @Test + void getRevenueByCategory_WithEmptyData_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + + when(orderRepository.findRevenueByCategory(any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueByCategory_WithNullRevenue_ShouldHandleGracefully() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + + List projections = Arrays.asList( + new RevenueByCategoryProjection() { + @Override + public String getCategory() { + return "Electronics"; + } + @Override + public BigDecimal getTotalRevenue() { + return null; + } + } + ); + + when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + .thenReturn(projections); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertEquals(1, result.size()); + assertEquals("Electronics", result.get(0).getCategory()); + assertNull(result.get(0).getTotalRevenue()); + } + + @Test + void getRevenueByCategory_WithStartDateAfterEndDate_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 12, 31); + LocalDate endDate = LocalDate.of(2025, 1, 1); + + when(orderRepository.findRevenueByCategory(any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertTrue(result.isEmpty()); + } + private RevenueSummaryProjection summaryProjection(LocalDate date, BigDecimal revenue) { + return new RevenueSummaryProjection() { + public LocalDate getPeriod() { return date; } + public BigDecimal getTotalRevenue() { return revenue; } + }; +} + + private RevenueByCategoryProjection categoryProjection(String category, BigDecimal revenue) { + return new RevenueByCategoryProjection() { + public String getCategory() { return category; } + public BigDecimal getTotalRevenue() { return revenue; } + }; + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..f03dd4c --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,15 @@ +# Test Database Configuration +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 +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# H2 Console (optional, for debugging) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console