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