diff --git a/pom.xml b/pom.xml index ef1d43b..299c965 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ com.github.Podzilla podzilla-utils-lib - v1.1.6 + v1.1.12 diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java index 417f46a..8c167f7 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -34,6 +34,7 @@ public List getTopSpenders( request.getStartDate(), request.getEndDate(), request.getPage(), - request.getSize()); + request.getSize() + ); } } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java index 600f13e..b7fc570 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -1,15 +1,10 @@ package com.Podzilla.analytics.api.controllers; import org.springframework.http.ResponseEntity; -// import org.springframework.web.bind.MethodArgumentNotValidException; -// import org.springframework.web.bind.MissingServletRequestParameterException; -// import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -// import org.springframework.web.method.annotation. -// MethodArgumentTypeMismatchException; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; @@ -44,7 +39,8 @@ public ResponseEntity> getPlaceToShipTime( fulfillmentAnalyticsService.getPlaceToShipTimeResponse( req.getStartDate(), req.getEndDate(), - req.getGroupBy()); + req.getGroupBy() + ); return ResponseEntity.ok(reportData); } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java index e0a11c9..8820458 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -31,7 +31,8 @@ public class InventoryReportController { + "the total value of inventory " + "grouped by product categories") @GetMapping("/value/by-category") - public List getInventoryValueByCategor() { + public List + getInventoryValueByCategory() { return inventoryAnalyticsService.getInventoryValueByCategory(); } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java index cf0a7ae..4e81ea2 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -11,8 +11,6 @@ import com.Podzilla.analytics.services.ProfitAnalyticsService; import io.swagger.v3.oas.annotations.Operation; -// import io.swagger.v3.oas.annotations.responses.ApiResponse; -// import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RabbitTesterController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RabbitTesterController.java new file mode 100644 index 0000000..0e3dd5e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RabbitTesterController.java @@ -0,0 +1,163 @@ +package com.Podzilla.analytics.api.controllers; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.Podzilla.analytics.messaging.AnalyticsRabbitListener; +import com.podzilla.mq.events.BaseEvent; +import com.podzilla.mq.events.ConfirmationType; +import com.podzilla.mq.events.ProductSnapshot; +import com.podzilla.mq.events.WarehouseOrderFulfillmentFailedEvent; +import com.podzilla.mq.events.CourierRegisteredEvent; +import com.podzilla.mq.events.CustomerRegisteredEvent; +import com.podzilla.mq.events.DeliveryAddress; +import com.podzilla.mq.events.InventoryUpdatedEvent; +import com.podzilla.mq.events.OrderAssignedToCourierEvent; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderDeliveredEvent; +import com.podzilla.mq.events.OrderDeliveryFailedEvent; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; +import com.podzilla.mq.events.OrderPlacedEvent; +import com.podzilla.mq.events.ProductCreatedEvent; + +import java.util.ArrayList; +@RestController +@RequestMapping("/rabbit-tester") +public class RabbitTesterController { + + static final int QUANTITY = 5; + @Autowired + private AnalyticsRabbitListener listener; + + @GetMapping("/courier-registered-event") + public void testCourierRegisteredEvent() { + BaseEvent event = new CourierRegisteredEvent( + "87f23fee-2e09-4331-bc9c-912045ef0832", + "ahmad the courier", "010"); + listener.handleUserEvents(event); + } + + @GetMapping("/customer-registered-event") + public void testCustomerRegisteredEvent() { + BaseEvent event = new CustomerRegisteredEvent( + "27f7f5ca-6729-461e-882a-0c5123889bec", + "7amada"); + listener.handleUserEvents(event); + } + + @GetMapping("/order-assigned-to-courier-event") + public void testOrderAssignedToCourierEvent( + @RequestParam final String orderId, + @RequestParam final String courierId + ) { + BaseEvent event = new OrderAssignedToCourierEvent( + orderId, + courierId, + new BigDecimal("10.0"), 0.0, 0.0, "signature", + ConfirmationType.QR_CODE); + listener.handleOrderEvents(event); + } + + @GetMapping("/order-cancelled-event") + public void testOrderCancelledEvent( + @RequestParam final String orderId + ) { + BaseEvent event = new OrderCancelledEvent( + orderId, + "2", // customerId (not used in the event) + "rabbit reason", + new ArrayList<>() + ); + listener.handleOrderEvents(event); + } + + @GetMapping("/order-delivered-event") + public void testOrderDeliveredEvent( + @RequestParam final String orderId + ) { + BaseEvent event = new OrderDeliveredEvent( + orderId, "2", + new BigDecimal("4.73")); + listener.handleOrderEvents(event); + } + + @GetMapping("/order-delivery-failed-event") + public void testOrderDeliveryFailedEvent( + @RequestParam final String orderId + ) { + BaseEvent event = new OrderDeliveryFailedEvent( + orderId, "the rabit delivery failed reason", "2"); + listener.handleOrderEvents(event); + } + + @GetMapping("/order-out-for-delivery-event") + public void testOrderOutForDeliveryEvent( + @RequestParam final String orderId + ) { + BaseEvent event = new OrderOutForDeliveryEvent( + orderId, "2"); + listener.handleOrderEvents(event); + } + @GetMapping("/order-fulfillment-failed-event") + public void testOrderFailedToFulfill( + @RequestParam final String orderId + ) { + BaseEvent event = new WarehouseOrderFulfillmentFailedEvent( + orderId, "order fulfillment failed rabbit reason"); + listener.handleOrderEvents(event); + } + + @GetMapping("/order-placed-event") + public void testOrderPlacedEvent( + @RequestParam final String customerId, + @RequestParam final String productId1, + @RequestParam final String productId2 +) { + BaseEvent event = new OrderPlacedEvent( + "a1aa7c7d-fe6a-491f-a2cc-b3b923340777", + customerId, + Arrays.asList( + new com.podzilla.mq.events.OrderItem(productId1, + QUANTITY, new BigDecimal("8.5")), + new com.podzilla.mq.events.OrderItem(productId2, + QUANTITY, new BigDecimal("12.75")) + ), + new DeliveryAddress( + "rabbit street", + "rabbit city wallahy", + "some state", + "some country", + "some postal code"), + new BigDecimal("13290.0"), 0.0, 0.0, "signature", + ConfirmationType.QR_CODE); + listener.handleOrderEvents(event); + } + + @GetMapping("inventory-updated-event") + public void testInventoryUpdatedEvent( + @RequestParam final String productId, + @RequestParam final Integer quantity) { + BaseEvent event = new InventoryUpdatedEvent( + List.of(new ProductSnapshot(productId, quantity))); + listener.handleInventoryEvents(event); + } + + @GetMapping("product-created-event") + public void testProductCreatedEvent() { + BaseEvent event = new ProductCreatedEvent( + "f12afb47-ad23-4ca8-a162-8b12de7a5e49", + "the rabbit product", + "some category", + new BigDecimal("10.0"), + Integer.valueOf(1)); + listener.handleInventoryEvents(event); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java index 580803e..7bede34 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java @@ -1,8 +1,10 @@ package com.Podzilla.analytics.api.dtos; -import java.time.LocalDateTime; -import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDate; + import com.Podzilla.analytics.validation.annotations.ValidDateRange; +import com.Podzilla.analytics.validation.annotations.ValidPagination; + import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; @@ -10,21 +12,21 @@ import io.swagger.v3.oas.annotations.media.Schema; @ValidDateRange +@ValidPagination @Getter @AllArgsConstructor -public class DateRangePaginationRequest { +public class DateRangePaginationRequest + implements IDateRangeRequest, IPaginationRequest { @NotNull(message = "startDate is required") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - @Schema(description = "Start date and time of the range " - + "(inclusive)", example = "2024-01-01T00:00:00", required = true) - private LocalDateTime startDate; + @Schema(description = "Start date of the range " + + "(inclusive)", example = "2024-01-01", required = true) + private LocalDate startDate; @NotNull(message = "endDate is required") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) - @Schema(description = "End date and time of the range " - + "(inclusive)", example = "2024-01-31T23:59:59", required = true) - private LocalDateTime endDate; + @Schema(description = "End date of the range " + + "(inclusive)", example = "2024-01-31", required = true) + private LocalDate endDate; @Min(value = 0, message = "Page " + "number must be greater than or equal to 0") diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java index 084b895..586c69f 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java @@ -14,7 +14,7 @@ @ValidDateRange @Getter @AllArgsConstructor -public class DateRangeRequest { +public class DateRangeRequest implements IDateRangeRequest { @NotNull(message = "startDate is required") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/IDateRangeRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/IDateRangeRequest.java new file mode 100644 index 0000000..114cbd8 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/IDateRangeRequest.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.api.dtos; +import java.time.LocalDate; + + +public interface IDateRangeRequest { + LocalDate getStartDate(); + LocalDate getEndDate(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/IPaginationRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/IPaginationRequest.java new file mode 100644 index 0000000..52655c4 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/IPaginationRequest.java @@ -0,0 +1,6 @@ +package com.Podzilla.analytics.api.dtos; + +public interface IPaginationRequest { + int getPage(); + int getSize(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java index fc650e8..8cb6f79 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java @@ -2,11 +2,15 @@ import jakarta.validation.constraints.Min; import lombok.AllArgsConstructor; import lombok.Getter; + +import com.Podzilla.analytics.validation.annotations.ValidPagination; + import io.swagger.v3.oas.annotations.media.Schema; +@ValidPagination @Getter @AllArgsConstructor -public class PaginationRequest { +public class PaginationRequest implements IPaginationRequest { @Min(value = 0, message = "Page number " + "must be greater than or equal to 0") diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java index e3bce5d..3a3b506 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java @@ -3,22 +3,57 @@ import java.math.BigDecimal; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; -import lombok.Builder; +// import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; @Data -@Builder +// @Builder @NoArgsConstructor @AllArgsConstructor public class CourierAverageRatingResponse { @Schema(description = "ID of the courier", example = "101") - private Long courierId; + private UUID courierId; @Schema(description = "Full name of the courier", example = "John Doe") private String courierName; @Schema(description = "Average rating of the courier", example = "4.6") private BigDecimal averageRating; + + public static Builder builder() { + return new Builder(); + } + public static class Builder { + private UUID courierId; + private String courierName; + private BigDecimal averageRating; + + public Builder() { } + + public Builder courierId(final UUID courierId) { + this.courierId = courierId; + return this; + } + + public Builder courierName(final String courierName) { + this.courierName = courierName; + return this; + } + + public Builder averageRating(final BigDecimal averageRating) { + this.averageRating = averageRating; + return this; + } + + public CourierAverageRatingResponse build() { + return new CourierAverageRatingResponse( + courierId, + courierName, + averageRating + ); + } + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java index 1aa5e88..ebe79b6 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java @@ -5,6 +5,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; @Data @Builder @@ -13,7 +14,7 @@ public class CourierDeliveryCountResponse { @Schema(description = "ID of the courier", example = "101") - private Long courierId; + private UUID courierId; @Schema(description = "Full name of the courier", example = "Jane Smith") private String courierName; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java index 9b91740..cfa58fc 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java @@ -8,7 +8,7 @@ import lombok.NoArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema; - +import java.util.UUID; @Data @Builder @NoArgsConstructor @@ -16,7 +16,7 @@ public class CourierPerformanceReportResponse { @Schema(description = "ID of the courier", example = "105") - private Long courierId; + private UUID courierId; @Schema(description = "Full name of the courier", example = "Ali Hassan") private String courierName; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java index ffa0758..2c8a8fa 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java @@ -6,6 +6,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; @Data @Builder @@ -14,7 +15,7 @@ public class CourierSuccessRateResponse { @Schema(description = "ID of the courier", example = "103") - private Long courierId; + private UUID courierId; @Schema(description = "Full name of the courier", example = "Fatima Ahmed") private String courierName; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java index dd2a4db..e427be1 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java @@ -5,13 +5,14 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +import java.util.UUID; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CustomersTopSpendersResponse { - private Long customerId; + private UUID customerId; private String customerName; private BigDecimal totalSpending; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java index 1a2bd8f..7ccc44d 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java @@ -4,6 +4,7 @@ import org.springframework.format.annotation.DateTimeFormat; +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import com.Podzilla.analytics.validation.annotations.ValidDateRange; import io.swagger.v3.oas.annotations.media.Schema; @@ -16,7 +17,7 @@ @NoArgsConstructor @AllArgsConstructor @ValidDateRange -public class FulfillmentPlaceToShipRequest { +public class FulfillmentPlaceToShipRequest implements IDateRangeRequest { /** * Enum for grouping options in place-to-ship analytics. diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java index c87b2aa..f74755f 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java @@ -5,6 +5,7 @@ import org.springframework.format.annotation.DateTimeFormat; import com.Podzilla.analytics.validation.annotations.ValidDateRange; +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; @@ -20,7 +21,7 @@ @NoArgsConstructor @AllArgsConstructor @ValidDateRange -public class FulfillmentShipToDeliverRequest { +public class FulfillmentShipToDeliverRequest implements IDateRangeRequest { /** * Enum for grouping options in ship-to-deliver analytics. diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java index aa1596f..d4715cc 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java @@ -4,13 +4,14 @@ import lombok.Data; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; +import java.util.UUID; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class LowStockProductResponse { - private Long productId; + private UUID productId; private String productName; private Long currentQuantity; private Long threshold; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java index 4069630..fc924fc 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java @@ -6,7 +6,7 @@ import java.math.BigDecimal; import io.swagger.v3.oas.annotations.media.Schema; - +import java.util.UUID; @Data @Builder @@ -14,8 +14,9 @@ @AllArgsConstructor public class OrderRegionResponse { - @Schema(description = "Region ID", example = "12345") - private Long regionId; + @Schema(description = "Region ID", + example = "4731e9e0-c627-43f9-808a-7e8637abb912") + private UUID regionId; @Schema(description = "city name", example = "Metropolis") private String city; 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 index 582737b..db46713 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java @@ -5,7 +5,9 @@ import org.jetbrains.annotations.NotNull; import org.springframework.format.annotation.DateTimeFormat; + import com.Podzilla.analytics.validation.annotations.ValidDateRange; +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Positive; @@ -19,7 +21,7 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class TopSellerRequest { +public class TopSellerRequest implements IDateRangeRequest { @NotNull @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) @Schema(description = "Start date for the report (inclusive)", 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 index 18e38fe..46d5ae4 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java @@ -3,23 +3,66 @@ import java.math.BigDecimal; import lombok.AllArgsConstructor; -import lombok.Builder; +// import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema; - +import java.util.UUID; @Data @NoArgsConstructor @AllArgsConstructor -@Builder +// @Builder public class TopSellerResponse { - @Schema(description = "Product ID", example = "101") - private Long productId; + @Schema( + description = "Product ID", + example = "550e8400-e29b-41d4-a716-446655440000" + ) + private UUID 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; + + public static Builder builder() { + return new Builder(); + } + public static class Builder { + private UUID productId; + private String productName; + private String category; + private BigDecimal value; + + public Builder productId(final UUID productId) { + this.productId = productId; + return this; + } + + public Builder productName(final String productName) { + this.productName = productName; + return this; + } + + public Builder category(final String category) { + this.category = category; + return this; + } + + public Builder value(final BigDecimal value) { + this.value = value; + return this; + } + + public TopSellerResponse build() { + return new TopSellerResponse( + productId, + productName, + category, + 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 index 6eaf06e..0d558d1 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java @@ -5,6 +5,7 @@ import org.springframework.format.annotation.DateTimeFormat; import com.Podzilla.analytics.validation.annotations.ValidDateRange; +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; @@ -18,7 +19,7 @@ @AllArgsConstructor @Builder @Schema(description = "Request parameters for fetching revenue by category") -public class RevenueByCategoryRequest { +public class RevenueByCategoryRequest implements IDateRangeRequest { @NotNull(message = "Start date is required") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 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 index fb20cde..094fac5 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java @@ -5,6 +5,7 @@ import jakarta.validation.constraints.NotNull; import org.springframework.format.annotation.DateTimeFormat; +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import com.Podzilla.analytics.validation.annotations.ValidDateRange; import io.swagger.v3.oas.annotations.media.Schema; @@ -19,7 +20,7 @@ @AllArgsConstructor @Builder @Schema(description = "Request parameters for revenue summary") -public class RevenueSummaryRequest { +public class RevenueSummaryRequest implements IDateRangeRequest { @NotNull(message = "Start date is required") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) 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 index 74b88cd..227ef85 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java @@ -4,7 +4,7 @@ import java.time.LocalDate; import lombok.AllArgsConstructor; -import lombok.Builder; +// import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @@ -13,7 +13,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -@Builder +// @Builder public class RevenueSummaryResponse { @Schema(description = "Start date of the period for the revenue summary", example = "2023-01-01") @@ -22,5 +22,29 @@ public class RevenueSummaryResponse { @Schema(description = "Total revenue for the specified period", example = "12345.67") private BigDecimal totalRevenue; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private LocalDate periodStartDate; + private BigDecimal totalRevenue; + public Builder() { } + + public Builder periodStartDate(final LocalDate periodStartDate) { + this.periodStartDate = periodStartDate; + return this; + } + + public Builder totalRevenue(final BigDecimal totalRevenue) { + this.totalRevenue = totalRevenue; + return this; + } + + public RevenueSummaryResponse build() { + return new RevenueSummaryResponse(periodStartDate, totalRevenue); + } + } } diff --git a/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java index 2c7a4be..904176f 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java @@ -1,9 +1,10 @@ package com.Podzilla.analytics.api.projections.courier; import java.math.BigDecimal; +import java.util.UUID; public interface CourierPerformanceProjection { - Long getCourierId(); + UUID getCourierId(); String getCourierName(); diff --git a/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java index 00933ea..27da2a7 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java @@ -1,9 +1,10 @@ package com.Podzilla.analytics.api.projections.customer; import java.math.BigDecimal; +import java.util.UUID; public interface CustomersTopSpendersProjection { - Long getCustomerId(); + UUID getCustomerId(); String getCustomerName(); diff --git a/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java index 23e73c4..afd24f1 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java @@ -1,8 +1,8 @@ package com.Podzilla.analytics.api.projections.inventory; - +import java.util.UUID; public interface LowStockProductProjection { - Long getProductId(); + UUID getProductId(); String getProductName(); diff --git a/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java index 8b7816f..b15e7c3 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java @@ -1,9 +1,10 @@ package com.Podzilla.analytics.api.projections.order; import java.math.BigDecimal; +import java.util.UUID; public interface OrderRegionProjection { - Long getRegionId(); + UUID getRegionId(); String getCity(); String getCountry(); Long getOrderCount(); 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 index 9a6c165..184fee1 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java @@ -1,9 +1,10 @@ package com.Podzilla.analytics.api.projections.product; import java.math.BigDecimal; +import java.util.UUID; public interface TopSellingProductProjection { - Long getId(); + UUID getId(); String getName(); String getCategory(); BigDecimal getTotalRevenue(); diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 632b1f0..0015048 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -2,14 +2,14 @@ import com.Podzilla.analytics.models.Courier; import com.Podzilla.analytics.models.Customer; -import com.Podzilla.analytics.models.InventorySnapshot; import com.Podzilla.analytics.models.Order; import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.ProductSnapshot; import com.Podzilla.analytics.models.Region; -import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.models.OrderItem; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.repositories.CustomerRepository; -import com.Podzilla.analytics.repositories.InventorySnapshotRepository; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; import com.Podzilla.analytics.repositories.OrderRepository; import com.Podzilla.analytics.repositories.ProductRepository; import com.Podzilla.analytics.repositories.RegionRepository; @@ -24,7 +24,7 @@ import java.util.Arrays; import java.util.List; import java.util.Random; - +import java.util.UUID; @Component @RequiredArgsConstructor @@ -35,7 +35,7 @@ public class DatabaseSeeder implements CommandLineRunner { private final ProductRepository productRepository; private final RegionRepository regionRepository; private final OrderRepository orderRepository; - private final InventorySnapshotRepository inventorySnapshotRepository; + private final ProductSnapshotRepository productSnapshotRepository; private final Random random = new Random(); private static final int LOW_STOCK_PROD1 = 10; @@ -122,24 +122,30 @@ public void run(final String... args) { System.out.println("Seeded Orders: " + orderRepository.count()); System.out.println("Seeding Inventory Snapshots..."); - seedInventorySnapshots(products); - System.out.println("Seeded Inventory Snapshots: " - + inventorySnapshotRepository.count()); + seedProductSnapshots(products); + System.out.println("Seeded Product Snapshots: " + + productSnapshotRepository.count()); System.out.println("Database seeding finished."); } private List seedRegions() { Region region1 = regionRepository.save( - Region.builder().city("Metropolis").state("NY") + Region.builder() + // .id(UUID.randomUUID()) + .city("Metropolis").state("NY") .country("USA").postalCode("10001") .build()); Region region2 = regionRepository.save( - Region.builder().city("Gotham").state("NJ") + Region.builder() + // .id(UUID.randomUUID()) + .city("Gotham").state("NJ") .country("USA").postalCode("07001") .build()); Region region3 = regionRepository.save( - Region.builder().city("Star City").state("CA") + Region.builder() + // .id(UUID.randomUUID()) + .city("Star City").state("CA") .country("USA").postalCode("90210") .build()); return Arrays.asList(region1, region2, region3); @@ -147,18 +153,22 @@ private List seedRegions() { private List seedProducts() { Product prod1 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) .name("Podzilla Pro").category("Electronics") .cost(PRICE_PROD1) .lowStockThreshold(LOW_STOCK_PROD1).build()); Product prod2 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) .name("Podzilla Mini").category("Electronics") .cost(PRICE_PROD2) .lowStockThreshold(LOW_STOCK_PROD2).build()); Product prod3 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) .name("Charging Case").category("Accessories") .cost(PRICE_PROD3) .lowStockThreshold(LOW_STOCK_PROD3).build()); Product prod4 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) .name("Podzilla Cover").category("Accessories") .cost(PRICE_PROD4) .lowStockThreshold(LOW_STOCK_PROD4).build()); @@ -167,24 +177,33 @@ private List seedProducts() { private List seedCouriers() { Courier courier1 = courierRepository.save( - Courier.builder().name("Speedy Delivery Inc.") - .status(Courier.CourierStatus.ACTIVE).build()); + Courier.builder() + .id(UUID.randomUUID()) + .name("Speedy Delivery Inc.").build()); Courier courier2 = courierRepository.save( - Courier.builder().name("Reliable Couriers Co.") - .status(Courier.CourierStatus.ACTIVE).build()); + Courier.builder() + .id(UUID.randomUUID()) + .name("Reliable Couriers Co.").build()); Courier courier3 = courierRepository.save( - Courier.builder().name("Overnight Express") - .status(Courier.CourierStatus.INACTIVE).build()); + Courier.builder() + .id(UUID.randomUUID()) + .name("Overnight Express").build()); return Arrays.asList(courier1, courier2, courier3); } private List seedCustomers() { Customer cust1 = customerRepository.save( - Customer.builder().name("Alice Smith").build()); + Customer.builder() + .id(UUID.randomUUID()) + .name("Alice Smith").build()); Customer cust2 = customerRepository.save( - Customer.builder().name("Bob Johnson").build()); + Customer.builder() + .id(UUID.randomUUID()) + .name("Bob Johnson").build()); Customer cust3 = customerRepository.save( - Customer.builder().name("Charlie Brown").build()); + Customer.builder() + .id(UUID.randomUUID()) + .name("Charlie Brown").build()); return Arrays.asList(cust1, cust2, cust3); } @@ -199,9 +218,10 @@ private void seedOrders( LocalDateTime placed1 = today.minusDays(ORDER_1_DAYS_PRIOR) .atTime(ORDER_1_HOUR, ORDER_1_MINUTE); Order order1 = Order.builder() + .id(UUID.randomUUID()) .customer(customers.get(0)).courier(couriers.get(0)) .region(regions.get(0)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .orderPlacedTimestamp(placed1) .shippedTimestamp(placed1.plusHours(ORDER_1_SHIP_HOURS)) .deliveredTimestamp(placed1.plusDays(ORDER_1_DELIVER_DAYS) @@ -212,13 +232,13 @@ private void seedOrders( .totalAmount(BigDecimal.ZERO) .courierRating(RATING_GOOD) .build(); - SalesLineItem itemFirstOrderFirst = SalesLineItem.builder() + OrderItem itemFirstOrderFirst = OrderItem.builder() .order(order1).product(products.get(0)).quantity(1) .pricePerUnit(PRICE_PROD1).build(); - SalesLineItem itemFirstOrderSecond = SalesLineItem.builder() + OrderItem itemFirstOrderSecond = OrderItem.builder() .order(order1).product(products.get(2)).quantity(2) .pricePerUnit(PRICE_PROD3).build(); - order1.setSalesLineItems(Arrays.asList(itemFirstOrderFirst, + order1.setOrderItems(Arrays.asList(itemFirstOrderFirst, itemFirstOrderSecond)); order1.setNumberOfItems(itemFirstOrderFirst.getQuantity() + itemFirstOrderSecond.getQuantity()); @@ -234,6 +254,7 @@ private void seedOrders( LocalDateTime placed2 = today.minusDays(ORDER_2_DAYS_PRIOR) .atTime(ORDER_2_HOUR, ORDER_2_MINUTE); Order order2 = Order.builder() + .id(UUID.randomUUID()) .customer(customers.get(1)).courier(couriers.get(1)) .region(regions.get(1)) .status(Order.OrderStatus.SHIPPED) @@ -244,10 +265,10 @@ private void seedOrders( .plusHours(ORDER_2_SHIP_HOURS)) .courierRating(null).failureReason(null) .build(); - SalesLineItem itemSecondOrderFirst = SalesLineItem.builder() + OrderItem itemSecondOrderFirst = OrderItem.builder() .order(order2).product(products.get(1)).quantity(1) .pricePerUnit(PRICE_PROD2).build(); - order2.setSalesLineItems(List.of(itemSecondOrderFirst)); + order2.setOrderItems(List.of(itemSecondOrderFirst)); order2.setNumberOfItems(itemSecondOrderFirst.getQuantity()); order2.setTotalAmount( itemSecondOrderFirst.getPricePerUnit().multiply( @@ -259,11 +280,12 @@ private void seedOrders( LocalDateTime placed3 = today.minusDays(ORDER_3_DAYS_PRIOR) .atTime(ORDER_3_HOUR, ORDER_3_MINUTE); Order order3 = Order.builder() + .id(UUID.randomUUID()) .customer(customers.get(0)).courier(couriers.get(0)) .region(regions.get(2)) - .status(Order.OrderStatus.FAILED) + .status(Order.OrderStatus.DELIVERY_FAILED) .orderPlacedTimestamp(placed3) - .status(Order.OrderStatus.FAILED) + .status(Order.OrderStatus.DELIVERY_FAILED) .orderPlacedTimestamp(placed3) .shippedTimestamp(placed3.plusHours(ORDER_3_SHIP_HOURS)) .deliveredTimestamp(null) @@ -271,10 +293,10 @@ private void seedOrders( .failureReason("Delivery address incorrect") .courierRating(RATING_POOR) .build(); - SalesLineItem itemThirdOrderFirst = SalesLineItem.builder() + OrderItem itemThirdOrderFirst = OrderItem.builder() .order(order3).product(products.get(INDEX_THREE)).quantity(1) .pricePerUnit(PRICE_PROD4).build(); - order3.setSalesLineItems(List.of(itemThirdOrderFirst)); + order3.setOrderItems(List.of(itemThirdOrderFirst)); order3.setNumberOfItems(itemThirdOrderFirst.getQuantity()); order3.setTotalAmount( itemThirdOrderFirst.getPricePerUnit().multiply( @@ -285,9 +307,10 @@ private void seedOrders( LocalDateTime placed4 = today.minusDays(ORDER_4_DAYS_PRIOR) .atTime(ORDER_4_HOUR, ORDER_4_MINUTE); Order order4 = Order.builder() + .id(UUID.randomUUID()) .customer(customers.get(2)).courier(couriers.get(1)) .region(regions.get(0)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .orderPlacedTimestamp(placed4) .shippedTimestamp(placed4.plusHours(ORDER_4_SHIP_HOURS)) .deliveredTimestamp(placed4.plusHours(ORDER_4_DELIVER_HOURS)) @@ -296,13 +319,13 @@ private void seedOrders( .totalAmount(BigDecimal.ZERO) .courierRating(RATING_EXCELLENT) .build(); - SalesLineItem itemFourthOrderFirst = SalesLineItem.builder() + OrderItem itemFourthOrderFirst = OrderItem.builder() .order(order4).product(products.get(0)).quantity(1) .pricePerUnit(PRICE_PROD1).build(); - SalesLineItem itemFourthOrderSecond = SalesLineItem.builder() + OrderItem itemFourthOrderSecond = OrderItem.builder() .order(order4).product(products.get(INDEX_THREE)).quantity(1) .pricePerUnit(PRICE_PROD4).build(); - order4.setSalesLineItems(Arrays.asList(itemFourthOrderFirst, + order4.setOrderItems(Arrays.asList(itemFourthOrderFirst, itemFourthOrderSecond)); order4.setNumberOfItems( itemFourthOrderFirst.getQuantity() + itemFourthOrderSecond @@ -316,29 +339,29 @@ private void seedOrders( orderRepository.save(order4); } - private void seedInventorySnapshots(final List products) { - seedInventorySnapshot(products.get(0), INVENTORY_RANGE_PROD1, + private void seedProductSnapshots(final List products) { + seedProductSnapshot(products.get(0), INVENTORY_RANGE_PROD1, INVENTORY_QUANTITY_PROD1); - seedInventorySnapshot(products.get(1), INVENTORY_RANGE_PROD2, + seedProductSnapshot(products.get(1), INVENTORY_RANGE_PROD2, INVENTORY_QUANTITY_PROD2); - seedInventorySnapshot(products.get(2), INVENTORY_RANGE_PROD3, + seedProductSnapshot(products.get(2), INVENTORY_RANGE_PROD3, INVENTORY_QUANTITY_PROD3); - seedInventorySnapshot(products.get(INDEX_THREE), INVENTORY_RANGE_PROD4, + seedProductSnapshot(products.get(INDEX_THREE), INVENTORY_RANGE_PROD4, INVENTORY_QUANTITY_PROD4); } - private void seedInventorySnapshot( + private void seedProductSnapshot( final Product product, final int range, final int quantity) { - inventorySnapshotRepository.save( - InventorySnapshot.builder() + productSnapshotRepository.save( + ProductSnapshot.builder() .product(product) .quantity(random.nextInt(range) + product.getLowStockThreshold()) .timestamp(LocalDateTime.now().minusDays( INVENTORY_SNAPSHOT_DAYS_PRIOR_1)) .build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder() + productSnapshotRepository.save( + ProductSnapshot.builder() .product(product) .quantity(random.nextInt(quantity) + product.getLowStockThreshold()) diff --git a/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java new file mode 100644 index 0000000..7642c35 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java @@ -0,0 +1,39 @@ +package com.Podzilla.analytics.messaging; + +// import org.springframework.amqp.rabbit.annotation.RabbitListener; +// import com.podzilla.mq.EventsConstants; +import org.springframework.beans.factory.annotation.Autowired; + +import com.podzilla.mq.events.BaseEvent; + +import org.springframework.stereotype.Service; + + + +@Service +public class AnalyticsRabbitListener { + + @Autowired + private InvokerDispatcher dispatcher; + + // @RabbitListener( + // queues = EventsConstants.ANALYTICS_USER_EVENT_QUEUE + // ) + public void handleUserEvents(final BaseEvent userEvent) { + dispatcher.dispatch(userEvent); + } + + // @RabbitListener( + // queues = EventsConstants.ANALYTICS_ORDER_EVENT_QUEUE + // ) + public void handleOrderEvents(final BaseEvent orderEvent) { + dispatcher.dispatch(orderEvent); + } + + // @RabbitListener( + // queues = EventsConstants.ANALYTICS_INVENTORY_EVENT_QUEUE + // ) + public void handleInventoryEvents(final BaseEvent inventoryEvent) { + dispatcher.dispatch(inventoryEvent); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcher.java b/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcher.java new file mode 100644 index 0000000..00cb016 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcher.java @@ -0,0 +1,40 @@ +package com.Podzilla.analytics.messaging; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.Podzilla.analytics.messaging.invokers.Invoker; + +public class InvokerDispatcher { + private final Map, Invoker> invokers; + + public InvokerDispatcher() { + this.invokers = new ConcurrentHashMap<>(); + } + + public void registerInvoker( + final Class event, final Invoker invoker + ) { + if (event == null || invoker == null) { + throw new IllegalArgumentException( + "Event and Invoker cannot be null" + ); + } + invokers.put(event, invoker); + } + + @SuppressWarnings("unchecked") + public void dispatch(final T event) { + if (event == null) { + throw new IllegalArgumentException("Event cannot be null"); + } + + Invoker invoker = (Invoker) invokers.get(event.getClass()); + if (invoker != null) { + invoker.invoke(event); + } else { + throw new RuntimeException("No invoker found for: " + + event.getClass()); + } + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcherConfig.java b/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcherConfig.java new file mode 100644 index 0000000..bae26ac --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcherConfig.java @@ -0,0 +1,115 @@ +package com.Podzilla.analytics.messaging; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.Podzilla.analytics.messaging.invokers.user.CourierRegisteredInvoker; +import com.Podzilla.analytics.messaging.invokers.user.CustomerRegisteredInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderAssignedToCourierInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderDeliveryFailedInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderPlacedInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderCancelledInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderDeliveredInvoker; +import com.Podzilla.analytics.messaging.invokers.order.OrderOutForDeliveryInvoker; +import com.Podzilla.analytics.messaging.invokers.InvokerFactory; +import com.Podzilla.analytics.messaging.invokers.inventory.InventoryUpdatedInvoker; +import com.Podzilla.analytics.messaging.invokers.inventory.ProductCreatedInvoker; +import com.Podzilla.analytics.messaging.invokers.inventory.OrderFulfillmentFailedInvoker; + +import com.podzilla.mq.events.CourierRegisteredEvent; +import com.podzilla.mq.events.CustomerRegisteredEvent; +import com.podzilla.mq.events.OrderAssignedToCourierEvent; +import com.podzilla.mq.events.OrderDeliveryFailedEvent; +import com.podzilla.mq.events.OrderPlacedEvent; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.podzilla.mq.events.OrderDeliveredEvent; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; +import com.podzilla.mq.events.InventoryUpdatedEvent; +import com.podzilla.mq.events.ProductCreatedEvent; +import com.podzilla.mq.events.WarehouseOrderFulfillmentFailedEvent; + + +@Configuration +public class InvokerDispatcherConfig { + + @Autowired + private final InvokerFactory invokerFactory; + + public InvokerDispatcherConfig(final InvokerFactory invokerFactory) { + this.invokerFactory = invokerFactory; + } + + @Bean + public InvokerDispatcher invokerDispatcher() { + InvokerDispatcher dispatcher = new InvokerDispatcher(); + + registerUserInvokers(dispatcher); + registerOrderInvokers(dispatcher); + registerInventoryInvokers(dispatcher); + + return dispatcher; + } + + private void registerUserInvokers( + final InvokerDispatcher dispatcher + ) { + dispatcher.registerInvoker( + CourierRegisteredEvent.class, + invokerFactory.createInvoker(CourierRegisteredInvoker.class) + ); + + dispatcher.registerInvoker( + CustomerRegisteredEvent.class, + invokerFactory.createInvoker(CustomerRegisteredInvoker.class) + ); + } + + private void registerOrderInvokers( + final InvokerDispatcher dispatcher + ) { + dispatcher.registerInvoker( + OrderAssignedToCourierEvent.class, + invokerFactory.createInvoker(OrderAssignedToCourierInvoker.class) + ); + dispatcher.registerInvoker( + OrderCancelledEvent.class, + invokerFactory.createInvoker(OrderCancelledInvoker.class) + ); + dispatcher.registerInvoker( + OrderDeliveredEvent.class, + invokerFactory.createInvoker(OrderDeliveredInvoker.class) + ); + dispatcher.registerInvoker( + OrderDeliveryFailedEvent.class, + invokerFactory.createInvoker(OrderDeliveryFailedInvoker.class) + ); + dispatcher.registerInvoker( + OrderOutForDeliveryEvent.class, + invokerFactory.createInvoker(OrderOutForDeliveryInvoker.class) + ); + dispatcher.registerInvoker( + OrderPlacedEvent.class, + invokerFactory.createInvoker(OrderPlacedInvoker.class) + ); + } + + private void registerInventoryInvokers( + final InvokerDispatcher dispatcher + ) { + dispatcher.registerInvoker( + InventoryUpdatedEvent.class, + invokerFactory.createInvoker(InventoryUpdatedInvoker.class) + ); + dispatcher.registerInvoker( + ProductCreatedEvent.class, + invokerFactory.createInvoker(ProductCreatedInvoker.class) + ); + + dispatcher.registerInvoker( + WarehouseOrderFulfillmentFailedEvent.class, + invokerFactory.createInvoker(OrderFulfillmentFailedInvoker.class) + ); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java deleted file mode 100644 index afd38a2..0000000 --- a/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.Podzilla.analytics.messaging; - -import org.springframework.beans.factory.annotation.Autowired; - -import com.Podzilla.analytics.eventhandler.EventHandlerDispatcher; - -public class RabbitListener { - - @Autowired - private EventHandlerDispatcher dispatcher; -} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/Command.java b/src/main/java/com/Podzilla/analytics/messaging/commands/Command.java new file mode 100644 index 0000000..3fb749f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/Command.java @@ -0,0 +1,5 @@ +package com.Podzilla.analytics.messaging.commands; + +public interface Command { + void execute(); +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/CommandFactory.java b/src/main/java/com/Podzilla/analytics/messaging/commands/CommandFactory.java new file mode 100644 index 0000000..d385e02 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/CommandFactory.java @@ -0,0 +1,200 @@ +package com.Podzilla.analytics.messaging.commands; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.Podzilla.analytics.services.CustomerAnalyticsService; +import com.Podzilla.analytics.services.CourierAnalyticsService; +import com.Podzilla.analytics.services.ProductAnalyticsService; +import com.Podzilla.analytics.services.InventoryAnalyticsService; +import com.Podzilla.analytics.services.OrderAnalyticsService; +import com.Podzilla.analytics.services.RegionService; +import com.Podzilla.analytics.messaging.commands.user.RegisterCustomerCommand; +import com.Podzilla.analytics.messaging.commands.user.RegisterCourierCommand; +import com.Podzilla.analytics.messaging.commands.inventory.CreateProductCommand; +import com.Podzilla.analytics.messaging.commands.inventory.UpdateInventoryCommand; +import com.Podzilla.analytics.messaging.commands.inventory.MarkOrderAsFailedToFulfillCommand; +import com.Podzilla.analytics.messaging.commands.order.PlaceOrderCommand; +import com.Podzilla.analytics.messaging.commands.order.AssignCourierToOrderCommand; +import com.Podzilla.analytics.messaging.commands.order.CancelOrderCommand; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsOutForDeliveryCommand; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsDeliveredCommand; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsFailedToDeliverCommand; + + +import com.podzilla.mq.events.DeliveryAddress; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.List; +@Component +public class CommandFactory { + + @Autowired + private CustomerAnalyticsService customerAnalyticsService; + + @Autowired + private CourierAnalyticsService courierAnalyticsService; + + @Autowired + private ProductAnalyticsService productAnalyticsService; + + @Autowired + private InventoryAnalyticsService inventoryAnalyticsService; + + @Autowired + private OrderAnalyticsService orderAnalyticsService; + + + @Autowired + private RegionService regionService; + + public RegisterCustomerCommand createRegisterCustomerCommand( + final String customerId, + final String customerName + ) { + return RegisterCustomerCommand.builder() + .customerAnalyticsService(customerAnalyticsService) + .customerId(customerId) + .customerName(customerName) + .build(); + } + + public RegisterCourierCommand createRegisterCourierCommand( + final String courierId, + final String courierName + ) { + return RegisterCourierCommand.builder() + .courierAnalyticsService(courierAnalyticsService) + .courierId(courierId) + .courierName(courierName) + .build(); + } + + public CreateProductCommand createCreateProductCommand( + final String productId, + final String productName, + final String productCategory, + final BigDecimal productCost, + final Integer productLowStockThreshold + ) { + return CreateProductCommand.builder() + .productAnalyticsService(productAnalyticsService) + .productId(productId) + .productName(productName) + .productCategory(productCategory) + .productCost(productCost) + .productLowStockThreshold(productLowStockThreshold) + .build(); + } + + public UpdateInventoryCommand createUpdateInventoryCommand( + final String productId, + final Integer quantity, + final Instant timestamp + ) { + return UpdateInventoryCommand.builder() + .inventoryAnalyticsService(inventoryAnalyticsService) + .productId(productId) + .quantity(quantity) + .timestamp(timestamp) + .build(); + } + + public PlaceOrderCommand createPlaceOrderCommand( + final String orderId, + final String customerId, + final List items, + final DeliveryAddress deliveryAddress, + final BigDecimal totalAmount, + final Instant timestamp + ) { + return PlaceOrderCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .regionService(regionService) + .orderId(orderId) + .customerId(customerId) + .items(items) + .deliveryAddress(deliveryAddress) + .totalAmount(totalAmount) + .timestamp(timestamp) + .build(); + } + + public CancelOrderCommand createCancelOrderCommand( + final String orderId, + final String reason, + final Instant timestamp + ) { + return CancelOrderCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .reason(reason) + .timestamp(timestamp) + .build(); + } + + public AssignCourierToOrderCommand createAssignCourierToOrderCommand( + final String orderId, + final String courierId + ) { + return AssignCourierToOrderCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .courierId(courierId) + .build(); + } + + public MarkOrderAsOutForDeliveryCommand + createMarkOrderAsOutForDeliveryCommand( + final String orderId, + final Instant timestamp + ) { + return MarkOrderAsOutForDeliveryCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .timestamp(timestamp) + .build(); + } + + public MarkOrderAsDeliveredCommand createMarkOrderAsDeliveredCommand( + final String orderId, + final BigDecimal courierRating, + final Instant timestamp + ) { + return MarkOrderAsDeliveredCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .courierRating(courierRating) + .timestamp(timestamp) + .build(); + } + + public MarkOrderAsFailedToDeliverCommand + createMarkOrderAsFailedToDeliverCommand( + final String orderId, + final String reason, + final Instant timestamp + ) { + return MarkOrderAsFailedToDeliverCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .reason(reason) + .timestamp(timestamp) + .build(); + } + + public MarkOrderAsFailedToFulfillCommand + createMarkOrderAsFailedToFulfillCommand( + final String orderId, + final String reason, + final Instant timestamp + ) { + return MarkOrderAsFailedToFulfillCommand.builder() + .orderAnalyticsService(orderAnalyticsService) + .orderId(orderId) + .reason(reason) + .timestamp(timestamp) + .build(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/CreateProductCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/CreateProductCommand.java new file mode 100644 index 0000000..a13cb48 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/CreateProductCommand.java @@ -0,0 +1,30 @@ +package com.Podzilla.analytics.messaging.commands.inventory; + +import java.math.BigDecimal; +import com.Podzilla.analytics.services.ProductAnalyticsService; + +import lombok.Builder; + +import com.Podzilla.analytics.messaging.commands.Command; + +@Builder +public class CreateProductCommand implements Command { + + private ProductAnalyticsService productAnalyticsService; + private String productId; + private String productName; + private String productCategory; + private BigDecimal productCost; + private Integer productLowStockThreshold; + + @Override + public void execute() { + productAnalyticsService.saveProduct( + productId, + productName, + productCategory, + productCost, + productLowStockThreshold + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/MarkOrderAsFailedToFulfillCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/MarkOrderAsFailedToFulfillCommand.java new file mode 100644 index 0000000..8340fc3 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/MarkOrderAsFailedToFulfillCommand.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.messaging.commands.inventory; + +import com.Podzilla.analytics.messaging.commands.Command; +import com.Podzilla.analytics.services.OrderAnalyticsService; + +import java.time.Instant; +import lombok.Builder; + +@Builder +public class MarkOrderAsFailedToFulfillCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private String reason; + private Instant timestamp; + + @Override + public void execute() { + orderAnalyticsService.markOrderAsFailedToFulfill( + orderId, + reason, + timestamp + ); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/UpdateInventoryCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/UpdateInventoryCommand.java new file mode 100644 index 0000000..1bbfcc2 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/inventory/UpdateInventoryCommand.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.messaging.commands.inventory; + +import com.Podzilla.analytics.services.InventoryAnalyticsService; + +import lombok.Builder; + +import com.Podzilla.analytics.messaging.commands.Command; +import java.time.Instant; + +@Builder +public class UpdateInventoryCommand implements Command { + private final InventoryAnalyticsService inventoryAnalyticsService; + private final String productId; + private final Integer quantity; + private final Instant timestamp; + + @Override + public void execute() { + inventoryAnalyticsService.saveInventorySnapshot( + productId, + quantity, + timestamp + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/AssignCourierToOrderCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/AssignCourierToOrderCommand.java new file mode 100644 index 0000000..4dbc0c7 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/AssignCourierToOrderCommand.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import com.Podzilla.analytics.messaging.commands.Command; +import com.Podzilla.analytics.services.OrderAnalyticsService; +import lombok.Builder; + +@Builder +public class AssignCourierToOrderCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private String courierId; + + @Override + public void execute() { + orderAnalyticsService.assignCourier(orderId, courierId); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/CancelOrderCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/CancelOrderCommand.java new file mode 100644 index 0000000..d11ca82 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/CancelOrderCommand.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import java.time.Instant; + +import com.Podzilla.analytics.services.OrderAnalyticsService; +import com.Podzilla.analytics.messaging.commands.Command; + +import lombok.Builder; + +@Builder +public class CancelOrderCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private String reason; + private Instant timestamp; + + @Override + public void execute() { + orderAnalyticsService.cancelOrder( + orderId, + reason, + timestamp + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsDeliveredCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsDeliveredCommand.java new file mode 100644 index 0000000..bd5e3e5 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsDeliveredCommand.java @@ -0,0 +1,27 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import java.math.BigDecimal; + +import com.Podzilla.analytics.services.OrderAnalyticsService; + +import lombok.Builder; + +import com.Podzilla.analytics.messaging.commands.Command; +import java.time.Instant; + +@Builder +public class MarkOrderAsDeliveredCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private BigDecimal courierRating; + private Instant timestamp; + + @Override + public void execute() { + orderAnalyticsService.markOrderAsDelivered( + orderId, + courierRating, + timestamp + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsFailedToDeliverCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsFailedToDeliverCommand.java new file mode 100644 index 0000000..f788710 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsFailedToDeliverCommand.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import com.Podzilla.analytics.services.OrderAnalyticsService; +import com.Podzilla.analytics.messaging.commands.Command; + +import java.time.Instant; +import lombok.Builder; + +@Builder +public class MarkOrderAsFailedToDeliverCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private String reason; + private Instant timestamp; + + @Override + public void execute() { + orderAnalyticsService.markOrderAsFailedToDeliver( + orderId, + reason, + timestamp + ); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsOutForDeliveryCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsOutForDeliveryCommand.java new file mode 100644 index 0000000..867f4c1 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsOutForDeliveryCommand.java @@ -0,0 +1,20 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import com.Podzilla.analytics.services.OrderAnalyticsService; +import com.Podzilla.analytics.messaging.commands.Command; +import java.time.Instant; + +import lombok.Builder; + +@Builder +public class MarkOrderAsOutForDeliveryCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private String orderId; + private Instant timestamp; + + @Override + public void execute() { + orderAnalyticsService.markOrderAsOutForDelivery(orderId, timestamp); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/order/PlaceOrderCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/order/PlaceOrderCommand.java new file mode 100644 index 0000000..9415b8a --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/order/PlaceOrderCommand.java @@ -0,0 +1,43 @@ +package com.Podzilla.analytics.messaging.commands.order; + +import java.math.BigDecimal; + +import com.Podzilla.analytics.services.OrderAnalyticsService; +import com.Podzilla.analytics.services.RegionService; +import com.podzilla.mq.events.DeliveryAddress; +import com.Podzilla.analytics.messaging.commands.Command; +import com.Podzilla.analytics.models.Region; + +import java.util.List; +import java.time.Instant; +import lombok.Builder; + +@Builder +public class PlaceOrderCommand implements Command { + private OrderAnalyticsService orderAnalyticsService; + private RegionService regionService; + private String orderId; + private String customerId; + private List items; + private DeliveryAddress deliveryAddress; + private BigDecimal totalAmount; + private Instant timestamp; + + @Override + public void execute() { + Region region = regionService.saveRegion( + deliveryAddress.getCity(), + deliveryAddress.getState(), + deliveryAddress.getCountry(), + deliveryAddress.getPostalCode() + ); + orderAnalyticsService.saveOrder( + orderId, + customerId, + items, + region, + totalAmount, + timestamp + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCourierCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCourierCommand.java new file mode 100644 index 0000000..58d638e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCourierCommand.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.messaging.commands.user; + +import com.Podzilla.analytics.messaging.commands.Command; +import com.Podzilla.analytics.services.CourierAnalyticsService; + +import lombok.Builder; + +@Builder +public class RegisterCourierCommand implements Command { + + private CourierAnalyticsService courierAnalyticsService; + private String courierId; + private String courierName; + + @Override + public void execute() { + courierAnalyticsService.saveCourier( + courierId, + courierName + ); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCustomerCommand.java b/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCustomerCommand.java new file mode 100644 index 0000000..6d05d5b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCustomerCommand.java @@ -0,0 +1,23 @@ +package com.Podzilla.analytics.messaging.commands.user; + +import com.Podzilla.analytics.messaging.commands.Command; +import com.Podzilla.analytics.services.CustomerAnalyticsService; + +import lombok.Builder; + +@Builder +public class RegisterCustomerCommand implements Command { + + private CustomerAnalyticsService customerAnalyticsService; + private String customerId; + private String customerName; + + @Override + public void execute() { + customerAnalyticsService.saveCustomer( + customerId, + customerName + ); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/Invoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/Invoker.java new file mode 100644 index 0000000..8d01d14 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/Invoker.java @@ -0,0 +1,5 @@ +package com.Podzilla.analytics.messaging.invokers; + +public interface Invoker { // T should be the BaseEvent subclass of the event + void invoke(T event); +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/InvokerFactory.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/InvokerFactory.java new file mode 100644 index 0000000..8577f28 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/InvokerFactory.java @@ -0,0 +1,35 @@ +package com.Podzilla.analytics.messaging.invokers; + +import java.lang.reflect.Constructor; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; + +@Component +public class InvokerFactory { + + @Autowired + private final CommandFactory commandFactory; + + public InvokerFactory(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + public > T createInvoker( + final Class invokerClass + ) { + try { + Constructor constructor = + invokerClass.getConstructor(CommandFactory.class); + return constructor.newInstance(commandFactory); + } catch (Exception e) { + throw new RuntimeException( + "Failed to create invoker of type: " + invokerClass.getName(), e + ); + } +} + +} + diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/InventoryUpdatedInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/InventoryUpdatedInvoker.java new file mode 100644 index 0000000..efae48e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/InventoryUpdatedInvoker.java @@ -0,0 +1,30 @@ +package com.Podzilla.analytics.messaging.invokers.inventory; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.commands.inventory.UpdateInventoryCommand; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.InventoryUpdatedEvent; + +public class InventoryUpdatedInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + + public InventoryUpdatedInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final InventoryUpdatedEvent event) { + event.getProductSnapshots().stream().map( + snapshot -> commandFactory.createUpdateInventoryCommand( + snapshot.getProductId(), + snapshot.getNewQuantity(), + event.getTimestamp())) + .forEach(UpdateInventoryCommand::execute); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/OrderFulfillmentFailedInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/OrderFulfillmentFailedInvoker.java new file mode 100644 index 0000000..ecb813d --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/OrderFulfillmentFailedInvoker.java @@ -0,0 +1,29 @@ +package com.Podzilla.analytics.messaging.invokers.inventory; +import org.springframework.beans.factory.annotation.Autowired; +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.WarehouseOrderFulfillmentFailedEvent; +import com.Podzilla.analytics.messaging.commands.inventory.MarkOrderAsFailedToFulfillCommand; + +public class OrderFulfillmentFailedInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + + public OrderFulfillmentFailedInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final WarehouseOrderFulfillmentFailedEvent event) { + MarkOrderAsFailedToFulfillCommand command = + commandFactory.createMarkOrderAsFailedToFulfillCommand( + event.getOrderId(), + event.getReason(), + event.getTimestamp() + ); + command.execute(); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/ProductCreatedInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/ProductCreatedInvoker.java new file mode 100644 index 0000000..ad36a28 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/ProductCreatedInvoker.java @@ -0,0 +1,31 @@ +package com.Podzilla.analytics.messaging.invokers.inventory; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.commands.inventory.CreateProductCommand; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.ProductCreatedEvent; + +public class ProductCreatedInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public ProductCreatedInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final ProductCreatedEvent event) { + CreateProductCommand command = commandFactory + .createCreateProductCommand( + event.getProductId(), + event.getName(), + event.getCategory(), + event.getCost(), + event.getLowStockThreshold() + ); + command.execute(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderAssignedToCourierInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderAssignedToCourierInvoker.java new file mode 100644 index 0000000..81b53d0 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderAssignedToCourierInvoker.java @@ -0,0 +1,28 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderAssignedToCourierEvent; +import com.Podzilla.analytics.messaging.commands.order.AssignCourierToOrderCommand; + +public class OrderAssignedToCourierInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderAssignedToCourierInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderAssignedToCourierEvent event) { + AssignCourierToOrderCommand command = commandFactory + .createAssignCourierToOrderCommand( + event.getOrderId(), + event.getCourierId() + ); + command.execute(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderCancelledInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderCancelledInvoker.java new file mode 100644 index 0000000..4b81e0a --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderCancelledInvoker.java @@ -0,0 +1,28 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderCancelledEvent; +import com.Podzilla.analytics.messaging.commands.order.CancelOrderCommand; +public class OrderCancelledInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderCancelledInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderCancelledEvent event) { + CancelOrderCommand command = commandFactory + .createCancelOrderCommand( + event.getOrderId(), + event.getReason(), + event.getTimestamp() + ); + command.execute(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveredInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveredInvoker.java new file mode 100644 index 0000000..c28e276 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveredInvoker.java @@ -0,0 +1,30 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsDeliveredCommand; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderDeliveredEvent; + +public class OrderDeliveredInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderDeliveredInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderDeliveredEvent event) { + MarkOrderAsDeliveredCommand command = + commandFactory.createMarkOrderAsDeliveredCommand( + event.getOrderId(), + event.getCourierRating(), + event.getTimestamp() + ); + command.execute(); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java new file mode 100644 index 0000000..c483511 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java @@ -0,0 +1,30 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderDeliveryFailedEvent; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsFailedToDeliverCommand; + +public class OrderDeliveryFailedInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderDeliveryFailedInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderDeliveryFailedEvent event) { + MarkOrderAsFailedToDeliverCommand command = + commandFactory.createMarkOrderAsFailedToDeliverCommand( + event.getOrderId(), + event.getCourierId(), + event.getTimestamp() + ); + command.execute(); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderOutForDeliveryInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderOutForDeliveryInvoker.java new file mode 100644 index 0000000..7ceed8e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderOutForDeliveryInvoker.java @@ -0,0 +1,28 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderOutForDeliveryEvent; +import com.Podzilla.analytics.messaging.commands.order.MarkOrderAsOutForDeliveryCommand; + +public class OrderOutForDeliveryInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderOutForDeliveryInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderOutForDeliveryEvent event) { + MarkOrderAsOutForDeliveryCommand command = commandFactory + .createMarkOrderAsOutForDeliveryCommand( + event.getOrderId(), + event.getTimestamp() + ); + command.execute(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderPlacedInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderPlacedInvoker.java new file mode 100644 index 0000000..923a6ca --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderPlacedInvoker.java @@ -0,0 +1,33 @@ +package com.Podzilla.analytics.messaging.invokers.order; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.commands.order.PlaceOrderCommand; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.OrderPlacedEvent; + +public class OrderPlacedInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public OrderPlacedInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final OrderPlacedEvent event) { + PlaceOrderCommand command = commandFactory + .createPlaceOrderCommand( + event.getOrderId(), + event.getCustomerId(), + event.getItems(), + event.getDeliveryAddress(), + event.getTotalAmount(), + event.getTimestamp() + ); + command.execute(); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CourierRegisteredInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CourierRegisteredInvoker.java new file mode 100644 index 0000000..760625b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CourierRegisteredInvoker.java @@ -0,0 +1,28 @@ +package com.Podzilla.analytics.messaging.invokers.user; + +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.CourierRegisteredEvent; +import org.springframework.beans.factory.annotation.Autowired; +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.commands.user.RegisterCourierCommand; + + +public class CourierRegisteredInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public CourierRegisteredInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final CourierRegisteredEvent event) { + RegisterCourierCommand command = commandFactory + .createRegisterCourierCommand( + event.getCourierId(), + event.getName() + ); + command.execute(); + } +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CustomerRegisteredInvoker.java b/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CustomerRegisteredInvoker.java new file mode 100644 index 0000000..0c0ca4a --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/user/CustomerRegisteredInvoker.java @@ -0,0 +1,29 @@ +package com.Podzilla.analytics.messaging.invokers.user; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.messaging.commands.CommandFactory; +import com.Podzilla.analytics.messaging.invokers.Invoker; +import com.podzilla.mq.events.CustomerRegisteredEvent; +import com.Podzilla.analytics.messaging.commands.user.RegisterCustomerCommand; + +public class CustomerRegisteredInvoker + implements Invoker { + + @Autowired + private final CommandFactory commandFactory; + public CustomerRegisteredInvoker(final CommandFactory commandFactory) { + this.commandFactory = commandFactory; + } + + @Override + public void invoke(final CustomerRegisteredEvent event) { + RegisterCustomerCommand command = commandFactory + .createRegisterCustomerCommand( + event.getCustomerId(), + event.getName() + ); + command.execute(); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/models/Courier.java b/src/main/java/com/Podzilla/analytics/models/Courier.java index 0e50fd0..e1fd7fa 100644 --- a/src/main/java/com/Podzilla/analytics/models/Courier.java +++ b/src/main/java/com/Podzilla/analytics/models/Courier.java @@ -1,35 +1,59 @@ package com.Podzilla.analytics.models; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; +import java.util.UUID; + @Entity @Table(name = "couriers") @Data -@Builder @NoArgsConstructor @AllArgsConstructor public class Courier { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private UUID id; private String name; - @Enumerated(EnumType.STRING) - private CourierStatus status; + @OneToMany(mappedBy = "courier", cascade = CascadeType.ALL) + private List orders; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private String name; + private List orders; + + public Builder() { + } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder name(final String name) { + this.name = name; + return this; + } + + public Builder orders(final List orders) { + this.orders = orders; + return this; + } - public enum CourierStatus { - ACTIVE, - INACTIVE, - SUSPENDED + public Courier build() { + return new Courier(id, name, orders); + } } } diff --git a/src/main/java/com/Podzilla/analytics/models/Customer.java b/src/main/java/com/Podzilla/analytics/models/Customer.java index f63cbc9..123ca36 100644 --- a/src/main/java/com/Podzilla/analytics/models/Customer.java +++ b/src/main/java/com/Podzilla/analytics/models/Customer.java @@ -1,24 +1,57 @@ package com.Podzilla.analytics.models; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; +import java.util.UUID; + @Entity @Table(name = "customers") @Data -@Builder @NoArgsConstructor @AllArgsConstructor public class Customer { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private UUID id; private String name; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) + private List orders; + + public static Builder builder() { + return new Builder(); + } + public static class Builder { + private UUID id; + private String name; + private List orders; + + public Builder() { } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder name(final String name) { + this.name = name; + return this; + } + + public Builder orders(final List orders) { + this.orders = orders; + return this; + } + + public Customer build() { + return new Customer(id, name, orders); + } + } } diff --git a/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java b/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java deleted file mode 100644 index f5fd12d..0000000 --- a/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.Podzilla.analytics.models; - -import java.time.LocalDateTime; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "inventory_snapshots") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class InventorySnapshot { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private LocalDateTime timestamp; - - @ManyToOne - @JoinColumn(name = "product_id", nullable = false) - private Product product; - - private int quantity; -} diff --git a/src/main/java/com/Podzilla/analytics/models/Order.java b/src/main/java/com/Podzilla/analytics/models/Order.java index f3ec9b4..16c3b90 100644 --- a/src/main/java/com/Podzilla/analytics/models/Order.java +++ b/src/main/java/com/Podzilla/analytics/models/Order.java @@ -9,33 +9,33 @@ import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; @Entity @Table(name = "orders") @Data -@Builder @NoArgsConstructor @AllArgsConstructor public class Order { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private UUID id; private BigDecimal totalAmount; + private LocalDateTime orderPlacedTimestamp; + private LocalDateTime orderFulfillmentFailedTimestamp; + private LocalDateTime orderCancelledTimestamp; private LocalDateTime shippedTimestamp; private LocalDateTime deliveredTimestamp; + private LocalDateTime orderDeliveryFailedTimestamp; private LocalDateTime finalStatusTimestamp; @Enumerated(EnumType.STRING) @@ -52,7 +52,7 @@ public class Order { private Customer customer; @ManyToOne - @JoinColumn(name = "courier_id", nullable = false) + @JoinColumn(name = "courier_id", nullable = true) private Courier courier; @ManyToOne @@ -60,13 +60,155 @@ public class Order { private Region region; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) - private List salesLineItems; + private List orderItems; public enum OrderStatus { PLACED, + FULFILLMENT_FAILED, + CANCELLED, SHIPPED, - DELIVERED_PENDING_PAYMENT, - COMPLETED, - FAILED + DELIVERED, + DELIVERY_FAILED + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private BigDecimal totalAmount; + private LocalDateTime orderPlacedTimestamp; + private LocalDateTime orderFulfillmentFailedTimestamp; + private LocalDateTime orderCancelledTimestamp; + private LocalDateTime shippedTimestamp; + private LocalDateTime deliveredTimestamp; + private LocalDateTime orderDeliveryFailedTimestamp; + private LocalDateTime finalStatusTimestamp; + private OrderStatus status; + private String failureReason; + private int numberOfItems; + private BigDecimal courierRating; + private Customer customer; + private Courier courier; + private Region region; + private List orderItems; + + public Builder() { + } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder totalAmount(final BigDecimal totalAmount) { + this.totalAmount = totalAmount; + return this; + } + + public Builder orderPlacedTimestamp( + final LocalDateTime orderPlacedTimestamp) { + this.orderPlacedTimestamp = orderPlacedTimestamp; + return this; + } + + public Builder orderFulfillmentFailedTimestamp( + final LocalDateTime orderFulfillmentFailedTimestamp) { + this.orderFulfillmentFailedTimestamp = + orderFulfillmentFailedTimestamp; + return this; + } + + public Builder orderCancelledTimestamp( + final LocalDateTime orderCancelledTimestamp) { + this.orderCancelledTimestamp = orderCancelledTimestamp; + return this; + } + + public Builder shippedTimestamp(final LocalDateTime shippedTimestamp) { + this.shippedTimestamp = shippedTimestamp; + return this; + } + + public Builder deliveredTimestamp( + final LocalDateTime deliveredTimestamp) { + this.deliveredTimestamp = deliveredTimestamp; + return this; + } + + public Builder orderDeliveryFailedTimestamp( + final LocalDateTime orderDeliveryFailedTimestamp) { + this.orderDeliveryFailedTimestamp = orderDeliveryFailedTimestamp; + return this; + } + + public Builder finalStatusTimestamp( + final LocalDateTime finalStatusTimestamp) { + this.finalStatusTimestamp = finalStatusTimestamp; + return this; + } + + public Builder status(final OrderStatus status) { + this.status = status; + return this; + } + + public Builder failureReason(final String failureReason) { + this.failureReason = failureReason; + return this; + } + + public Builder numberOfItems(final int numberOfItems) { + this.numberOfItems = numberOfItems; + return this; + } + + public Builder courierRating(final BigDecimal courierRating) { + this.courierRating = courierRating; + return this; + } + + public Builder customer(final Customer customer) { + this.customer = customer; + return this; + } + + public Builder courier(final Courier courier) { + this.courier = courier; + return this; + } + + public Builder region(final Region region) { + this.region = region; + return this; + } + + public Builder orderItems( + final List orderItems) { + this.orderItems = orderItems; + return this; + } + + public Order build() { + return new Order( + id, + totalAmount, + orderPlacedTimestamp, + orderFulfillmentFailedTimestamp, + orderCancelledTimestamp, + shippedTimestamp, + deliveredTimestamp, + orderDeliveryFailedTimestamp, + finalStatusTimestamp, + status, + failureReason, + numberOfItems, + courierRating, + customer, + courier, + region, + orderItems); + } } } diff --git a/src/main/java/com/Podzilla/analytics/models/OrderItem.java b/src/main/java/com/Podzilla/analytics/models/OrderItem.java new file mode 100644 index 0000000..06e07a4 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/OrderItem.java @@ -0,0 +1,87 @@ +package com.Podzilla.analytics.models; + +import java.math.BigDecimal; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; + +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.UUID; + +@Entity +@Table(name = "order_items") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OrderItem { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private int quantity; + private BigDecimal pricePerUnit; + + @ManyToOne + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + @ManyToOne + @JoinColumn(name = "order_id", nullable = false) + private Order order; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private int quantity; + private BigDecimal pricePerUnit; + private Product product; + private Order order; + + public Builder() { + } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder quantity(final int quantity) { + this.quantity = quantity; + return this; + } + + public Builder pricePerUnit(final BigDecimal pricePerUnit) { + this.pricePerUnit = pricePerUnit; + return this; + } + + public Builder product(final Product product) { + this.product = product; + return this; + } + + public Builder order(final Order order) { + this.order = order; + return this; + } + + public OrderItem build() { + return new OrderItem( + id, + quantity, + pricePerUnit, + product, + order); + } + } +} diff --git a/src/main/java/com/Podzilla/analytics/models/Product.java b/src/main/java/com/Podzilla/analytics/models/Product.java index 30f73ae..fc6223e 100644 --- a/src/main/java/com/Podzilla/analytics/models/Product.java +++ b/src/main/java/com/Podzilla/analytics/models/Product.java @@ -3,27 +3,66 @@ import java.math.BigDecimal; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; @Entity @Table(name = "products") @Data -@Builder @NoArgsConstructor @AllArgsConstructor public class Product { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private UUID id; private String name; private String category; private BigDecimal cost; private int lowStockThreshold; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private String name; + private String category; + private BigDecimal cost; + private int lowStockThreshold; + + public Builder() { } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder name(final String name) { + this.name = name; + return this; + } + + public Builder category(final String category) { + this.category = category; + return this; + } + + public Builder cost(final BigDecimal cost) { + this.cost = cost; + return this; + } + + public Builder lowStockThreshold(final int lowStockThreshold) { + this.lowStockThreshold = lowStockThreshold; + return this; + } + + public Product build() { + return new Product(id, name, category, cost, lowStockThreshold); + } + } } diff --git a/src/main/java/com/Podzilla/analytics/models/ProductSnapshot.java b/src/main/java/com/Podzilla/analytics/models/ProductSnapshot.java new file mode 100644 index 0000000..8dda8de --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/ProductSnapshot.java @@ -0,0 +1,73 @@ +package com.Podzilla.analytics.models; + +import java.time.LocalDateTime; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.UUID; + +@Entity +@Table(name = "product_snapshots") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSnapshot { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + private LocalDateTime timestamp; + + @ManyToOne + @JoinColumn(name = "product_id", nullable = false) + private Product product; + + private int quantity; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private LocalDateTime timestamp; + private Product product; + private int quantity; + + public Builder() { + } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder timestamp(final LocalDateTime timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder product(final Product product) { + this.product = product; + return this; + } + + public Builder quantity(final int quantity) { + this.quantity = quantity; + return this; + } + + public ProductSnapshot build() { + return new ProductSnapshot(id, timestamp, product, quantity); + } + } + +} diff --git a/src/main/java/com/Podzilla/analytics/models/Region.java b/src/main/java/com/Podzilla/analytics/models/Region.java index 01945d0..5ba9fb4 100644 --- a/src/main/java/com/Podzilla/analytics/models/Region.java +++ b/src/main/java/com/Podzilla/analytics/models/Region.java @@ -6,22 +6,64 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; @Entity @Table(name = "regions") @Data -@Builder @NoArgsConstructor @AllArgsConstructor public class Region { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; private String city; private String state; private String country; private String postalCode; + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private UUID id; + private String city; + private String state; + private String country; + private String postalCode; + + public Builder() { } + + public Builder id(final UUID id) { + this.id = id; + return this; + } + + public Builder city(final String city) { + this.city = city; + return this; + } + + public Builder state(final String state) { + this.state = state; + return this; + } + + public Builder country(final String country) { + this.country = country; + return this; + } + + public Builder postalCode(final String postalCode) { + this.postalCode = postalCode; + return this; + } + + public Region build() { + return new Region(id, city, state, country, postalCode); + } + } } diff --git a/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java b/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java deleted file mode 100644 index d9e1212..0000000 --- a/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.Podzilla.analytics.models; - -import java.math.BigDecimal; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "sales_line_items") -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SalesLineItem { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private int quantity; - private BigDecimal pricePerUnit; - - @ManyToOne - @JoinColumn(name = "product_id", nullable = false) - private Product product; - - @ManyToOne - @JoinColumn(name = "order_id", nullable = false) - private Order order; -} diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index eae7c5e..56925c1 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -10,21 +11,20 @@ import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; import com.Podzilla.analytics.models.Courier; -public interface CourierRepository extends JpaRepository { +public interface CourierRepository extends JpaRepository { - @Query(value = "SELECT c.id AS courierId, " + @Query("SELECT c.id AS courierId, " + "c.name AS courierName, " + "COUNT(o.id) AS deliveryCount, " - + "SUM(CASE WHEN o.status = 'COMPLETED' THEN 1 ELSE 0 END) " + + "SUM(CASE WHEN o.status = 'DELIVERED' THEN 1 ELSE 0 END) " + "AS completedCount, " - + "AVG(CASE WHEN o.status = 'COMPLETED' THEN o.courier_rating " + + "AVG(CASE WHEN o.status = 'DELIVERED' THEN o.courierRating " + "ELSE NULL END) AS averageRating " - + "FROM couriers c " - + "LEFT JOIN orders o " - + "ON c.id = o.courier_id " - + "AND o.final_status_timestamp BETWEEN :startDate AND :endDate " - + "GROUP BY c.id, c.name " - + "ORDER BY courierId", nativeQuery = true) + + "FROM Courier c " + + "LEFT JOIN Order o " + + "ON c.id = o.courier.id " + + "AND o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + + "GROUP BY c.id, c.name ") List findCourierPerformanceBetweenDates( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index 79bd7f8..92af9b4 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -11,20 +11,21 @@ import com.Podzilla.analytics.models.Customer; import java.time.LocalDateTime; +import java.util.UUID; @Repository -public interface CustomerRepository extends JpaRepository { +public interface CustomerRepository extends JpaRepository { - @Query(value = "SELECT c.id as customerId, c.name as customerName, " - + "SUM(o.total_amount) as totalSpending " - + "FROM customers c " - + "JOIN orders o ON c.id = o.customer_id " - + "WHERE o.order_placed_timestamp " - + "BETWEEN :startDate AND :endDate " - + "GROUP BY c.id, c.name " - + "ORDER BY totalSpending DESC", nativeQuery = true) - Page findTopSpenders( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate, - Pageable pageable); + @Query("SELECT c.id AS customerId, c.name AS customerName, " + + "COALESCE(SUM(o.totalAmount), 0) AS totalSpending " + + "FROM Customer c " + + "LEFT JOIN c.orders o " + + "WITH o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + + "AND o.status = 'DELIVERED' " + + "GROUP BY c.id, c.name " + + "ORDER BY totalSpending DESC") + Page findTopSpenders( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + Pageable pageable); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java deleted file mode 100644 index 219a3fc..0000000 --- a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.Podzilla.analytics.repositories; - -import java.util.List; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.stereotype.Repository; - -import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; -import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; -import com.Podzilla.analytics.models.InventorySnapshot; - -@Repository -public interface InventorySnapshotRepository - extends JpaRepository { - - @Query(value = "SELECT p.category as category, " - + "SUM(s.quantity * p.cost) as totalStockValue " - + "FROM inventory_snapshots s " - + "JOIN products p ON s.product_id = p.id " - + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " - + "FROM inventory_snapshots s2 WHERE " - + "s2.product_id = s.product_id) " - + "GROUP BY p.category", nativeQuery = true) - List getInventoryValueByCategory(); - - @Query(value = "SELECT p.id as productId, p.name as productName, " - + "s.quantity as currentQuantity, " - + "p.low_stock_threshold as threshold " - + "FROM inventory_snapshots s " - + "JOIN products p ON s.product_id = p.id " - + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " - + "FROM inventory_snapshots s2 WHERE " - + "s2.product_id = s.product_id) " -+ "AND s.quantity <= p.low_stock_threshold", nativeQuery = true) - Page getLowStockProducts(Pageable pageable); -} diff --git a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderItemRepository.java similarity index 52% rename from src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java rename to src/main/java/com/Podzilla/analytics/repositories/OrderItemRepository.java index b4b9ac8..94ddd06 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderItemRepository.java @@ -4,22 +4,23 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.models.OrderItem; import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; import java.time.LocalDateTime; import java.util.List; +import java.util.UUID; -public interface SalesLineItemRepository - extends JpaRepository { - @Query("SELECT sli.product.category as category, " - + "SUM(sli.quantity * sli.pricePerUnit) as totalRevenue, " - + "SUM(sli.quantity * sli.product.cost) as totalCost " - + "FROM SalesLineItem sli " - + "WHERE sli.order.orderPlacedTimestamp BETWEEN " +public interface OrderItemRepository + extends JpaRepository { + @Query("SELECT oi.product.category as category, " + + "SUM(oi.quantity * oi.pricePerUnit) as totalRevenue, " + + "SUM(oi.quantity * oi.product.cost) as totalCost " + + "FROM OrderItem oi " + + "WHERE oi.order.finalStatusTimestamp BETWEEN " + ":startDate AND :endDate " - + "AND sli.order.status = 'COMPLETED' " - + "GROUP BY sli.product.category") + + "AND oi.order.status = 'DELIVERED' " + + "GROUP BY oi.product.category") List findSalesByCategoryBetweenDates( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index ae6118b..c3e38da 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -16,149 +16,153 @@ import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; import com.Podzilla.analytics.models.Order; +import java.util.UUID; -public interface OrderRepository extends JpaRepository { +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) + @Query("SELECT 'OVERALL' AS groupByValue, " + + "AVG( (EXTRACT(EPOCH FROM o.shippedTimestamp) - " + + "EXTRACT(EPOCH FROM o.orderPlacedTimestamp)) / 3600) " + + "AS averageDuration " + + "FROM Order o " + + "WHERE o.orderPlacedTimestamp BETWEEN :startDate AND :endDate " + + "AND o.shippedTimestamp IS NOT NULL") 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) + @Query("SELECT CONCAT('RegionID_', r.id) AS groupByValue, " + + "AVG( (EXTRACT(EPOCH FROM o.shippedTimestamp) - " + + "EXTRACT(EPOCH FROM o.orderPlacedTimestamp)) / 3600) " + + "AS averageDuration " + + "FROM Order o " + + "JOIN o.region r " + + "WHERE o.orderPlacedTimestamp BETWEEN :startDate AND :endDate " + + "AND o.shippedTimestamp IS NOT NULL " + + "GROUP BY r.id") List findPlaceToShipTimeByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @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) + @Query("SELECT 'OVERALL' AS groupByValue, " + + "AVG( (EXTRACT(EPOCH FROM o.deliveredTimestamp) - " + + "EXTRACT(EPOCH FROM o.shippedTimestamp)) / 3600) " + + "AS averageDuration " + + "FROM Order o " + + "WHERE o.shippedTimestamp BETWEEN :startDate AND :endDate " + + "AND o.deliveredTimestamp IS NOT NULL " + + "AND o.status = 'DELIVERED'") 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) + @Query("SELECT CONCAT('RegionID_', r.id) AS groupByValue, " + + "AVG( (EXTRACT(EPOCH FROM o.deliveredTimestamp) - " + + "EXTRACT(EPOCH FROM o.shippedTimestamp)) / 3600) " + + "AS averageDuration " + + "FROM Order o " + + "JOIN o.region r " + + "WHERE o.shippedTimestamp BETWEEN :startDate AND :endDate " + + "AND o.deliveredTimestamp IS NOT NULL " + + "AND o.status = 'DELIVERED' " + + "GROUP BY r.id") 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) + @Query("SELECT CONCAT('CourierID_', c.id) AS groupByValue, " + + "AVG( (EXTRACT(EPOCH FROM o.deliveredTimestamp) - " + + "EXTRACT(EPOCH FROM o.shippedTimestamp)) / 3600) " + + "AS averageDuration " + + "FROM Order o " + + "JOIN o.courier c " + + "WHERE o.shippedTimestamp BETWEEN :startDate AND :endDate " + + "AND o.deliveredTimestamp IS NOT NULL " + + "AND o.status = 'DELIVERED' " + + "GROUP BY c.id") List findShipToDeliverTimeByCourier( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @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", - nativeQuery = true) + @Query("SELECT r.id AS regionId, " + + "r.city AS city, " + + "r.country AS country, " + + "COUNT(o) AS orderCount, " + + "AVG(o.totalAmount) AS averageOrderValue " + + "FROM Order o " + + "JOIN o.region r " + + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + + "AND o.status = 'DELIVERED' " + + "GROUP BY r.id, r.city, r.country " + + "ORDER BY orderCount DESC, averageOrderValue DESC") List findOrdersByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "SELECT o.status as status, " - + "count(o.id) as count " - + "FROM orders o " - + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + @Query("SELECT o.status AS status, " + + "COUNT(o) AS count " + + "FROM Order o " + + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + "GROUP BY o.status " - + "ORDER BY count desc", - nativeQuery = true) + + "ORDER BY count DESC") List findOrderStatusCounts( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @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", - nativeQuery = true) + @Query("SELECT o.failureReason AS reason, " + + "COUNT(o) AS count " + + "FROM Order o " + + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + + "AND o.status IN ('DELIVERY_FAILED', 'CANCELLED') " + + "GROUP BY o.failureReason " + + "ORDER BY count DESC") List findFailureReasons( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @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) + @Query("SELECT (SUM(CASE WHEN o.status = 'DELIVERY_FAILED' " + + "THEN 1 ELSE 0 END) * 1.0 " + + "/ COUNT(o)) AS failureRate " + + "FROM Order o " + + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate") OrderFailureRateProjection calculateFailureRate( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "SELECT " - + "t.period, " - + "SUM(t.total_amount) as totalRevenue " + @Query("SELECT period AS period, " + + "SUM(rev) 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) + + " SELECT CASE " + + " WHEN :reportPeriod = 'DAILY' " + + " THEN CAST(o.orderPlacedTimestamp AS date) " + + " WHEN :reportPeriod = 'WEEKLY' " + + " THEN FUNCTION('DATE_TRUNC','week',o.orderPlacedTimestamp) " + + " WHEN :reportPeriod = 'MONTHLY' " + + " THEN FUNCTION('DATE_TRUNC','month',o.orderPlacedTimestamp) " + + " END AS period, " + + " o.totalAmount AS rev " + + "FROM Order o " + + "WHERE o.finalStatusTimestamp >= :startDate " + + "AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' " + + ") x " + + "GROUP BY period " + + "ORDER BY totalRevenue DESC") 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') " + @Query("SELECT p.category AS category, " + + "SUM(oi.quantity * oi.pricePerUnit) AS totalRevenue " + + "FROM OrderItem oi " + + "JOIN oi.order o " + + "JOIN oi.product p " + + "WHERE o.finalStatusTimestamp >= :startDate " + + "AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' " + "GROUP BY p.category " - + "ORDER BY SUM(sli.quantity * sli.price_per_unit) DESC", - nativeQuery = true) + + "ORDER BY totalRevenue DESC") 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 425e6c8..fa50cb5 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -9,37 +9,25 @@ import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; import com.Podzilla.analytics.models.Product; +import java.util.UUID; +public interface ProductRepository extends JpaRepository { -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) - + @Query("SELECT p.id AS id, " + + "p.name AS name, " + + "p.category AS category, " + + "SUM(oi.quantity * oi.pricePerUnit) AS totalRevenue, " + + "SUM(oi.quantity) AS totalUnits " + + "FROM OrderItem oi " + + "JOIN oi.order o " + + "JOIN oi.product p " + + "WHERE o.finalStatusTimestamp >= :startDate " + + "AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' " + + "GROUP BY p.id, p.name, p.category " + + "ORDER BY CASE WHEN :sortBy = 'REVENUE' " + + "THEN SUM(oi.quantity * oi.pricePerUnit) " + + " WHEN :sortBy = 'UNITS' THEN SUM(oi.quantity) " + + " ELSE SUM(oi.quantity * oi.pricePerUnit) END DESC") List findTopSellers( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java new file mode 100644 index 0000000..6759b77 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java @@ -0,0 +1,40 @@ +package com.Podzilla.analytics.repositories; + +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; +import com.Podzilla.analytics.models.ProductSnapshot; + +@Repository +public interface ProductSnapshotRepository + extends JpaRepository { + + @Query("SELECT p.category AS category, " + + "SUM(s.quantity * p.cost) AS totalStockValue " + + "FROM ProductSnapshot s " + + "JOIN s.product p " + + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " + + " FROM ProductSnapshot s2 " + + " WHERE s2.product.id = s.product.id) " + + "GROUP BY p.category") + List getInventoryValueByCategory(); + + @Query("SELECT p.id AS productId, " + + "p.name AS productName, " + + "s.quantity AS currentQuantity, " + + "p.lowStockThreshold AS threshold " + + "FROM ProductSnapshot s " + + "JOIN s.product p " + + "WHERE s.timestamp = (SELECT MAX(s2.timestamp) " + + " FROM ProductSnapshot s2 " + + " WHERE s2.product.id = s.product.id) " + + "AND s.quantity <= p.lowStockThreshold") + Page getLowStockProducts(Pageable pageable); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java index 64a5c44..5aa30d8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.Podzilla.analytics.models.Region; +import java.util.UUID; -public interface RegionRepository extends JpaRepository { +public interface RegionRepository extends JpaRepository { } diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 8376613..33033e2 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -4,6 +4,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.UUID; import org.springframework.stereotype.Service; @@ -12,8 +13,10 @@ import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; +import com.Podzilla.analytics.models.Courier; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.util.MetricCalculator; +import com.Podzilla.analytics.util.StringToUUIDParser; import lombok.RequiredArgsConstructor; @@ -22,70 +25,89 @@ public class CourierAnalyticsService { private final CourierRepository courierRepository; - private List getCourierPerformanceData( + private List getCourierPerformanceData( final LocalDate startDate, - final LocalDate endDate) { + final LocalDate endDate + ) { LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); return courierRepository.findCourierPerformanceBetweenDates( startDateTime, - endDateTime); + endDateTime + ); } - public List getCourierDeliveryCounts( + public List getCourierDeliveryCounts( final LocalDate startDate, - final LocalDate endDate) { - return getCourierPerformanceData(startDate, endDate).stream() - .map(data -> CourierDeliveryCountResponse.builder() - .courierId(data.getCourierId()) - .courierName(data.getCourierName()) - .deliveryCount(data.getDeliveryCount()) - .build()) - .toList(); - } + final LocalDate endDate + ) { + return getCourierPerformanceData(startDate, endDate).stream() + .map(data -> CourierDeliveryCountResponse.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .deliveryCount(data.getDeliveryCount()) + .build()) + .toList(); + } - public List getCourierSuccessRate( + public List getCourierSuccessRate( final LocalDate startDate, - final LocalDate endDate) { - return getCourierPerformanceData(startDate, endDate).stream() - .map(data -> CourierSuccessRateResponse.builder() - .courierId(data.getCourierId()) - .courierName(data.getCourierName()) - .successRate( - MetricCalculator.calculateRate( - data.getCompletedCount(), - data.getDeliveryCount())) - .build()) - .toList(); - } + final LocalDate endDate + ) { + return getCourierPerformanceData(startDate, endDate).stream() + .map(data -> CourierSuccessRateResponse.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .successRate( + MetricCalculator.calculateRate( + data.getCompletedCount(), + data.getDeliveryCount())) + .build()) + .toList(); + } - public List getCourierAverageRating( + public List getCourierAverageRating( final LocalDate startDate, - final LocalDate endDate) { - return getCourierPerformanceData(startDate, endDate).stream() - .map(data -> CourierAverageRatingResponse.builder() - .courierId(data.getCourierId()) - .courierName(data.getCourierName()) - .averageRating(data.getAverageRating()) - .build()) - .toList(); - } + final LocalDate endDate + ) { + return getCourierPerformanceData(startDate, endDate).stream() + .map(data -> CourierAverageRatingResponse.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .averageRating(data.getAverageRating()) + .build()) + .toList(); + } - public List getCourierPerformanceReport( + public List + getCourierPerformanceReport( final LocalDate startDate, - final LocalDate endDate) { - return getCourierPerformanceData(startDate, endDate).stream() - .map(data -> CourierPerformanceReportResponse.builder() - .courierId(data.getCourierId()) - .courierName(data.getCourierName()) - .deliveryCount(data.getDeliveryCount()) - .successRate( - MetricCalculator.calculateRate( - data.getCompletedCount(), - data.getDeliveryCount())) - .averageRating(data.getAverageRating()) - .build()) - .toList(); + final LocalDate endDate + ) { + return getCourierPerformanceData(startDate, endDate).stream() + .map(data -> CourierPerformanceReportResponse.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .deliveryCount(data.getDeliveryCount()) + .successRate( + MetricCalculator.calculateRate( + data.getCompletedCount(), + data.getDeliveryCount())) + .averageRating(data.getAverageRating()) + .build()) + .toList(); } + + public void saveCourier( + final String courierId, + final String courierName + ) { + UUID id = StringToUUIDParser.parseStringToUUID(courierId); + Courier courier = Courier.builder() + .id(id) + .name(courierName) + .build(); + courierRepository.save(courier); + } } diff --git a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java index aab2d88..0afeebb 100644 --- a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java @@ -5,11 +5,16 @@ import com.Podzilla.analytics.api.dtos.customer.CustomersTopSpendersResponse; import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.util.DatetimeFormatter; +import com.Podzilla.analytics.util.StringToUUIDParser; +import com.Podzilla.analytics.models.Customer; import lombok.RequiredArgsConstructor; import java.time.LocalDateTime; +import java.time.LocalDate; import java.util.List; +import java.util.UUID; @Service @RequiredArgsConstructor @@ -17,13 +22,17 @@ public class CustomerAnalyticsService { private final CustomerRepository customerRepository; public List getTopSpenders( - final LocalDateTime startDate, - final LocalDateTime endDate, + final LocalDate startDate, + final LocalDate endDate, final int page, final int size) { + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); PageRequest pageRequest = PageRequest.of(page, size); List topSpenders = customerRepository -.findTopSpenders(startDate, endDate, pageRequest) +.findTopSpenders(startDateTime, endDateTime, pageRequest) .stream() .map(row -> CustomersTopSpendersResponse.builder() .customerId(row.getCustomerId()) @@ -33,4 +42,18 @@ public List getTopSpenders( .toList(); return topSpenders; } + + public void saveCustomer( + final String customerId, + final String customerName + ) { + UUID id = StringToUUIDParser.parseStringToUUID(customerId); + Customer customer = Customer.builder() + .id(id) + .name(customerName) + .build(); + System.out.println("Customer object created: " + + customer.getName() + " with ID: " + customer.getId()); + customerRepository.save(customer); + } } diff --git a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java index e701a78..5fca47b 100644 --- a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java @@ -4,12 +4,9 @@ import com.Podzilla.analytics.repositories.OrderRepository; import com.Podzilla.analytics.util.DatetimeFormatter; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; -import com.Podzilla.analytics.api.projections.fulfillment -.FulfillmentTimeProjection; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; +import com.Podzilla.analytics.api.projections.fulfillment.FulfillmentTimeProjection; import lombok.RequiredArgsConstructor; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java index 7bcefa3..ca61ada 100644 --- a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java @@ -6,15 +6,24 @@ import com.Podzilla.analytics.api.dtos.inventory.InventoryValueByCategoryResponse; import com.Podzilla.analytics.api.dtos.inventory.LowStockProductResponse; -import com.Podzilla.analytics.repositories.InventorySnapshotRepository; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.ProductSnapshot; +import com.Podzilla.analytics.util.StringToUUIDParser; +import com.Podzilla.analytics.util.DatetimeFormatter; import java.util.List; +import java.util.UUID; +import java.time.Instant; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class InventoryAnalyticsService { - private final InventorySnapshotRepository inventoryRepo; + private final ProductSnapshotRepository inventoryRepo; + private final ProductRepository productRepository; public List getInventoryValueByCategory() { List invVByCy = inventoryRepo @@ -41,4 +50,25 @@ public Page getLowStockProducts(final int page, .build()); return lowStockPro; } + + public void saveInventorySnapshot( + final String productId, + final Integer quantity, + final Instant timestamp + ) { + UUID productUUID = StringToUUIDParser.parseStringToUUID(productId); + Product product = productRepository.findById(productUUID) + .orElseThrow( + () -> new IllegalArgumentException("Product not found") + ); + LocalDateTime snapshotTimestamp = DatetimeFormatter + .convertIntsantToDateTime(timestamp); + ProductSnapshot inventorySnapshot = ProductSnapshot.builder() + .product(product) + .quantity(quantity) + .timestamp(snapshotTimestamp) + .build(); + + inventoryRepo.save(inventorySnapshot); + } } diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java index 9af3233..eb74223 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -1,5 +1,7 @@ package com.Podzilla.analytics.services; +import java.math.BigDecimal; +import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -14,17 +16,32 @@ 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.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; import com.Podzilla.analytics.repositories.OrderRepository; import com.Podzilla.analytics.util.DatetimeFormatter; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Order.OrderStatus; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.util.StringToUUIDParser; + + import lombok.RequiredArgsConstructor; +import java.util.UUID; -@RequiredArgsConstructor @Service +@RequiredArgsConstructor public class OrderAnalyticsService { private final OrderRepository orderRepository; + private final CustomerRepository customerRepository; + private final CourierRepository courierRepository; + private final OrderItemService orderItemService; + public List getOrdersByRegion( final LocalDate startDate, final LocalDate endDate @@ -33,12 +50,8 @@ public List getOrdersByRegion( DatetimeFormatter.convertStartDateToDatetime(startDate); LocalDateTime endDateTime = DatetimeFormatter.convertEndDateToDatetime(endDate); - System.out.println("Start date a1a1: " + startDate); - System.out.println("End date b1b1: " + endDate); List ordersByRegion = orderRepository.findOrdersByRegion(startDateTime, endDateTime); - System.out.println("Start date a2a2: " + startDate); - System.out.println("End date b2b2: " + endDate); return ordersByRegion.stream() .map(data -> OrderRegionResponse.builder() .regionId(data.getRegionId()) @@ -92,4 +105,163 @@ public OrderFailureResponse getOrdersFailures( .failureRate(failureRate.getFailureRate()) .build(); } + + public Order saveOrder( + final String orderId, + final String customerId, + final List items, + final Region region, + final BigDecimal totalAmount, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + UUID customerUUID = + StringToUUIDParser.parseStringToUUID(customerId); + Customer customer = + customerRepository.findById(customerUUID) + .orElseThrow(() -> new RuntimeException("Customer not found")); + int numberOfItems = items.stream() + .mapToInt(com.podzilla.mq.events.OrderItem::getQuantity) + .sum(); + LocalDateTime orderPlacedTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = Order.builder() + .id(orderUUID) + .totalAmount(totalAmount) + .orderPlacedTimestamp(orderPlacedTimestamp) + .finalStatusTimestamp(orderPlacedTimestamp) + .region(region) + .customer(customer) + .numberOfItems(numberOfItems) + .status(OrderStatus.PLACED) + .build(); + orderRepository.save(order); + + List orderItems = + items.stream() + .map(item -> orderItemService.saveOrderItem( + item.getQuantity(), + item.getPricePerUnit(), + item.getProductId(), + orderUUID.toString() + )) + .toList(); + order.setOrderItems(orderItems); + return orderRepository.save(order); + } + + public Order cancelOrder( + final String orderId, + final String reason, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + LocalDateTime orderCancelledTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + order.setStatus(OrderStatus.CANCELLED); + order.setFailureReason(reason); + order.setOrderCancelledTimestamp(orderCancelledTimestamp); + order.setFinalStatusTimestamp(orderCancelledTimestamp); + return orderRepository.save(order); + } + + public void assignCourier( + final String orderId, + final String courierId + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + UUID courierUUID = + StringToUUIDParser.parseStringToUUID(courierId); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + Courier courier = + courierRepository.findById(courierUUID) + .orElseThrow(() -> new RuntimeException("Courier not found")); + order.setCourier(courier); + orderRepository.save(order); + } + public void markOrderAsOutForDelivery( + final String orderId, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + LocalDateTime orderOutForDeliveryTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + order.setStatus(OrderStatus.SHIPPED); + order.setShippedTimestamp(orderOutForDeliveryTimestamp); + order.setFinalStatusTimestamp(orderOutForDeliveryTimestamp); + orderRepository.save(order); + } + + public void markOrderAsDelivered( + final String orderId, + final BigDecimal courierRating, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + LocalDateTime orderDeliveredTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + order.setStatus(OrderStatus.DELIVERED); + order.setDeliveredTimestamp(orderDeliveredTimestamp); + order.setFinalStatusTimestamp(orderDeliveredTimestamp); + order.setCourierRating(courierRating); + orderRepository.save(order); + } + + public void markOrderAsFailedToDeliver( + final String orderId, + final String reason, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + LocalDateTime orderFailedToDeliverTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + order.setStatus(OrderStatus.DELIVERY_FAILED); + order.setFailureReason(reason); + order.setOrderDeliveryFailedTimestamp( + orderFailedToDeliverTimestamp + ); + order.setFinalStatusTimestamp(orderFailedToDeliverTimestamp); + orderRepository.save(order); + } + + public void markOrderAsFailedToFulfill( + final String orderId, + final String reason, + final Instant timeStamp + ) { + UUID orderUUID = + StringToUUIDParser.parseStringToUUID(orderId); + LocalDateTime orderFulfillmentFailedTimestamp = + DatetimeFormatter.convertIntsantToDateTime(timeStamp); + Order order = + orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + order.setStatus(OrderStatus.FULFILLMENT_FAILED); + order.setFailureReason(reason); + order.setOrderFulfillmentFailedTimestamp( + orderFulfillmentFailedTimestamp + ); + order.setFinalStatusTimestamp(orderFulfillmentFailedTimestamp); + orderRepository.save(order); + } } diff --git a/src/main/java/com/Podzilla/analytics/services/OrderItemService.java b/src/main/java/com/Podzilla/analytics/services/OrderItemService.java new file mode 100644 index 0000000..daa91bc --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/OrderItemService.java @@ -0,0 +1,55 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import com.Podzilla.analytics.repositories.OrderItemRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.OrderRepository; + + +import com.Podzilla.analytics.util.StringToUUIDParser; + +import org.springframework.beans.factory.annotation.Autowired; +import com.Podzilla.analytics.models.OrderItem; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.Order; + + +import java.math.BigDecimal; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class OrderItemService { + @Autowired + private final OrderItemRepository orderItemRepository; + + @Autowired + private final ProductRepository productRepository; + + @Autowired + private final OrderRepository orderRepository; + + public OrderItem saveOrderItem( + final int quantity, + final BigDecimal pricePerUnit, + final String productId, + final String orderId + ) { + UUID productUUID = StringToUUIDParser.parseStringToUUID(productId); + UUID orderUUID = StringToUUIDParser.parseStringToUUID(orderId); + Product product = productRepository.findById(productUUID) + .orElseThrow(() -> new RuntimeException("Product not found")); + Order order = orderRepository.findById(orderUUID) + .orElseThrow(() -> new RuntimeException("Order not found")); + + OrderItem orderItem = OrderItem.builder() + .quantity(quantity) + .pricePerUnit(pricePerUnit) + .product(product) + .order(order) + .build(); + return orderItemRepository.save(orderItem); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java index 3cb64ba..350f005 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -13,6 +13,9 @@ import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; import com.Podzilla.analytics.repositories.ProductRepository; import lombok.RequiredArgsConstructor; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.util.StringToUUIDParser; +import java.util.UUID; @RequiredArgsConstructor @Service @@ -36,7 +39,8 @@ public List getTopSellers( final LocalDate startDate, final LocalDate endDate, final Integer limit, - final SortBy sortBy) { + final SortBy sortBy +) { final String sortByString = sortBy != null ? sortBy.name() : SortBy.REVENUE.name(); @@ -72,4 +76,22 @@ public List getTopSellers( return topSellersList; } + + public void saveProduct( + final String productId, + final String productName, + final String productCategory, + final BigDecimal productCost, + final Integer productLowStockThreshold + ) { + UUID id = StringToUUIDParser.parseStringToUUID(productId); + Product product = Product.builder() + .id(id) + .name(productName) + .category(productCategory) + .cost(productCost) + .lowStockThreshold(productLowStockThreshold) + .build(); + productRepository.save(product); + } } diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java index 85d3fb3..0daf15e 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -4,7 +4,7 @@ import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; -import com.Podzilla.analytics.repositories.SalesLineItemRepository; +import com.Podzilla.analytics.repositories.OrderItemRepository; import lombok.RequiredArgsConstructor; @@ -19,14 +19,12 @@ @RequiredArgsConstructor @Service public class ProfitAnalyticsService { - private final SalesLineItemRepository salesLineItemRepository; - // Precision constant for percentage calculations + private final OrderItemRepository salesLineItemRepository; private static final int PERCENTAGE_PRECISION = 4; public List getProfitByCategory( final LocalDate startDate, final LocalDate endDate) { - // Convert LocalDate to LocalDateTime for start of day and end of day LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); diff --git a/src/main/java/com/Podzilla/analytics/services/RegionService.java b/src/main/java/com/Podzilla/analytics/services/RegionService.java new file mode 100644 index 0000000..f653fed --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/RegionService.java @@ -0,0 +1,34 @@ +package com.Podzilla.analytics.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.Podzilla.analytics.repositories.RegionRepository; + +import lombok.RequiredArgsConstructor; +import com.Podzilla.analytics.models.Region; + + + +@Service +@RequiredArgsConstructor +public class RegionService { + + @Autowired + private final RegionRepository regionRepository; + + public Region saveRegion( + final String city, + final String state, + final String country, + final String postalCode + ) { + Region region = Region.builder() + .city(city) + .state(state) + .country(country) + .postalCode(postalCode) + .build(); + return regionRepository.save(region); + } +} diff --git a/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java b/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java index 8a3c110..83b7f67 100644 --- a/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java +++ b/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java @@ -3,6 +3,8 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.Instant; +import java.time.ZoneId; public class DatetimeFormatter { public static LocalDateTime convertStartDateToDatetime( @@ -15,4 +17,12 @@ public static LocalDateTime convertEndDateToDatetime( ) { return endDate.atTime(LocalTime.MAX); } + public static LocalDateTime convertIntsantToDateTime( + final Instant timestamp + ) { + return LocalDateTime.ofInstant( + timestamp, + ZoneId.systemDefault() + ); + } } diff --git a/src/main/java/com/Podzilla/analytics/util/StringToUUIDParser.java b/src/main/java/com/Podzilla/analytics/util/StringToUUIDParser.java new file mode 100644 index 0000000..678cbb8 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/util/StringToUUIDParser.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.util; + +import java.util.UUID; + +public class StringToUUIDParser { + public static UUID parseStringToUUID(final String str) { + try { + return UUID.fromString(str); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid input: " + str, e); + } + } + +} diff --git a/src/main/java/com/Podzilla/analytics/validation/annotations/ValidPagination.java b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidPagination.java new file mode 100644 index 0000000..3f0b745 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidPagination.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.validation.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.Podzilla.analytics.validation.validators.PaginationValidator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = PaginationValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidPagination { + String message() default "Page must be greater than or equal to 0 " + + "and size must be greater than 0"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java index 26fa7cb..eddcd1e 100644 --- a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java +++ b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java @@ -1,33 +1,19 @@ package com.Podzilla.analytics.validation.validators; -import java.time.LocalDate; -import java.lang.reflect.Method; - +import com.Podzilla.analytics.api.dtos.IDateRangeRequest; import com.Podzilla.analytics.validation.annotations.ValidDateRange; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public final class DateRangeValidator implements - ConstraintValidator { + ConstraintValidator { @Override - public boolean isValid(final Object value, + public boolean isValid(final IDateRangeRequest request, final ConstraintValidatorContext context) { - if (value == null) { + if (request.getStartDate() == null || request.getEndDate() == null) { return true; } - - try { - Method getStartDate = value.getClass().getMethod("getStartDate"); - Method getEndDate = value.getClass().getMethod("getEndDate"); - LocalDate startDate = (LocalDate) getStartDate.invoke(value); - LocalDate endDate = (LocalDate) getEndDate.invoke(value); - if (startDate == null || endDate == null) { - return true; // Let @NotNull handle this - } - return !endDate.isBefore(startDate); - } catch (Exception e) { - return false; - } + return request.getEndDate().isAfter(request.getStartDate()); } } diff --git a/src/main/java/com/Podzilla/analytics/validation/validators/PaginationValidator.java b/src/main/java/com/Podzilla/analytics/validation/validators/PaginationValidator.java new file mode 100644 index 0000000..a9bad9f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/validation/validators/PaginationValidator.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.validation.validators; + +import com.Podzilla.analytics.api.dtos.IPaginationRequest; +import com.Podzilla.analytics.validation.annotations.ValidPagination; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public final class PaginationValidator implements + ConstraintValidator { + + @Override + public boolean isValid(final IPaginationRequest request, + final ConstraintValidatorContext context) { + return request.getPage() >= 0 && request.getSize() > 0; + } + +} diff --git a/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java index f383c56..7a31e5a 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java @@ -4,6 +4,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.UUID; import jakarta.persistence.EntityManager; @@ -69,8 +70,11 @@ void setUp() { entityManager.flush(); entityManager.clear(); - customer1 = customerRepository.save(Customer.builder().name("John Doe").build()); + customer1 = customerRepository.save(Customer.builder() + .id(UUID.randomUUID()) + .name("John Doe").build()); region1 = regionRepository.save(Region.builder() + // .id(UUID.randomUUID()) .city("Sample City") .state("Sample State") .country("Sample Country") @@ -78,19 +82,20 @@ void setUp() { .build()); courierJane = courierRepository.save(Courier.builder() + .id(UUID.randomUUID()) .name("Jane Smith") - .status(Courier.CourierStatus.ACTIVE) .build()); courierJohn = courierRepository.save(Courier.builder() + .id(UUID.randomUUID()) .name("John Doe") - .status(Courier.CourierStatus.ACTIVE) .build()); orderRepository.save(Order.builder() + .id(UUID.randomUUID()) .totalAmount(new BigDecimal("50.00")) .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .numberOfItems(1) .courierRating(new BigDecimal("4.0")) .customer(customer1) @@ -99,9 +104,10 @@ void setUp() { .build()); orderRepository.save(Order.builder() + .id(UUID.randomUUID()) .totalAmount(new BigDecimal("75.00")) .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .numberOfItems(1) .courierRating(new BigDecimal("4.0")) .customer(customer1) @@ -110,9 +116,10 @@ void setUp() { .build()); orderRepository.save(Order.builder() + .id(UUID.randomUUID()) .totalAmount(new BigDecimal("120.00")) .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .numberOfItems(2) .courierRating(new BigDecimal("5.0")) .customer(customer1) @@ -121,9 +128,10 @@ void setUp() { .build()); orderRepository.save(Order.builder() + .id(UUID.randomUUID()) .totalAmount(new BigDecimal("30.00")) .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) - .status(Order.OrderStatus.FAILED) + .status(Order.OrderStatus.DELIVERY_FAILED) .numberOfItems(1) .courierRating(null) .customer(customer1) @@ -131,10 +139,11 @@ void setUp() { .region(region1) .build()); - orderRepository.save(Order.builder() + orderRepository.save(Order.builder() + .id(UUID.randomUUID()) .totalAmount(new BigDecimal("90.00")) .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) - .status(Order.OrderStatus.COMPLETED) + .status(Order.OrderStatus.DELIVERED) .numberOfItems(1) .courierRating(new BigDecimal("3.0")) .customer(customer1) diff --git a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java index e76925d..394b713 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java @@ -316,65 +316,65 @@ public void testGetShipToDeliverTime_InvalidGroupBy() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } - @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); - - // Build URL with query parameters - String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") - .queryParam("startDate", sameDate.toString()) - .queryParam("endDate", sameDate.toString()) - .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) - .toUriString(); - - // Execute request - ResponseEntity> response = restTemplate.exchange( - url, - HttpMethod.GET, - null, - new ParameterizedTypeReference>() {}); - - // Verify - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); - } - - @Test - 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); - - // Build URL with query parameters - String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") - .queryParam("startDate", sameDate.toString()) - .queryParam("endDate", sameDate.toString()) - .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) - .toUriString(); - - // Execute request - ResponseEntity> response = restTemplate.exchange( - url, - HttpMethod.GET, - null, - new ParameterizedTypeReference>() {}); - - // Verify - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); - } + // @Test + // 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); + + // // Build URL with query parameters + // String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + // .queryParam("startDate", sameDate.toString()) + // .queryParam("endDate", sameDate.toString()) + // .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + // .toUriString(); + + // // Execute request + // ResponseEntity> response = restTemplate.exchange( + // url, + // HttpMethod.GET, + // null, + // new ParameterizedTypeReference>() {}); + + // // Verify + // assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + // assertThat(response.getBody()).isNotNull(); + // assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); + // } + + // @Test + // 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); + + // // Build URL with query parameters + // String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + // .queryParam("startDate", sameDate.toString()) + // .queryParam("endDate", sameDate.toString()) + // .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + // .toUriString(); + + // // Execute request + // ResponseEntity> response = restTemplate.exchange( + // url, + // HttpMethod.GET, + // null, + // new ParameterizedTypeReference>() {}); + + // // Verify + // assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + // assertThat(response.getBody()).isNotNull(); + // assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); + // } @Test public void testGetPlaceToShipTime_FutureDates() { diff --git a/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java index 6c1f50c..fab6d70 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java @@ -230,18 +230,19 @@ public void testGetProfitByCategory_FutureDateRange() { } @Test - public void testGetProfitByCategory_SameDayRange() { - // Test same start and end date - LocalDate sameDate = LocalDate.of(2024, 1, 1); + public void testGetProfitByCategory_ConsecutiveDayRange() { + // Test consecutive dates (1 day range) + LocalDate startDate = LocalDate.of(2024, 1, 1); + LocalDate endDate = LocalDate.of(2024, 1, 2); // Configure mock service - when(mockService.getProfitByCategory(sameDate, sameDate)) + when(mockService.getProfitByCategory(startDate, endDate)) .thenReturn(profitData); - // Build URL with same day for start and end + // Build URL with consecutive days for start and end String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") - .queryParam("startDate", sameDate.toString()) - .queryParam("endDate", sameDate.toString()) + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) .toUriString(); // Execute request diff --git a/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java index b7adcc7..ac9d401 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java @@ -1,119 +1,117 @@ -// 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. -// } +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; +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.revenue.RevenueSummaryRequest.Period; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.services.RevenueReportService; + +@SpringBootTest +@AutoConfigureMockMvc +class RevenueReportControllerTest { + + @Autowired + private MockMvc mockMvc; + + @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-analytics/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(), any(), 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-analytics/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(), any(), 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 + } +} diff --git a/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java index 8e1bd51..4a8d2ca 100644 --- a/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java +++ b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java @@ -1,654 +1,679 @@ -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)); - } - } -} +// 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.UUID; +// 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() +// .id(UUID.randomUUID()) +// .name("Smartphone") +// .category("Electronics") +// .cost(new BigDecimal("300.00")) +// .lowStockThreshold(5) +// .build(); + +// laptop = Product.builder() +// .id(UUID.randomUUID()) +// .name("Laptop") +// .category("Electronics") +// .cost(new BigDecimal("700.00")) +// .lowStockThreshold(3) +// .build(); + +// book = Product.builder() +// .id(UUID.randomUUID()) +// .name("Programming Book") +// .category("Books") +// .cost(new BigDecimal("20.00")) +// .lowStockThreshold(10) +// .build(); + +// tablet = Product.builder() +// .id(UUID.randomUUID()) +// .name("Tablet") +// .category("Electronics") +// .cost(new BigDecimal("200.00")) +// .lowStockThreshold(5) +// .build(); + +// headphones = Product.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .name("Test Customer") +// .build(); +// customerRepository.save(customer); + +// courier = Courier.builder() +// .id(UUID.randomUUID()) +// .name("Test Courier") +// .status(Courier.CourierStatus.ACTIVE) +// .build(); +// courierRepository.save(courier); + +// region = Region.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .order(order1) +// .product(phone) +// .quantity(2) // 2 phones +// .pricePerUnit(new BigDecimal("500.00")) // $500 each +// .build(); + +// SalesLineItem item1_2 = SalesLineItem.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .order(order3) +// .product(book) +// .quantity(5) // 5 books +// .pricePerUnit(new BigDecimal("40.00")) // $40 each +// .build(); + +// SalesLineItem item3_2 = SalesLineItem.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .order(order5) +// .product(headphones) +// .quantity(5) // 5 headphones +// .pricePerUnit(new BigDecimal("100.00")) // $100 each = $500 total +// .build(); + +// SalesLineItem item5_2 = SalesLineItem.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .order(order6) +// .product(phone) +// .quantity(1) // 1 phone +// .pricePerUnit(new BigDecimal("450.00")) // $450 each +// .build(); + +// SalesLineItem item6_2 = SalesLineItem.builder() +// .id(UUID.randomUUID()) +// .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 index acab99f..225bd12 100644 --- a/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java +++ b/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java @@ -1,155 +1,164 @@ -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 +// 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 java.util.UUID; + +// 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() +// .id(UUID.randomUUID()) +// .city("Test City") +// .state("Test State") +// .country("Test Country") +// .postalCode("12345") +// .build(); +// region = regionRepository.save(region); + +// // Create courier +// Courier courier = Courier.builder() +// .id(UUID.randomUUID()) +// .name("Test Courier") +// .status(Courier.CourierStatus.ACTIVE) +// .build(); +// courier = courierRepository.save(courier); + +// // Create customer +// Customer customer = Customer.builder() +// .id(UUID.randomUUID()) +// .name("Test Customer") +// .build(); +// customer = customerRepository.save(customer); + +// // Create products +// Product product1 = Product.builder() +// .id(UUID.randomUUID()) +// .name("Phone Case") +// .category("Accessories") +// .build(); + +// Product product2 = Product.builder() +// .id(UUID.randomUUID()) +// .name("Wireless Mouse") +// .category("Electronics") +// .build(); + +// productRepository.saveAll(List.of(product1, product2)); + +// // Create order with all required relationships +// Order order1 = Order.builder() +// .id(UUID.randomUUID()) +// .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() +// .id(UUID.randomUUID()) +// .order(order1) +// .product(product1) +// .quantity(2) +// .pricePerUnit(new BigDecimal("10.00")) +// .build(); + +// SalesLineItem item2 = SalesLineItem.builder() +// .id(UUID.randomUUID()) +// .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/CourierAnalyticsServiceTest.java b/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java index 1cb8c82..52a5dba 100644 --- a/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java +++ b/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -50,7 +51,7 @@ void setUp() { } private CourierPerformanceProjection createMockProjection( - Long courierId, String courierName, Long deliveryCount, Long completedCount, BigDecimal averageRating) { + UUID courierId, String courierName, Long deliveryCount, Long completedCount, BigDecimal averageRating) { CourierPerformanceProjection mockProjection = Mockito.mock(CourierPerformanceProjection.class); Mockito.lenient().when(mockProjection.getCourierId()).thenReturn(courierId); Mockito.lenient().when(mockProjection.getCourierName()).thenReturn(courierName); @@ -63,10 +64,12 @@ private CourierPerformanceProjection createMockProjection( @Test void getCourierDeliveryCounts_shouldReturnCorrectCountsForMultipleCouriers() { // Arrange + UUID courierId1 = UUID.randomUUID(); + UUID courierId2 = UUID.randomUUID(); CourierPerformanceProjection janeData = createMockProjection( - 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + courierId1, "Jane", 10L, 8L, new BigDecimal("4.5")); CourierPerformanceProjection johnData = createMockProjection( - 2L, "John", 5L, 5L, new BigDecimal("4.0")); + courierId2, "John", 5L, 5L, new BigDecimal("4.0")); when(courierRepository.findCourierPerformanceBetweenDates( any(LocalDateTime.class), any(LocalDateTime.class))) @@ -84,14 +87,14 @@ void getCourierDeliveryCounts_shouldReturnCorrectCountsForMultipleCouriers() { .filter(r -> r.getCourierName().equals("Jane")) .findFirst().orElse(null); assertNotNull(janeResponse); - assertEquals(1L, janeResponse.getCourierId()); + assertEquals(courierId1, janeResponse.getCourierId()); assertEquals(10, janeResponse.getDeliveryCount()); CourierDeliveryCountResponse johnResponse = result.stream() .filter(r -> r.getCourierName().equals("John")) .findFirst().orElse(null); assertNotNull(johnResponse); - assertEquals(2L, johnResponse.getCourierId()); + assertEquals(courierId2, johnResponse.getCourierId()); assertEquals(5, johnResponse.getDeliveryCount()); // Verify repository method was called with correct LocalDateTime arguments @@ -122,14 +125,17 @@ void getCourierDeliveryCounts_shouldReturnEmptyListWhenNoData() { void getCourierSuccessRate_shouldReturnCorrectRates() { // Arrange // Jane: 8 completed out of 10 deliveries = 80% + UUID courierId1 = UUID.randomUUID(); + UUID courierId2 = UUID.randomUUID(); + UUID courierId3 = UUID.randomUUID(); CourierPerformanceProjection janeData = createMockProjection( - 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + courierId1, "Jane", 10L, 8L, new BigDecimal("4.5")); // John: 5 completed out of 5 deliveries = 100% CourierPerformanceProjection johnData = createMockProjection( - 2L, "John", 5L, 5L, new BigDecimal("4.0")); + courierId2, "John", 5L, 5L, new BigDecimal("4.0")); // Peter: 0 completed out of 2 deliveries = 0% CourierPerformanceProjection peterData = createMockProjection( - 3L, "Peter", 2L, 0L, new BigDecimal("3.0")); + courierId3, "Peter", 2L, 0L, new BigDecimal("3.0")); when(courierRepository.findCourierPerformanceBetweenDates( any(LocalDateTime.class), any(LocalDateTime.class))) @@ -147,21 +153,21 @@ void getCourierSuccessRate_shouldReturnCorrectRates() { .filter(r -> r.getCourierName().equals("Jane")) .findFirst().orElse(null); assertNotNull(janeResponse); - assertEquals(1L, janeResponse.getCourierId()); + assertEquals(courierId1, janeResponse.getCourierId()); assertTrue(janeResponse.getSuccessRate().compareTo(new BigDecimal("0.80")) == 0); CourierSuccessRateResponse johnResponse = result.stream() .filter(r -> r.getCourierName().equals("John")) .findFirst().orElse(null); assertNotNull(johnResponse); - assertEquals(2L, johnResponse.getCourierId()); + assertEquals(courierId2, johnResponse.getCourierId()); assertTrue(johnResponse.getSuccessRate().compareTo(new BigDecimal("1.00")) == 0); CourierSuccessRateResponse peterResponse = result.stream() .filter(r -> r.getCourierName().equals("Peter")) .findFirst().orElse(null); assertNotNull(peterResponse); - assertEquals(3L, peterResponse.getCourierId()); + assertEquals(courierId3, peterResponse.getCourierId()); assertTrue(peterResponse.getSuccessRate().compareTo(new BigDecimal("0.00")) == 0); Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( @@ -173,8 +179,9 @@ void getCourierSuccessRate_shouldHandleZeroDeliveryCountGracefully() { // Arrange // Mark: 0 completed out of 0 deliveries. MetricCalculator should handle this // (e.g., return 0 or null) + UUID MarkId = UUID.randomUUID(); CourierPerformanceProjection markData = createMockProjection( - 4L, "Mark", 0L, 0L, new BigDecimal("0.0")); + MarkId, "Mark", 0L, 0L, new BigDecimal("0.0")); when(courierRepository.findCourierPerformanceBetweenDates( any(LocalDateTime.class), any(LocalDateTime.class))) @@ -188,7 +195,7 @@ void getCourierSuccessRate_shouldHandleZeroDeliveryCountGracefully() { assertNotNull(result); assertEquals(1, result.size()); CourierSuccessRateResponse markResponse = result.get(0); - assertEquals(4L, markResponse.getCourierId()); + assertEquals(MarkId, markResponse.getCourierId()); assertTrue(markResponse.getSuccessRate().compareTo(new BigDecimal("0.00")) == 0); Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( @@ -216,14 +223,17 @@ void getCourierSuccessRate_shouldReturnEmptyListWhenNoData() { @Test void getCourierAverageRating_shouldReturnCorrectAverageRatings() { + UUID courierId1 = UUID.randomUUID(); + UUID courierId2 = UUID.randomUUID(); + UUID courierId3 = UUID.randomUUID(); // Arrange CourierPerformanceProjection janeData = createMockProjection( - 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + courierId1, "Jane", 10L, 8L, new BigDecimal("4.5")); CourierPerformanceProjection johnData = createMockProjection( - 2L, "John", 5L, 5L, new BigDecimal("4.0")); + courierId2, "John", 5L, 5L, new BigDecimal("4.0")); // Peter: No rating available or 0.0 rating (depends on projection and database) CourierPerformanceProjection peterData = createMockProjection( - 3L, "Peter", 2L, 0L, null); // Assuming null for no rating + courierId3, "Peter", 2L, 0L, null); // Assuming null for no rating when(courierRepository.findCourierPerformanceBetweenDates( any(LocalDateTime.class), any(LocalDateTime.class))) @@ -241,14 +251,14 @@ void getCourierAverageRating_shouldReturnCorrectAverageRatings() { .filter(r -> r.getCourierName().equals("Jane")) .findFirst().orElse(null); assertNotNull(janeResponse); - assertEquals(1L, janeResponse.getCourierId()); + assertEquals(courierId1, janeResponse.getCourierId()); assertEquals(new BigDecimal("4.5"), janeResponse.getAverageRating()); CourierAverageRatingResponse johnResponse = result.stream() .filter(r -> r.getCourierName().equals("John")) .findFirst().orElse(null); assertNotNull(johnResponse); - assertEquals(2L, johnResponse.getCourierId()); + assertEquals(courierId2, johnResponse.getCourierId()); assertEquals(new BigDecimal("4.0"), johnResponse.getAverageRating()); CourierAverageRatingResponse peterResponse = result.stream() @@ -256,7 +266,7 @@ void getCourierAverageRating_shouldReturnCorrectAverageRatings() { .findFirst().orElse(null); assertNotNull(peterResponse); assertNull(peterResponse.getAverageRating()); - + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( expectedStartDateTime, expectedEndDateTime); } @@ -284,11 +294,13 @@ void getCourierAverageRating_shouldReturnEmptyListWhenNoData() { void getCourierPerformanceReport_shouldReturnComprehensiveReport() { // Arrange // Jane: 8 completed out of 10 deliveries = 80%, Avg Rating 4.5 + UUID courierId1 = UUID.randomUUID(); + UUID courierId2 = UUID.randomUUID(); CourierPerformanceProjection janeData = createMockProjection( - 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + courierId1, "Jane", 10L, 8L, new BigDecimal("4.5")); // John: 5 completed out of 5 deliveries = 100%, Avg Rating 4.0 CourierPerformanceProjection johnData = createMockProjection( - 2L, "John", 5L, 5L, new BigDecimal("4.0")); + courierId2, "John", 5L, 5L, new BigDecimal("4.0")); when(courierRepository.findCourierPerformanceBetweenDates( any(LocalDateTime.class), any(LocalDateTime.class))) @@ -306,7 +318,7 @@ void getCourierPerformanceReport_shouldReturnComprehensiveReport() { .filter(r -> r.getCourierName().equals("Jane")) .findFirst().orElse(null); assertNotNull(janeResponse); - assertEquals(1L, janeResponse.getCourierId()); + assertEquals(courierId1, janeResponse.getCourierId()); assertEquals(10, janeResponse.getDeliveryCount()); assertTrue(janeResponse.getSuccessRate().compareTo(new BigDecimal("0.80")) == 0); assertTrue(janeResponse.getAverageRating().compareTo(new BigDecimal("4.5")) == 0); @@ -315,7 +327,7 @@ void getCourierPerformanceReport_shouldReturnComprehensiveReport() { .filter(r -> r.getCourierName().equals("John")) .findFirst().orElse(null); assertNotNull(johnResponse); - assertEquals(2L, johnResponse.getCourierId()); + assertEquals(courierId2, johnResponse.getCourierId()); assertEquals(5, johnResponse.getDeliveryCount()); assertTrue(johnResponse.getSuccessRate().compareTo(new BigDecimal("1.00")) == 0); assertTrue(johnResponse.getAverageRating().compareTo(new BigDecimal("4.0")) == 0); diff --git a/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java index fb2b5ee..6d6ecd8 100644 --- a/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java +++ b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; // Keep import if TopSellerRequest still uses LocalDate +import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; // Import LocalDateTime import static org.junit.jupiter.api.Assertions.assertTrue; @@ -56,41 +57,43 @@ void getTopSellers_SortByRevenue_ShouldReturnCorrectList() { // Start of the day AFTER the end day to include the whole end day in the query LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + UUID productId1 = UUID.randomUUID(); + UUID productId2 = UUID.randomUUID(); // 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) - ); + createProjection(productId1, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(productId2, "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); + eq(startDate), + eq(endDate), + eq(2), + eq("REVENUE"))) + .thenReturn(projections); // Act - List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + 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())); + 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(productId2, 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(productId1, 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 @@ -108,33 +111,34 @@ void getTopSellers_SortByUnits_ShouldReturnCorrectList() { LocalDateTime startDate = requestStartDate.atStartOfDay(); LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + UUID productId1 = UUID.randomUUID(); + UUID productId2 = UUID.randomUUID(); List projections = Arrays.asList( - createProjection(1L, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), - createProjection(2L, "MacBook", "Electronics", new BigDecimal("2000.00"), 2L) - ); + createProjection(productId1, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(productId2, "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); + eq(startDate), + eq(endDate), + eq(2), + eq("UNITS"))) + .thenReturn(projections); // Act - List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + 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(productId1, 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. + // Note: The projection returns revenue and units as BigDecimal and Long + // respectively. assertEquals(new BigDecimal("5"), result.get(0).getValue()); - - assertEquals(2L, result.get(1).getProductId()); + assertEquals(productId2, result.get(1).getProductId()); assertEquals("MacBook", result.get(1).getProductName()); assertEquals("Electronics", result.get(1).getCategory()); assertEquals(new BigDecimal("2"), result.get(1).getValue()); @@ -155,16 +159,16 @@ void getTopSellers_WithEmptyResult_ShouldReturnEmptyList() { // Use any() matchers for LocalDateTime parameters when(productRepository.findTopSellers(any(LocalDateTime.class), any(LocalDateTime.class), any(), any())) - .thenReturn(Collections.emptyList()); + .thenReturn(Collections.emptyList()); // Act - List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + List result = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), request.getLimit(), request.getSortBy()); // Assert assertTrue(result.isEmpty()); } - @Test void getTopSellers_WithZeroLimit_ShouldReturnEmptyList() { // Arrange @@ -181,45 +185,50 @@ void getTopSellers_WithZeroLimit_ShouldReturnEmptyList() { 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()); + eq(startDate), + eq(endDate), + eq(0), + eq("REVENUE"))) + .thenReturn(Collections.emptyList()); // Act - List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + List result = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), request.getLimit(), request.getSortBy()); // Assert assertTrue(result.isEmpty()); } private TopSellingProductProjection createProjection( - final Long id, + final UUID id, final String name, final String category, final BigDecimal revenue, final Long units) { return new TopSellingProductProjection() { + @Override - public Long getId() { + public UUID 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; diff --git a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java index c578ea0..f007740 100644 --- a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java +++ b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java @@ -49,12 +49,11 @@ void getRevenueSummary_WithValidData_ShouldReturnCorrectSummary() { .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")) - ); + 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); + .thenReturn(projections); // Act List result = revenueReportService.getRevenueSummary(request.getStartDate(), @@ -80,7 +79,7 @@ void getRevenueSummary_WithEmptyData_ShouldReturnEmptyList() { .build(); when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) - .thenReturn(Collections.emptyList()); + .thenReturn(Collections.emptyList()); // Act List result = revenueReportService.getRevenueSummary(request.getStartDate(), @@ -102,7 +101,7 @@ void getRevenueSummary_WithStartDateAfterEndDate_ShouldReturnEmptyList() { .build(); when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) - .thenReturn(Collections.emptyList()); + .thenReturn(Collections.emptyList()); // Act List result = revenueReportService.getRevenueSummary(request.getStartDate(), @@ -116,13 +115,13 @@ void getRevenueSummary_WithStartDateAfterEndDate_ShouldReturnEmptyList() { 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")) - ); + 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 + .thenReturn(projections);// Act List result = revenueReportService.getRevenueByCategory(startDate, endDate); // Assert @@ -140,7 +139,7 @@ void getRevenueByCategory_WithEmptyData_ShouldReturnEmptyList() { LocalDate endDate = LocalDate.of(2025, 12, 31); when(orderRepository.findRevenueByCategory(any(), any())) - .thenReturn(Collections.emptyList()); + .thenReturn(Collections.emptyList()); // Act List result = revenueReportService.getRevenueByCategory(startDate, endDate); @@ -156,20 +155,20 @@ void getRevenueByCategory_WithNullRevenue_ShouldHandleGracefully() { 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; - } - } - ); + new RevenueByCategoryProjection() { + @Override + public String getCategory() { + return "Electronics"; + } + + @Override + public BigDecimal getTotalRevenue() { + return null; + } + }); when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) - .thenReturn(projections); + .thenReturn(projections); // Act List result = revenueReportService.getRevenueByCategory(startDate, endDate); @@ -187,7 +186,7 @@ void getRevenueByCategory_WithStartDateAfterEndDate_ShouldReturnEmptyList() { LocalDate endDate = LocalDate.of(2025, 1, 1); when(orderRepository.findRevenueByCategory(any(), any())) - .thenReturn(Collections.emptyList()); + .thenReturn(Collections.emptyList()); // Act List result = revenueReportService.getRevenueByCategory(startDate, endDate); @@ -195,17 +194,28 @@ void getRevenueByCategory_WithStartDateAfterEndDate_ShouldReturnEmptyList() { // Assert assertTrue(result.isEmpty()); } + private RevenueSummaryProjection summaryProjection(LocalDate date, BigDecimal revenue) { - return new RevenueSummaryProjection() { - public LocalDate getPeriod() { return date; } - public BigDecimal getTotalRevenue() { return 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; } + public String getCategory() { + return category; + } + + public BigDecimal getTotalRevenue() { + return revenue; + } }; } }