Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package com.Podzilla.analytics.api.controllers;

import org.springframework.http.ResponseEntity;
// import org.springframework.web.bind.MethodArgumentNotValidException;
// import org.springframework.web.bind.MissingServletRequestParameterException;
// import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// import org.springframework.web.method.annotation.
// MethodArgumentTypeMismatchException;

import com.Podzilla.analytics.api.dtos.fulfillment
.FulfillmentPlaceToShipRequest;
import com.Podzilla.analytics.api.dtos.fulfillment
.FulfillmentShipToDeliverRequest;
import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest;
import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest;
import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse;
import com.Podzilla.analytics.services.FulfillmentAnalyticsService;

Expand All @@ -27,34 +30,37 @@
public class FulfillmentReportController {
private final FulfillmentAnalyticsService fulfillmentAnalyticsService;

@Operation(summary = "Get average time from order placement to shipping",
description = "Returns the average time (in hours) between when"
@Operation(
summary = "Get average time from order placement to shipping",
description = "Returns the average time (in hours) between when"
+ " an order was placed and when it was shipped, grouped"
+ " by the specified dimension")
+ " by the specified dimension"
)
@GetMapping("/place-to-ship-time")
public ResponseEntity<List<FulfillmentTimeResponse>> getPlaceToShipTime(
@Valid @ModelAttribute final FulfillmentPlaceToShipRequest req) {

List<FulfillmentTimeResponse> reportData = fulfillmentAnalyticsService
.getPlaceToShipTimeResponse(
final List<FulfillmentTimeResponse> reportData =
fulfillmentAnalyticsService.getPlaceToShipTimeResponse(
req.getStartDate(),
req.getEndDate(),
req.getGroupBy());
return ResponseEntity.ok(reportData);
}

@Operation(summary = "Get average time from shipping to delivery",
description = "Returns the average time (in hours) between when"

@Operation(
summary = "Get average time from shipping to delivery",
description = "Returns the average time (in hours) between when"
+ " an order was shipped and when it was delivered, grouped"
+ " by the specified dimension")
+ " by the specified dimension"
)
@GetMapping("/ship-to-deliver-time")
public ResponseEntity<List<FulfillmentTimeResponse>> getShipToDeliverTime(
@Valid @ModelAttribute final FulfillmentShipToDeliverRequest req) {

log.debug(req.toString());

List<FulfillmentTimeResponse> reportData = fulfillmentAnalyticsService
.getShipToDeliverTimeResponse(
final List<FulfillmentTimeResponse> reportData =
fulfillmentAnalyticsService.getShipToDeliverTimeResponse(
req.getStartDate(),
req.getEndDate(),
req.getGroupBy());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,48 @@
package com.Podzilla.analytics.api.controllers;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.Podzilla.analytics.api.dtos.DateRangeRequest;
import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory;
import com.Podzilla.analytics.services.ProfitAnalyticsService;

import io.swagger.v3.oas.annotations.Operation;
// import io.swagger.v3.oas.annotations.responses.ApiResponse;
// import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

/**
* REST controller for profit analytics operations.
* Provides endpoints to analyze revenue, cost, and profit metrics.
*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/profit-analytics")
public class ProfitReportController {
private final ProfitAnalyticsService profitAnalyticsService;


@Operation(
summary = "Get profit by product category",
description = "Returns the revenue, cost, and profit metrics "
+ "grouped by product category")
@GetMapping("/by-category")
public ResponseEntity<List<ProfitByCategory>> getProfitByCategory(
@Valid @ModelAttribute final DateRangeRequest request) {

List<ProfitByCategory> profitData =
profitAnalyticsService.getProfitByCategory(
request.getStartDate(),
request.getEndDate());
return ResponseEntity.ok(profitData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public enum PlaceToShipGroupBy {
@NotNull(message = "groupBy is required")
@Schema(description = "How to group the results (OVERALL, REGION, COURIER "
+ "depending on endpoint)", example = "OVERALL", required = true)
private PlaceToShipGroupBy groupBy = PlaceToShipGroupBy.OVERALL;
private PlaceToShipGroupBy groupBy;

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ public enum ShipToDeliverGroupBy {
@NotNull(message = "groupBy is required")
@Schema(description = "How to group the results (OVERALL, REGION, COURIER "
+ "depending on endpoint)", example = "OVERALL", required = true)
private ShipToDeliverGroupBy groupBy = ShipToDeliverGroupBy.OVERALL;
private ShipToDeliverGroupBy groupBy;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.Podzilla.analytics.api.dtos.profit;

import java.math.BigDecimal;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProfitByCategory {

@Schema(description = "Product category name", example = "Electronics")
private String category;

@Schema(description = "Total revenue for the category in the given period",
example = "15000.50")
private BigDecimal totalRevenue;

@Schema(description = "Total cost for the category in the given period",
example = "10000.25")
private BigDecimal totalCost;

@Schema(description = "Gross profit (revenue - cost)",
example = "5000.25")
private BigDecimal grossProfit;

@Schema(description = "Gross profit margin percentage",
example = "33.33")
private BigDecimal grossProfitMargin;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.Podzilla.analytics.api.projections.profit;

import java.math.BigDecimal;

/**
* Projection interface for profit by category query results
*/
public interface ProfitByCategoryProjection {
String getCategory();
BigDecimal getTotalRevenue();
BigDecimal getTotalCost();
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
package com.Podzilla.analytics.repositories;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.Podzilla.analytics.models.SalesLineItem;
import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection;

import java.time.LocalDateTime;
import java.util.List;

public interface SalesLineItemRepository
extends JpaRepository<SalesLineItem, Long> {
extends JpaRepository<SalesLineItem, Long> {
@Query("SELECT sli.product.category as category, "
+ "SUM(sli.quantity * sli.pricePerUnit) as totalRevenue, "
+ "SUM(sli.quantity * sli.product.cost) as totalCost "
+ "FROM SalesLineItem sli "
+ "WHERE sli.order.orderPlacedTimestamp BETWEEN "
+ ":startDate AND :endDate "
+ "AND sli.order.status = 'COMPLETED' "
+ "GROUP BY sli.product.category")
List<ProfitByCategoryProjection> findSalesByCategoryBetweenDates(
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,62 @@

import org.springframework.stereotype.Service;

import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory;
import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection;
import com.Podzilla.analytics.repositories.SalesLineItemRepository;

import lombok.RequiredArgsConstructor;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
public class ProfitAnalyticsService {
private final SalesLineItemRepository salesLineItemRepository;
// Precision constant for percentage calculations
private static final int PERCENTAGE_PRECISION = 4;

public List<ProfitByCategory> getProfitByCategory(
final LocalDate startDate,
final LocalDate endDate) {
// Convert LocalDate to LocalDateTime for start of day and end of day
LocalDateTime startDateTime = startDate.atStartOfDay();
LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX);

List<ProfitByCategoryProjection> salesData = salesLineItemRepository
.findSalesByCategoryBetweenDates(startDateTime, endDateTime);

return salesData.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}

private ProfitByCategory convertToDTO(
final ProfitByCategoryProjection projection) {
BigDecimal totalRevenue = projection.getTotalRevenue();
BigDecimal totalCost = projection.getTotalCost();
BigDecimal grossProfit = totalRevenue.subtract(totalCost);

BigDecimal grossProfitMargin = BigDecimal.ZERO;
if (totalRevenue.compareTo(BigDecimal.ZERO) > 0) {
grossProfitMargin = grossProfit
.divide(totalRevenue, PERCENTAGE_PRECISION,
RoundingMode.HALF_UP)
.multiply(new BigDecimal("100"));
}

return ProfitByCategory.builder()
.category(projection.getCategory())
.totalRevenue(totalRevenue)
.totalCost(totalCost)
.grossProfit(grossProfit)
.grossProfitMargin(grossProfitMargin)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,33 @@
package com.Podzilla.analytics.validation.validators;

import com.Podzilla.analytics.api.dtos.DateRangeRequest;
import java.time.LocalDate;
import java.lang.reflect.Method;

import com.Podzilla.analytics.validation.annotations.ValidDateRange;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public final class DateRangeValidator implements
ConstraintValidator<ValidDateRange, DateRangeRequest> {
ConstraintValidator<ValidDateRange, Object> {
@Override
public boolean isValid(final DateRangeRequest request,
public boolean isValid(final Object value,
final ConstraintValidatorContext context) {
if (request.getStartDate() == null || request.getEndDate() == null) {
if (value == null) {
return true;
}
return request.getEndDate().isAfter(request.getStartDate());

try {
Method getStartDate = value.getClass().getMethod("getStartDate");
Method getEndDate = value.getClass().getMethod("getEndDate");
LocalDate startDate = (LocalDate) getStartDate.invoke(value);
LocalDate endDate = (LocalDate) getEndDate.invoke(value);
if (startDate == null || endDate == null) {
return true; // Let @NotNull handle this
}
return !endDate.isBefore(startDate);
} catch (Exception e) {
return false;
}
}
}
Loading