From 01519fb2fb7c58a3c580d7d070196fd8246cd6ef Mon Sep 17 00:00:00 2001 From: Abdulrahman Fahmy Date: Fri, 2 May 2025 22:03:58 +0300 Subject: [PATCH 01/28] Setup (#1) * chore: setup & eventhandler * chore: setup & eventhandler * chore: models * chore(setup):docker * chore: show empty folders --- .gitattributes | 2 + .gitignore | 33 +++ .mvn/wrapper/maven-wrapper.properties | 19 ++ docker-compose.yml | 64 +++++ dockerfile | 15 + mvnw | 259 ++++++++++++++++++ mvnw.cmd | 149 ++++++++++ pom.xml | 108 ++++++++ .../analytics/AnalyticsApplication.java | 13 + .../com/Podzilla/analytics/api/DTOs/.gitkeep | 0 .../analytics/api/controller/.gitkeep | 0 .../analytics/eventhandler/DTOs/aDTO.java | 5 + .../eventhandler/EventHandlerDispatcher.java | 22 ++ .../analytics/eventhandler/IEventHandler.java | 6 + .../eventHandlerDispatcherConfig.java | 21 ++ .../eventhandler/handlers/aHandler.java | 13 + .../analytics/messaging/RabbitListener.java | 11 + .../analytics/models/CourierAnalytic.java | 34 +++ .../analytics/models/CustomerAnalytic.java | 42 +++ .../analytics/models/OrderAnalytics.java | 62 +++++ .../analytics/models/WarehouseAnalytic.java | 44 +++ .../CourierAnalyticRepository.java | 21 ++ .../CustomerAnalyticRepository.java | 16 ++ .../repositories/OrderAnalyticRepository.java | 18 ++ .../WarehouseAnalyticRepository.java | 17 ++ .../com/Podzilla/analytics/services/.gitkeep | 0 src/main/resources/application.properties | 5 + .../analytics/AnalyticsApplicationTests.java | 13 + 28 files changed, 1012 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 docker-compose.yml create mode 100644 dockerfile create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/Podzilla/analytics/AnalyticsApplication.java create mode 100644 src/main/java/com/Podzilla/analytics/api/DTOs/.gitkeep create mode 100644 src/main/java/com/Podzilla/analytics/api/controller/.gitkeep create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/IEventHandler.java create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java create mode 100644 src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java create mode 100644 src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java create mode 100644 src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java create mode 100644 src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/services/.gitkeep create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/Podzilla/analytics/AnalyticsApplicationTests.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..135a31f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ + +services: + + # Database Service (PostgreSQL) + db: + image: postgres:15-alpine # Use a lightweight PostgreSQL image + container_name: analytics-db + ports: + - "5432:5432" # Map host port 5432 to container port 5432 (optional, useful for direct access) + environment: + POSTGRES_DB: analytics_db_dev # Database name + POSTGRES_USER: analytics_user # Database user + POSTGRES_PASSWORD: password # Database password + volumes: + - db_data:/var/lib/postgresql/data # Persist data to a named volume + healthcheck: # Optional but recommended health check + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + + # Message Queue Service (RabbitMQ with Management UI) + rabbitmq: + image: rabbitmq:3.12-management-alpine # Use RabbitMQ with the management plugin + container_name: analytics-rabbitmq + ports: + - "5672:5672" # Default AMQP port + - "15672:15672" # Management UI port (access at http://localhost:15672) + environment: + RABBITMQ_DEFAULT_USER: analytics_mq_user # RabbitMQ user + RABBITMQ_DEFAULT_PASS: analytics_mq_password # RabbitMQ password + healthcheck: # Basic health check for RabbitMQ + test: ["CMD", "rabbitmq-diagnostics", "check_system_status"] + interval: 10s + timeout: 5s + retries: 5 + + # Your Spring Boot Application Service + analytics-app: + build: . # Build the Docker image using the Dockerfile in the current directory + container_name: analytics-app + ports: + - "8080:8080" # Map host port 8080 to the app's internal port 8080 (or your custom port) + environment: # Pass environment variables to your Spring Boot app + # Database Configuration (Matches application.yml property names often via Spring Boot convention) + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db # 'db' is the service name in docker-compose + SPRING_DATASOURCE_USERNAME: analytics_user + SPRING_DATASOURCE_PASSWORD: analytics_password + SPRING_JPA_HIBERNATE_DDL_AUTO: update # Set DDL auto behavior (be cautious in prod!) + # RabbitMQ Configuration + SPRING_RABBITMQ_HOST: rabbitmq # 'rabbitmq' is the service name in docker-compose + SPRING_RABBITMQ_PORT: 5672 + SPRING_RABBITMQ_USERNAME: analytics_mq_user + SPRING_RABBITMQ_PASSWORD: analytics_mq_password + # Add any other necessary Spring Boot properties via environment variables + depends_on: # Ensure DB and RabbitMQ are running before starting the app + db: + condition: service_healthy # Wait for DB health check to pass + rabbitmq: + condition: service_healthy # Wait for RabbitMQ health check to pass + +# Define named volumes for data persistence +volumes: + db_data: # Data volume for the database \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..15a5981 --- /dev/null +++ b/dockerfile @@ -0,0 +1,15 @@ +# Stage 1: Build the application (This stage has JDK and Maven) +FROM maven:3.8.7-openjdk-21 AS builder # Use a Maven image with JDK 21 +WORKDIR /app +COPY pom.xml . # Copy the build file +COPY src ./src # <--- This is where you copy the source code in the BUILD stage +RUN mvn clean package -DskipTests # Build the project (skipping tests to speed up build) + +# Stage 2: Run the application (This stage has just a JRE) +FROM eclipse-temurin:21-jre-alpine # Use a smaller JRE image for running +WORKDIR /app +# Copy only the JAR artifact from the 'builder' stage to the current stage +COPY --from=builder /app/target/analytics-monolith-0.0.1-SNAPSHOT.jar /app/app.jar + +ENTRYPOINT ["java", "-jar", "app.jar"] +EXPOSE 8080 \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d01794a --- /dev/null +++ b/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + com.Podzilla + analytics + 0.0.1-SNAPSHOT + analytics + The Operational Analytics Service is an event-driven application designed to capture, process, and expose key operational data and derived insights from various upstream microservices (e.g., Warehouse, Courier, Order services). It acts as a centralized source of truth for historical operational events and their derived state, providing valuable analytics through a dedicated API + + + + + + + + + + + + + + + 21 + + + + io.github.cdimascio + java-dotenv + 5.2.2 + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.amqp + spring-rabbit-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java new file mode 100644 index 0000000..eb12c98 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java @@ -0,0 +1,13 @@ +package com.Podzilla.analytics; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AnalyticsApplication { + + public static void main(String[] args) { + SpringApplication.run(AnalyticsApplication.class, args); + } + +} diff --git a/src/main/java/com/Podzilla/analytics/api/DTOs/.gitkeep b/src/main/java/com/Podzilla/analytics/api/DTOs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/Podzilla/analytics/api/controller/.gitkeep b/src/main/java/com/Podzilla/analytics/api/controller/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java b/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java new file mode 100644 index 0000000..5d7694e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java @@ -0,0 +1,5 @@ +package com.Podzilla.analytics.eventhandler.DTOs; +// TODO remove this example +public class aDTO { + String a; +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java new file mode 100644 index 0000000..f5077db --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.eventhandler; + +import java.util.HashMap; + +public class EventHandlerDispatcher { + // dto , event + HashMap, IEventHandler> handlers; + + public void registerHandler(Class dto, IEventHandler handler) { + handlers.put(dto, handler); + } + + @SuppressWarnings("unchecked") + public void dispatch(T dto) { + IEventHandler handler = (IEventHandler) handlers.get(dto.getClass()); + if (handler != null) { + handler.handle(dto); + } else { + throw new RuntimeException("No handler found for: " + dto.getClass()); + } + } +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/IEventHandler.java b/src/main/java/com/Podzilla/analytics/eventhandler/IEventHandler.java new file mode 100644 index 0000000..c0edc83 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/IEventHandler.java @@ -0,0 +1,6 @@ +package com.Podzilla.analytics.eventhandler; + + +public interface IEventHandler { //T should be the DTO of the event + void handle(T eventDto); +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java b/src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java new file mode 100644 index 0000000..adb8f9b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java @@ -0,0 +1,21 @@ +package com.Podzilla.analytics.eventhandler; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.Podzilla.analytics.eventhandler.DTOs.aDTO; +import com.Podzilla.analytics.eventhandler.handlers.aHandler; + +@Configuration +public class eventHandlerDispatcherConfig { + + @Bean + public EventHandlerDispatcher commandDispatcher() { + EventHandlerDispatcher dispatcher = new EventHandlerDispatcher(); + + //TODO should add all the events here + //Example: + //dispatcher.registerHandler(aDTO.class, new aHandler()); + return dispatcher; + } +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java b/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java new file mode 100644 index 0000000..be66e00 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java @@ -0,0 +1,13 @@ +package com.Podzilla.analytics.eventhandler.handlers; + +import com.Podzilla.analytics.eventhandler.IEventHandler; +import com.Podzilla.analytics.eventhandler.DTOs.aDTO; + +public class aHandler implements IEventHandler{ + // TODO remove this example + @Override + public void handle(aDTO eventDto) { + + } + +} diff --git a/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java new file mode 100644 index 0000000..7113e03 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.messaging; + +import org.springframework.beans.factory.annotation.Autowired; + +import com.Podzilla.analytics.eventhandler.EventHandlerDispatcher; + +public class RabbitListener { + + @Autowired + EventHandlerDispatcher dispatcher; +} diff --git a/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java b/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java new file mode 100644 index 0000000..a209ee7 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java @@ -0,0 +1,34 @@ +package com.Podzilla.analytics.models; + +import jakarta.persistence.*; +import java.time.Instant; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +// NOTE: this is AI generated + + + + + +@Entity +@Table(name = "courier_analytics") +@Data // Generates getters, setters, toString, equals, hashCode +@NoArgsConstructor @AllArgsConstructor +public class CourierAnalytic { + + @Id // Primary Key + private String analyticId; + + private Instant dispatchTimestamp; + + private String courierId; + + private long duration; // Using long for duration + + private boolean orderDelivered; + + private double rating; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java b/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java new file mode 100644 index 0000000..6bf5f10 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java @@ -0,0 +1,42 @@ +package com.Podzilla.analytics.models; + + +import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions +import java.time.Instant; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + + + + + + + +// NOTE: this is AI generated + + + + + + +@Entity +@Table(name = "customer_analytics") // Table name in DB +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CustomerAnalytic { + + @Id // Primary Key + private String analyticId; + + private Instant timestamp; + + private String customerId; + + private double totalAmount; // Note: BigDecimal is generally preferred for currency + + private long duration; // Using long for duration + + private double rating; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java b/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java new file mode 100644 index 0000000..0b62baa --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java @@ -0,0 +1,62 @@ +package com.Podzilla.analytics.models; + + +import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions +import java.time.Instant; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + + + + + + + +// NOTE: this is AI generated + + + + + + + + + + + + + + + +@Entity +@Table(name = "order_analytics") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OrderAnalytics { + + @Id // Primary Key + private String analyticId; // Using analyticId as the primary key for this derived/analytic Order record + + private Instant timestamp; + + private String customerId; // Assuming 'customerID' was a typo + + private double totalAmount; // Note: BigDecimal is generally preferred for currency + + private double rating; + + // Note: This looks like the original Order ID, distinct from analyticId + // If 'analyticId' is a *new* ID for the analytic record, and 'orderID' is the *original* order ID, + // you might make orderID a natural key if needed for lookups, but analyticId is the PK here. + @Column(name = "original_order_id") // Give it a clear column name if different from field name + private String orderId; // Assuming 'orderID' was a typo + + // Mapping the status string directly + private String status; // Stores "completed", "failed", "inprogress" as text + // Alternatively, you could use an Enum if the set of statuses is fixed and small + // @Enumerated(EnumType.STRING) + // private OrderStatus status; // Need to define OrderStatus enum + +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java b/src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java new file mode 100644 index 0000000..48414d6 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java @@ -0,0 +1,44 @@ +package com.Podzilla.analytics.models; + +import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions +import java.time.Instant; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + + + + + +// NOTE: this is AI generated + + + + + + + + + +@Entity +@Table(name = "warehouse_analytics") // Table name in DB +@Data +@NoArgsConstructor +@AllArgsConstructor +public class WarehouseAnalytic { + + @Id // Primary Key + private String analyticId; + + private String productId; // Assuming 'productid' was a typo + + private Instant timestamp; + + private int currentQuantity; // Assuming 'current Quantity' was a typo + + private int soldQuantity; // Assuming 'sold quantity' was a typo + + private double profit; // Note: BigDecimal is generally preferred for currency/profit + + private boolean isLow; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java new file mode 100644 index 0000000..3d41a28 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java @@ -0,0 +1,21 @@ +package com.Podzilla.analytics.repositories; + +// Place in com.podzilla.erp.analytics.repository + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.Podzilla.analytics.models.CourierAnalytic; + +@Repository // Optional annotation for interfaces, but good practice +public interface CourierAnalyticRepository extends JpaRepository { + // Spring Data JPA automatically provides: + // save(CourierAnalytic entity) + // findById(String id) + // findAll() + // deleteById(String id) + // etc. + + // Add custom query methods here if needed later, e.g., + // List findByCourierIdAndDispatchTimestampBetween(String courierId, Instant start, Instant end); +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java new file mode 100644 index 0000000..c2b2335 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java @@ -0,0 +1,16 @@ +package com.Podzilla.analytics.repositories; + +// Place in com.podzilla.erp.analytics.repository + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.Podzilla.analytics.models.CustomerAnalytic; + +@Repository +public interface CustomerAnalyticRepository extends JpaRepository { + // Automatic CRUD methods provided + + // Example custom query: + // List findByCustomerIdAndTimestampBetween(String customerId, Instant start, Instant end); +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java new file mode 100644 index 0000000..d0b2c34 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java @@ -0,0 +1,18 @@ +package com.Podzilla.analytics.repositories; + +// Place in com.podzilla.erp.analytics.repository + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.Podzilla.analytics.models.OrderAnalytics; + +@Repository +public interface OrderAnalyticRepository extends JpaRepository { + // Automatic CRUD methods provided + + // Example custom queries: + // List findByCustomerIdAndTimestampBetween(String customerId, Instant start, Instant end); + // List findByStatus(String status); + // Optional findByOrderId(String orderId); // If originalOrderId is unique and you need lookup by it +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java new file mode 100644 index 0000000..e1e9803 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.repositories; + + + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import com.Podzilla.analytics.models.WarehouseAnalytic; + +@Repository +public interface WarehouseAnalyticRepository extends JpaRepository { + // Automatic CRUD methods provided + + // Example custom queries: + // List findByProductIdAndTimestampBetween(String productId, Instant start, Instant end); + // List findByIsLowTrue(); // Finds entities where isLow is true +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/.gitkeep b/src/main/java/com/Podzilla/analytics/services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..b139bac --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.application.name=analytics +spring.datasource.url=jdbc:postgresql://localhost:5432/analytics_db_dev +spring.datasource.username=analytics_user +spring.datasource.password=password +spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/AnalyticsApplicationTests.java b/src/test/java/com/Podzilla/analytics/AnalyticsApplicationTests.java new file mode 100644 index 0000000..4a25c58 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/AnalyticsApplicationTests.java @@ -0,0 +1,13 @@ +// package com.Podzilla.analytics; + +// import org.junit.jupiter.api.Test; +// import org.springframework.boot.test.context.SpringBootTest; + +// @SpringBootTest +// class AnalyticsApplicationTests { + +// @Test +// void contextLoads() { +// } + +// } From 1dc8e1044dc32ce97dec4b1043160a40a3ccc7fd Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Fri, 2 May 2025 23:34:52 +0300 Subject: [PATCH 02/28] chore: create base structure for models, services, repositories, and controllers (#2) --- .../api/{controller => controllers}/.gitkeep | 0 .../controllers/CourierReportController.java | 14 +++++ .../controllers/CustomerReportController.java | 14 +++++ .../FulfillmentReportController.java | 14 +++++ .../InventoryReportController.java | 14 +++++ .../controllers/OrderReportController.java | 14 +++++ .../controllers/ProductReportController.java | 15 +++++ .../controllers/ProfitReportController.java | 14 +++++ .../controllers/RevenueReportController.java | 14 +++++ .../Podzilla/analytics/models/Courier.java | 26 ++++++++ .../analytics/models/CourierAnalytic.java | 34 ---------- .../Podzilla/analytics/models/Customer.java | 17 +++++ .../analytics/models/CustomerAnalytic.java | 42 ------------- .../analytics/models/InventorySnapshot.java | 25 ++++++++ .../com/Podzilla/analytics/models/Order.java | 58 +++++++++++++++++ .../analytics/models/OrderAnalytics.java | 62 ------------------- .../Podzilla/analytics/models/Product.java | 22 +++++++ .../com/Podzilla/analytics/models/Region.java | 20 ++++++ .../analytics/models/SalesLineItem.java | 28 +++++++++ .../analytics/models/WarehouseAnalytic.java | 44 ------------- .../CourierAnalyticRepository.java | 21 ------- .../repositories/CourierRepository.java | 8 +++ .../CustomerAnalyticRepository.java | 16 ----- .../repositories/CustomerRepository.java | 8 +++ .../InventorySnapshotRepository.java | 8 +++ .../repositories/OrderAnalyticRepository.java | 18 ------ .../repositories/OrderRepository.java | 8 +++ .../repositories/ProductRepository.java | 8 +++ .../repositories/RegionRepository.java | 8 +++ .../repositories/SalesLineItemRepository.java | 8 +++ .../WarehouseAnalyticRepository.java | 17 ----- .../services/CourierAnalyticsService.java | 11 ++++ .../services/CustomerAnalyticsService.java | 11 ++++ .../services/FulfillmentAnalyticsService.java | 11 ++++ .../services/InventoryAnalyticsService.java | 10 +++ .../services/OrderAnalyticsService.java | 11 ++++ .../services/ProductAnalyticsService.java | 11 ++++ .../services/ProfitAnalyticsService.java | 10 +++ .../services/RevenueReportService.java | 12 ++++ 39 files changed, 452 insertions(+), 254 deletions(-) rename src/main/java/com/Podzilla/analytics/api/{controller => controllers}/.gitkeep (100%) create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java create mode 100644 src/main/java/com/Podzilla/analytics/models/Courier.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java create mode 100644 src/main/java/com/Podzilla/analytics/models/Customer.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java create mode 100644 src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java create mode 100644 src/main/java/com/Podzilla/analytics/models/Order.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java create mode 100644 src/main/java/com/Podzilla/analytics/models/Product.java create mode 100644 src/main/java/com/Podzilla/analytics/models/Region.java create mode 100644 src/main/java/com/Podzilla/analytics/models/SalesLineItem.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java delete mode 100644 src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java delete mode 100644 src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java delete mode 100644 src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java delete mode 100644 src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/RevenueReportService.java diff --git a/src/main/java/com/Podzilla/analytics/api/controller/.gitkeep b/src/main/java/com/Podzilla/analytics/api/controllers/.gitkeep similarity index 100% rename from src/main/java/com/Podzilla/analytics/api/controller/.gitkeep rename to src/main/java/com/Podzilla/analytics/api/controllers/.gitkeep diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java new file mode 100644 index 0000000..d8d9869 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.CourierAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/couriers") +public class CourierReportController { + private final CourierAnalyticsService courierAnalyticsService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java new file mode 100644 index 0000000..d804a89 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.CustomerAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("customers") +public class CustomerReportController { + private final CustomerAnalyticsService customerAnalyticsService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java new file mode 100644 index 0000000..8f518ea --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.FulfillmentAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("fulfillment") +public class FulfillmentReportController { + private final FulfillmentAnalyticsService fulfillmentAnalyticsService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java new file mode 100644 index 0000000..7aabf74 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.InventoryAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("inventory") +public class InventoryReportController { + private final InventoryAnalyticsService inventoryAnalyticsService; +} diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java new file mode 100644 index 0000000..8110b47 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.OrderAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/orders") +public class OrderReportController { + private final OrderAnalyticsService orderAnalyticsService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java new file mode 100644 index 0000000..59af7d6 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -0,0 +1,15 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.ProductAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/products") +public class ProductReportController { + private final ProductAnalyticsService productAnalyticsService; + +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java new file mode 100644 index 0000000..15485be --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.ProfitAnalyticsService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("profit") +public class ProfitReportController { + private final ProfitAnalyticsService profitAnalyticsService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java new file mode 100644 index 0000000..8c68406 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.controllers; + +import org.springframework.web.bind.annotation.*; + +import com.Podzilla.analytics.services.RevenueReportService; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/revenue") +public class RevenueReportController { + private final RevenueReportService revenueReportService; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/Courier.java b/src/main/java/com/Podzilla/analytics/models/Courier.java new file mode 100644 index 0000000..81d84bc --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/Courier.java @@ -0,0 +1,26 @@ +package com.Podzilla.analytics.models; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "couriers") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Courier { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + + @Enumerated(EnumType.STRING) + private CourierStatus status; + + public enum CourierStatus { + ACTIVE, + INACTIVE, + SUSPENDED + } +} diff --git a/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java b/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java deleted file mode 100644 index a209ee7..0000000 --- a/src/main/java/com/Podzilla/analytics/models/CourierAnalytic.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.Podzilla.analytics.models; - -import jakarta.persistence.*; -import java.time.Instant; - -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - -// NOTE: this is AI generated - - - - - -@Entity -@Table(name = "courier_analytics") -@Data // Generates getters, setters, toString, equals, hashCode -@NoArgsConstructor @AllArgsConstructor -public class CourierAnalytic { - - @Id // Primary Key - private String analyticId; - - private Instant dispatchTimestamp; - - private String courierId; - - private long duration; // Using long for duration - - private boolean orderDelivered; - - private double rating; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/Customer.java b/src/main/java/com/Podzilla/analytics/models/Customer.java new file mode 100644 index 0000000..c535e5e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/Customer.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.models; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "customers") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Customer { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; +} diff --git a/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java b/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java deleted file mode 100644 index 6bf5f10..0000000 --- a/src/main/java/com/Podzilla/analytics/models/CustomerAnalytic.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.Podzilla.analytics.models; - - -import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions -import java.time.Instant; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - - - - - - - -// NOTE: this is AI generated - - - - - - -@Entity -@Table(name = "customer_analytics") // Table name in DB -@Data -@NoArgsConstructor -@AllArgsConstructor -public class CustomerAnalytic { - - @Id // Primary Key - private String analyticId; - - private Instant timestamp; - - private String customerId; - - private double totalAmount; // Note: BigDecimal is generally preferred for currency - - private long duration; // Using long for duration - - private double rating; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java b/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java new file mode 100644 index 0000000..1e4b416 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.models; + +import java.time.LocalDateTime; +import jakarta.persistence.*; +import lombok.*; + +@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 new file mode 100644 index 0000000..6e644dd --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/Order.java @@ -0,0 +1,58 @@ +package com.Podzilla.analytics.models; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "orders") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private BigDecimal totalAmount; + private LocalDateTime orderPlacedTimestamp; + private LocalDateTime shippedTimestamp; + private LocalDateTime deliveredTimestamp; + private LocalDateTime finalStatusTimestamp; + + @Enumerated(EnumType.STRING) + private OrderStatus status; + + @Column(nullable = true) + private String failureReason; + + private int numberOfItems; + private BigDecimal courierRating; + + @ManyToOne + @JoinColumn(name = "customer_id", nullable = false) + private Customer customer; + + @ManyToOne + @JoinColumn(name = "courier_id", nullable = false) + private Courier courier; + + @ManyToOne + @JoinColumn(name = "region_id", nullable = false) + private Region region; + + @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) + private List salesLineItems; + + public enum OrderStatus { + PLACED, + SHIPPED, + DELIVERED_PENDING_PAYMENT, + COMPLETED, + FAILED + } +} diff --git a/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java b/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java deleted file mode 100644 index 0b62baa..0000000 --- a/src/main/java/com/Podzilla/analytics/models/OrderAnalytics.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.Podzilla.analytics.models; - - -import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions -import java.time.Instant; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - - - - - - - -// NOTE: this is AI generated - - - - - - - - - - - - - - - -@Entity -@Table(name = "order_analytics") -@Data -@NoArgsConstructor -@AllArgsConstructor -public class OrderAnalytics { - - @Id // Primary Key - private String analyticId; // Using analyticId as the primary key for this derived/analytic Order record - - private Instant timestamp; - - private String customerId; // Assuming 'customerID' was a typo - - private double totalAmount; // Note: BigDecimal is generally preferred for currency - - private double rating; - - // Note: This looks like the original Order ID, distinct from analyticId - // If 'analyticId' is a *new* ID for the analytic record, and 'orderID' is the *original* order ID, - // you might make orderID a natural key if needed for lookups, but analyticId is the PK here. - @Column(name = "original_order_id") // Give it a clear column name if different from field name - private String orderId; // Assuming 'orderID' was a typo - - // Mapping the status string directly - private String status; // Stores "completed", "failed", "inprogress" as text - // Alternatively, you could use an Enum if the set of statuses is fixed and small - // @Enumerated(EnumType.STRING) - // private OrderStatus status; // Need to define OrderStatus enum - -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/models/Product.java b/src/main/java/com/Podzilla/analytics/models/Product.java new file mode 100644 index 0000000..f5eecc4 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/Product.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.models; + +import java.math.BigDecimal; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "products") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Product { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + private String category; + private BigDecimal cost; + private int lowStockThreshold; +} diff --git a/src/main/java/com/Podzilla/analytics/models/Region.java b/src/main/java/com/Podzilla/analytics/models/Region.java new file mode 100644 index 0000000..07d773a --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/Region.java @@ -0,0 +1,20 @@ +package com.Podzilla.analytics.models; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "regions") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Region { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String city; + private String state; + private String country; + private String postalCode; +} diff --git a/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java b/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java new file mode 100644 index 0000000..62190aa --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java @@ -0,0 +1,28 @@ +package com.Podzilla.analytics.models; + +import java.math.BigDecimal; +import jakarta.persistence.*; +import lombok.*; + +@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/models/WarehouseAnalytic.java b/src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java deleted file mode 100644 index 48414d6..0000000 --- a/src/main/java/com/Podzilla/analytics/models/WarehouseAnalytic.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.Podzilla.analytics.models; - -import jakarta.persistence.*; // Use javax.persistence.* for older JPA versions -import java.time.Instant; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.AllArgsConstructor; - - - - - -// NOTE: this is AI generated - - - - - - - - - -@Entity -@Table(name = "warehouse_analytics") // Table name in DB -@Data -@NoArgsConstructor -@AllArgsConstructor -public class WarehouseAnalytic { - - @Id // Primary Key - private String analyticId; - - private String productId; // Assuming 'productid' was a typo - - private Instant timestamp; - - private int currentQuantity; // Assuming 'current Quantity' was a typo - - private int soldQuantity; // Assuming 'sold quantity' was a typo - - private double profit; // Note: BigDecimal is generally preferred for currency/profit - - private boolean isLow; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java deleted file mode 100644 index 3d41a28..0000000 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierAnalyticRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.Podzilla.analytics.repositories; - -// Place in com.podzilla.erp.analytics.repository - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.Podzilla.analytics.models.CourierAnalytic; - -@Repository // Optional annotation for interfaces, but good practice -public interface CourierAnalyticRepository extends JpaRepository { - // Spring Data JPA automatically provides: - // save(CourierAnalytic entity) - // findById(String id) - // findAll() - // deleteById(String id) - // etc. - - // Add custom query methods here if needed later, e.g., - // List findByCourierIdAndDispatchTimestampBetween(String courierId, Instant start, Instant end); -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java new file mode 100644 index 0000000..41921a6 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.Courier; + +public interface CourierRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java deleted file mode 100644 index c2b2335..0000000 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerAnalyticRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.Podzilla.analytics.repositories; - -// Place in com.podzilla.erp.analytics.repository - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.Podzilla.analytics.models.CustomerAnalytic; - -@Repository -public interface CustomerAnalyticRepository extends JpaRepository { - // Automatic CRUD methods provided - - // Example custom query: - // List findByCustomerIdAndTimestampBetween(String customerId, Instant start, Instant end); -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java new file mode 100644 index 0000000..8ff5924 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.Customer; + +public interface CustomerRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java new file mode 100644 index 0000000..5079e25 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.InventorySnapshot; + +public interface InventorySnapshotRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java deleted file mode 100644 index d0b2c34..0000000 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderAnalyticRepository.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.Podzilla.analytics.repositories; - -// Place in com.podzilla.erp.analytics.repository - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.Podzilla.analytics.models.OrderAnalytics; - -@Repository -public interface OrderAnalyticRepository extends JpaRepository { - // Automatic CRUD methods provided - - // Example custom queries: - // List findByCustomerIdAndTimestampBetween(String customerId, Instant start, Instant end); - // List findByStatus(String status); - // Optional findByOrderId(String orderId); // If originalOrderId is unique and you need lookup by it -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java new file mode 100644 index 0000000..09d556b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.Order; + +public interface OrderRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java new file mode 100644 index 0000000..cde17d0 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.Product; + +public interface ProductRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java new file mode 100644 index 0000000..7e686f7 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.Region; + +public interface RegionRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java new file mode 100644 index 0000000..0a0f331 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java @@ -0,0 +1,8 @@ +package com.Podzilla.analytics.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.Podzilla.analytics.models.SalesLineItem; + +public interface SalesLineItemRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java b/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java deleted file mode 100644 index e1e9803..0000000 --- a/src/main/java/com/Podzilla/analytics/repositories/WarehouseAnalyticRepository.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.Podzilla.analytics.repositories; - - - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import com.Podzilla.analytics.models.WarehouseAnalytic; - -@Repository -public interface WarehouseAnalyticRepository extends JpaRepository { - // Automatic CRUD methods provided - - // Example custom queries: - // List findByProductIdAndTimestampBetween(String productId, Instant start, Instant end); - // List findByIsLowTrue(); // Finds entities where isLow is true -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java new file mode 100644 index 0000000..9ff610f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class CourierAnalyticsService { + +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java new file mode 100644 index 0000000..36c68f4 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class CustomerAnalyticsService { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java new file mode 100644 index 0000000..70cb564 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class FulfillmentAnalyticsService { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java new file mode 100644 index 0000000..ba1e12d --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java @@ -0,0 +1,10 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class InventoryAnalyticsService { +} diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java new file mode 100644 index 0000000..94c0c91 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class OrderAnalyticsService { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java new file mode 100644 index 0000000..c54499c --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class ProductAnalyticsService { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java new file mode 100644 index 0000000..8d2d603 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -0,0 +1,10 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class ProfitAnalyticsService { +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java new file mode 100644 index 0000000..9454695 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -0,0 +1,12 @@ +package com.Podzilla.analytics.services; + +import org.springframework.stereotype.Service; + + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +public class RevenueReportService { + +} \ No newline at end of file From d6917cf191c7ec26814ee61c2ffe35d9ad908870 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 07:23:44 +0300 Subject: [PATCH 03/28] Courier analytics endpoints (#7) * chore: create base structure for models, services, repositories, and controllers * chore: fix docker and docker compose files * feat: added courier analytics endpoints * refactor: add parameter names to @RequestParam * refactor: restructure code with projections, updated service logic --- docker-compose.yml | 86 +++---- dockerfile | 18 +- pom.xml | 5 + .../controllers/CourierReportController.java | 46 ++++ .../analytics/api/{DTOs => dtos}/.gitkeep | 0 .../api/dtos/CourierAverageRatingDTO.java | 14 ++ .../api/dtos/CourierDeliveryCountDTO.java | 13 ++ .../api/dtos/CourierPerformanceReportDTO.java | 17 ++ .../api/dtos/CourierSuccessRateDTO.java | 14 ++ .../CourierPerformanceProjection.java | 15 ++ .../analytics/config/DatabaseSeeder.java | 217 ++++++++++++++++++ .../repositories/CourierRepository.java | 27 ++- .../services/CourierAnalyticsService.java | 68 ++++++ .../analytics/util/MetricCalculator.java | 51 ++++ src/main/resources/application.properties | 18 +- 15 files changed, 540 insertions(+), 69 deletions(-) rename src/main/java/com/Podzilla/analytics/api/{DTOs => dtos}/.gitkeep (100%) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java create mode 100644 src/main/java/com/Podzilla/analytics/util/MetricCalculator.java diff --git a/docker-compose.yml b/docker-compose.yml index 135a31f..d335ee2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,64 +1,46 @@ services: + analytics-app: + build: . + container_name: analytics-app + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db_dev + SPRING_DATASOURCE_USERNAME: analytics_user + SPRING_DATASOURCE_PASSWORD: password + depends_on: + - db - # Database Service (PostgreSQL) db: - image: postgres:15-alpine # Use a lightweight PostgreSQL image + image: postgres container_name: analytics-db ports: - - "5432:5432" # Map host port 5432 to container port 5432 (optional, useful for direct access) + - "5432:5432" environment: - POSTGRES_DB: analytics_db_dev # Database name - POSTGRES_USER: analytics_user # Database user - POSTGRES_PASSWORD: password # Database password + POSTGRES_DB: analytics_db_dev + POSTGRES_USER: analytics_user + POSTGRES_PASSWORD: password volumes: - - db_data:/var/lib/postgresql/data # Persist data to a named volume - healthcheck: # Optional but recommended health check - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - interval: 10s - timeout: 5s - retries: 5 + - db_data:/var/lib/postgresql/data - # Message Queue Service (RabbitMQ with Management UI) - rabbitmq: - image: rabbitmq:3.12-management-alpine # Use RabbitMQ with the management plugin - container_name: analytics-rabbitmq - ports: - - "5672:5672" # Default AMQP port - - "15672:15672" # Management UI port (access at http://localhost:15672) - environment: - RABBITMQ_DEFAULT_USER: analytics_mq_user # RabbitMQ user - RABBITMQ_DEFAULT_PASS: analytics_mq_password # RabbitMQ password - healthcheck: # Basic health check for RabbitMQ - test: ["CMD", "rabbitmq-diagnostics", "check_system_status"] - interval: 10s - timeout: 5s - retries: 5 - # Your Spring Boot Application Service - analytics-app: - build: . # Build the Docker image using the Dockerfile in the current directory - container_name: analytics-app - ports: - - "8080:8080" # Map host port 8080 to the app's internal port 8080 (or your custom port) - environment: # Pass environment variables to your Spring Boot app - # Database Configuration (Matches application.yml property names often via Spring Boot convention) - SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db # 'db' is the service name in docker-compose - SPRING_DATASOURCE_USERNAME: analytics_user - SPRING_DATASOURCE_PASSWORD: analytics_password - SPRING_JPA_HIBERNATE_DDL_AUTO: update # Set DDL auto behavior (be cautious in prod!) - # RabbitMQ Configuration - SPRING_RABBITMQ_HOST: rabbitmq # 'rabbitmq' is the service name in docker-compose - SPRING_RABBITMQ_PORT: 5672 - SPRING_RABBITMQ_USERNAME: analytics_mq_user - SPRING_RABBITMQ_PASSWORD: analytics_mq_password - # Add any other necessary Spring Boot properties via environment variables - depends_on: # Ensure DB and RabbitMQ are running before starting the app - db: - condition: service_healthy # Wait for DB health check to pass - rabbitmq: - condition: service_healthy # Wait for RabbitMQ health check to pass + # rabbitmq: + # image: rabbitmq:3.12-management-alpine # Use RabbitMQ with the management plugin + # container_name: analytics-rabbitmq + # ports: + # - "5672:5672" # Default AMQP port + # - "15672:15672" # Management UI port (access at http://localhost:15672) + # environment: + # RABBITMQ_DEFAULT_USER: analytics_mq_user # RabbitMQ user + # RABBITMQ_DEFAULT_PASS: analytics_mq_password # RabbitMQ password + # healthcheck: # Basic health check for RabbitMQ + # test: ["CMD", "rabbitmq-diagnostics", "check_system_status"] + # interval: 10s + # timeout: 5s + # retries: 5 + -# Define named volumes for data persistence volumes: - db_data: # Data volume for the database \ No newline at end of file + db_data: + \ No newline at end of file diff --git a/dockerfile b/dockerfile index 15a5981..e4e3051 100644 --- a/dockerfile +++ b/dockerfile @@ -1,15 +1,9 @@ -# Stage 1: Build the application (This stage has JDK and Maven) -FROM maven:3.8.7-openjdk-21 AS builder # Use a Maven image with JDK 21 -WORKDIR /app -COPY pom.xml . # Copy the build file -COPY src ./src # <--- This is where you copy the source code in the BUILD stage -RUN mvn clean package -DskipTests # Build the project (skipping tests to speed up build) +FROM openjdk:25-ea-4-jdk-oraclelinux9 -# Stage 2: Run the application (This stage has just a JRE) -FROM eclipse-temurin:21-jre-alpine # Use a smaller JRE image for running WORKDIR /app -# Copy only the JAR artifact from the 'builder' stage to the current stage -COPY --from=builder /app/target/analytics-monolith-0.0.1-SNAPSHOT.jar /app/app.jar -ENTRYPOINT ["java", "-jar", "app.jar"] -EXPOSE 8080 \ No newline at end of file +COPY ./target/*.jar analytics_app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "analytics_app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index d01794a..d2a88ad 100644 --- a/pom.xml +++ b/pom.xml @@ -74,6 +74,11 @@ spring-rabbit-test test + + jakarta.validation + jakarta.validation-api + 3.0.2 + diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index d8d9869..ae12a31 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -1,7 +1,16 @@ package com.Podzilla.analytics.api.controllers; +import java.time.LocalDate; +import java.util.List; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import com.Podzilla.analytics.api.dtos.CourierAverageRatingDTO; +import com.Podzilla.analytics.api.dtos.CourierDeliveryCountDTO; +import com.Podzilla.analytics.api.dtos.CourierPerformanceReportDTO; +import com.Podzilla.analytics.api.dtos.CourierSuccessRateDTO; import com.Podzilla.analytics.services.CourierAnalyticsService; import lombok.RequiredArgsConstructor; @@ -11,4 +20,41 @@ @RequestMapping("/couriers") public class CourierReportController { private final CourierAnalyticsService courierAnalyticsService; + + @GetMapping("/delivery-counts") + public ResponseEntity> getCourierDeliveryCounts( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + + List counts = courierAnalyticsService.getCourierDeliveryCounts(startDate, endDate); + return ResponseEntity.ok(counts); + } + + @GetMapping("/success-rate") + public ResponseEntity> getCourierSuccessRate( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + + List rates = courierAnalyticsService.getCourierSuccessRate(startDate, endDate); + return ResponseEntity.ok(rates); + } + + @GetMapping("/average-rating") + public ResponseEntity> getCourierAverageRating( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + + List ratings = courierAnalyticsService.getCourierAverageRating(startDate, endDate); + return ResponseEntity.ok(ratings); + } + + @GetMapping("/performance-report") + public ResponseEntity> getCourierPerformanceReport( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + + List report = courierAnalyticsService.getCourierPerformanceReport(startDate, endDate); + return ResponseEntity.ok(report); + } + } \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/DTOs/.gitkeep b/src/main/java/com/Podzilla/analytics/api/dtos/.gitkeep similarity index 100% rename from src/main/java/com/Podzilla/analytics/api/DTOs/.gitkeep rename to src/main/java/com/Podzilla/analytics/api/dtos/.gitkeep diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java new file mode 100644 index 0000000..ac3efdb --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierAverageRatingDTO { + private Long courierId; + private String courierName; + private BigDecimal averageRating; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java new file mode 100644 index 0000000..b6c56c1 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java @@ -0,0 +1,13 @@ +package com.Podzilla.analytics.api.dtos; + +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierDeliveryCountDTO { + private Long courierId; + private String courierName; + private Long deliveryCount; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java new file mode 100644 index 0000000..8af9250 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierPerformanceReportDTO { + private Long courierId; + private String courierName; + private Long deliveryCount; + private BigDecimal successRate; + private BigDecimal averageRating; + +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java new file mode 100644 index 0000000..40be4e3 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java @@ -0,0 +1,14 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierSuccessRateDTO { + private Long courierId; + private String courierName; + private BigDecimal successRate; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java new file mode 100644 index 0000000..6ef3ec6 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java @@ -0,0 +1,15 @@ +package com.Podzilla.analytics.api.projections; + +import java.math.BigDecimal; + +public interface CourierPerformanceProjection { + Long getCourierId(); + + String getCourierName(); + + Long getDeliveryCount(); + + Long getCompletedCount(); + + BigDecimal getAverageRating(); +} diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java new file mode 100644 index 0000000..81a563d --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -0,0 +1,217 @@ +package com.Podzilla.analytics.config; + +import com.Podzilla.analytics.models.*; +import com.Podzilla.analytics.repositories.*; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +@Component +@RequiredArgsConstructor +public class DatabaseSeeder implements CommandLineRunner { + + private final CourierRepository courierRepository; + private final CustomerRepository customerRepository; + private final ProductRepository productRepository; + private final RegionRepository regionRepository; + private final OrderRepository orderRepository; + // private final SalesLineItemRepository salesLineItemRepository; + private final InventorySnapshotRepository inventorySnapshotRepository; + + private final Random random = new Random(); + + @Override + @Transactional + public void run(String... args) throws Exception { + System.out.println("Checking if database needs seeding..."); + + if (courierRepository.count() > 0) { + System.out.println("Database already seeded. Skipping."); + return; + } + + System.out.println("Seeding database..."); + + // --- Seed Independent Entities First --- + + // Regions + Region region1 = regionRepository + .save(Region.builder().city("Metropolis").state("NY").country("USA").postalCode("10001").build()); + Region region2 = regionRepository + .save(Region.builder().city("Gotham").state("NJ").country("USA").postalCode("07001").build()); + Region region3 = regionRepository + .save(Region.builder().city("Star City").state("CA").country("USA").postalCode("90210").build()); + List regions = Arrays.asList(region1, region2, region3); + System.out.println("Seeded Regions: " + regions.size()); + + // Products + Product prod1 = productRepository.save(Product.builder().name("Podzilla Pro").category("Electronics") + .cost(new BigDecimal("199.99")).lowStockThreshold(10).build()); + Product prod2 = productRepository.save(Product.builder().name("Podzilla Mini").category("Electronics") + .cost(new BigDecimal("99.99")).lowStockThreshold(20).build()); + Product prod3 = productRepository.save(Product.builder().name("Charging Case").category("Accessories") + .cost(new BigDecimal("49.50")).lowStockThreshold(50).build()); + Product prod4 = productRepository.save(Product.builder().name("Podzilla Cover").category("Accessories") + .cost(new BigDecimal("19.95")).lowStockThreshold(30).build()); + List products = Arrays.asList(prod1, prod2, prod3, prod4); + System.out.println("Seeded Products: " + products.size()); + + // Couriers + Courier courier1 = courierRepository + .save(Courier.builder().name("Speedy Delivery Inc.").status(Courier.CourierStatus.ACTIVE).build()); + Courier courier2 = courierRepository + .save(Courier.builder().name("Reliable Couriers Co.").status(Courier.CourierStatus.ACTIVE).build()); + Courier courier3 = courierRepository + .save(Courier.builder().name("Overnight Express").status(Courier.CourierStatus.INACTIVE).build()); + List couriers = Arrays.asList(courier1, courier2, courier3); + System.out.println("Seeded Couriers: " + couriers.size()); + + // Customers + Customer cust1 = customerRepository.save(Customer.builder().name("Alice Smith").build()); + Customer cust2 = customerRepository.save(Customer.builder().name("Bob Johnson").build()); + Customer cust3 = customerRepository.save(Customer.builder().name("Charlie Brown").build()); + List customers = Arrays.asList(cust1, cust2, cust3); + System.out.println("Seeded Customers: " + customers.size()); + + // --- Seed Dependent Entities --- + + // Orders and SalesLineItems + System.out.println("Seeding Orders and SalesLineItems..."); + LocalDate today = LocalDate.now(); + + // Order 1 (Completed) + LocalDateTime placed1 = today.minusDays(10).atTime(9, 30); + Order order1 = Order.builder() + .customer(cust1) + .courier(courier1) + .region(region1) + .status(Order.OrderStatus.COMPLETED) + .orderPlacedTimestamp(placed1) + .shippedTimestamp(placed1.plusHours(4)) + .deliveredTimestamp(placed1.plusDays(2).plusHours(1)) + .finalStatusTimestamp(placed1.plusDays(2).plusHours(1)) // Same as delivered for COMPLETED + .numberOfItems(3) // Will be calculated from items + .totalAmount(BigDecimal.ZERO) // Will be calculated from items + .courierRating(new BigDecimal("4.5")) + .build(); + + SalesLineItem item1_1 = SalesLineItem.builder().order(order1).product(prod1).quantity(1) + .pricePerUnit(prod1.getCost()).build(); + SalesLineItem item1_2 = SalesLineItem.builder().order(order1).product(prod3).quantity(2) + .pricePerUnit(prod3.getCost()).build(); + order1.setSalesLineItems(Arrays.asList(item1_1, item1_2)); // Associate items before saving order due to cascade + order1.setNumberOfItems(item1_1.getQuantity() + item1_2.getQuantity()); + order1.setTotalAmount(item1_1.getPricePerUnit().multiply(BigDecimal.valueOf(item1_1.getQuantity())) + .add(item1_2.getPricePerUnit().multiply(BigDecimal.valueOf(item1_2.getQuantity())))); + orderRepository.save(order1); // CascadeType.ALL on Order.salesLineItems saves the items too + + // Order 2 (Shipped) + LocalDateTime placed2 = today.minusDays(3).atTime(14, 0); + Order order2 = Order.builder() + .customer(cust2) + .courier(courier2) + .region(region2) + .status(Order.OrderStatus.SHIPPED) + .orderPlacedTimestamp(placed2) + .shippedTimestamp(placed2.plusDays(1).plusHours(1)) + .finalStatusTimestamp(placed2.plusDays(1).plusHours(1)) // Same as shipped for SHIPPED + .courierRating(null) // Not rated yet + .failureReason(null) + .build(); + SalesLineItem item2_1 = SalesLineItem.builder().order(order2).product(prod2).quantity(1) + .pricePerUnit(prod2.getCost()).build(); + order2.setSalesLineItems(List.of(item2_1)); + order2.setNumberOfItems(item2_1.getQuantity()); + order2.setTotalAmount(item2_1.getPricePerUnit().multiply(BigDecimal.valueOf(item2_1.getQuantity()))); + orderRepository.save(order2); + + // Order 3 (Failed) + LocalDateTime placed3 = today.minusDays(5).atTime(11, 15); + Order order3 = Order.builder() + .customer(cust1) + .courier(courier1) + .region(region3) + .status(Order.OrderStatus.FAILED) + .orderPlacedTimestamp(placed3) + .shippedTimestamp(placed3.plusHours(6)) + .deliveredTimestamp(null) + .finalStatusTimestamp(placed3.plusDays(3)) // When it was marked failed + .failureReason("Delivery address incorrect") + .courierRating(new BigDecimal("2.0")) + .build(); + SalesLineItem item3_1 = SalesLineItem.builder().order(order3).product(prod4).quantity(1) + .pricePerUnit(prod4.getCost()).build(); + order3.setSalesLineItems(List.of(item3_1)); + order3.setNumberOfItems(item3_1.getQuantity()); + order3.setTotalAmount(item3_1.getPricePerUnit().multiply(BigDecimal.valueOf(item3_1.getQuantity()))); + orderRepository.save(order3); + + // Order 4 (Completed - Recent) + LocalDateTime placed4 = today.minusDays(1).atTime(16, 45); + Order order4 = Order.builder() + .customer(cust3) + .courier(courier2) + .region(region1) + .status(Order.OrderStatus.COMPLETED) + .orderPlacedTimestamp(placed4) + .shippedTimestamp(placed4.plusHours(3)) + .deliveredTimestamp(placed4.plusHours(20)) + .finalStatusTimestamp(placed4.plusHours(20)) + .numberOfItems(2) // Will be calculated + .totalAmount(BigDecimal.ZERO) // Will be calculated + .courierRating(new BigDecimal("5.0")) + .build(); + + SalesLineItem item4_1 = SalesLineItem.builder().order(order4).product(prod1).quantity(1) + .pricePerUnit(prod1.getCost()).build(); + SalesLineItem item4_2 = SalesLineItem.builder().order(order4).product(prod4).quantity(1) + .pricePerUnit(prod4.getCost()).build(); + order4.setSalesLineItems(Arrays.asList(item4_1, item4_2)); + order4.setNumberOfItems(item4_1.getQuantity() + item4_2.getQuantity()); + order4.setTotalAmount(item4_1.getPricePerUnit().multiply(BigDecimal.valueOf(item4_1.getQuantity())) + .add(item4_2.getPricePerUnit().multiply(BigDecimal.valueOf(item4_2.getQuantity())))); + orderRepository.save(order4); + + System.out.println("Seeded Orders: " + orderRepository.count()); // Should be 4 + + // Inventory Snapshots + System.out.println("Seeding Inventory Snapshots..."); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod1).quantity(random.nextInt(50) + prod1.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(5)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod2).quantity(random.nextInt(100) + prod2.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(5)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod3).quantity(random.nextInt(150) + prod3.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(5)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod4).quantity(random.nextInt(80) + prod4.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(5)).build()); + + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod1).quantity(random.nextInt(40) + prod1.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(1)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod2).quantity(random.nextInt(90) + prod2.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(1)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod3).quantity(random.nextInt(140) + prod3.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(1)).build()); + inventorySnapshotRepository.save( + InventorySnapshot.builder().product(prod4).quantity(random.nextInt(70) + prod4.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays(1)).build()); + + System.out.println("Seeded Inventory Snapshots: " + inventorySnapshotRepository.count()); + + System.out.println("Database seeding finished."); + } +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index 41921a6..421237c 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -1,8 +1,33 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; import com.Podzilla.analytics.models.Courier; public interface CourierRepository extends JpaRepository { -} \ No newline at end of file + + @Query(value = """ + 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) AS completedCount, + AVG(CASE WHEN o.status = 'COMPLETED' THEN o.courier_rating 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 + AND o.status IN ('COMPLETED', 'FAILED') + GROUP BY c.id, c.name + ORDER BY completedCount DESC + """, nativeQuery = true) + List findCourierPerformanceBetweenDates( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); + +} diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 9ff610f..ce7ecc7 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -1,11 +1,79 @@ package com.Podzilla.analytics.services; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; + import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.CourierAverageRatingDTO; +import com.Podzilla.analytics.api.dtos.CourierDeliveryCountDTO; +import com.Podzilla.analytics.api.dtos.CourierPerformanceReportDTO; +import com.Podzilla.analytics.api.dtos.CourierSuccessRateDTO; +import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.util.MetricCalculator; + import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class CourierAnalyticsService { + private final CourierRepository courierRepository; + + private List getCourierPerformanceData(LocalDate startDate, LocalDate endDate) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + return courierRepository.findCourierPerformanceBetweenDates(startDateTime, endDateTime); + } + + public List getCourierDeliveryCounts(LocalDate startDate, LocalDate endDate) { + List performanceData = getCourierPerformanceData(startDate, endDate); + return performanceData.stream() + .map(data -> CourierDeliveryCountDTO.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .deliveryCount(data.getDeliveryCount()) + .build()) + .toList(); + } + + public List getCourierSuccessRate(LocalDate startDate, LocalDate endDate) { + List performanceData = getCourierPerformanceData(startDate, endDate); + return performanceData.stream() + .map(data -> CourierSuccessRateDTO.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .successRate( + MetricCalculator.calculatePercentage(data.getCompletedCount(), data.getDeliveryCount())) + .build()) + .toList(); + } + + public List getCourierAverageRating(LocalDate startDate, LocalDate endDate) { + List performanceData = getCourierPerformanceData(startDate, endDate); + return performanceData.stream() + .map(data -> CourierAverageRatingDTO.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .averageRating(data.getAverageRating()) + .build()) + .toList(); + } + public List getCourierPerformanceReport(LocalDate startDate, LocalDate endDate) { + List performanceData = getCourierPerformanceData(startDate, endDate); + return performanceData.stream() + .map(data -> CourierPerformanceReportDTO.builder() + .courierId(data.getCourierId()) + .courierName(data.getCourierName()) + .deliveryCount(data.getDeliveryCount()) + .successRate( + MetricCalculator.calculatePercentage(data.getCompletedCount(), data.getDeliveryCount())) + .averageRating(data.getAverageRating()) + .build()) + .toList(); + } } \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java new file mode 100644 index 0000000..63a7a06 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java @@ -0,0 +1,51 @@ +package com.Podzilla.analytics.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public class MetricCalculator { + private static final int DEFAULT_SCALE = 2; + private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); + + private MetricCalculator() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Calculates a success rate or percentage. + * + * @param numerator The count of successful outcomes (e.g., completed + * deliveries). + * @param denominator The total count of attempts (e.g., completed + failed + * deliveries). + * @param scale The number of decimal places for the result. + * @param roundingMode The rounding mode to apply. + * @return The calculated percentage as a BigDecimal, or BigDecimal.ZERO if the + * denominator is zero. + */ + public static BigDecimal calculatePercentage(long numerator, long denominator, int scale, + RoundingMode roundingMode) { + if (denominator == 0) { + return BigDecimal.ZERO; + } + if (numerator < 0 || denominator < 0) { + return BigDecimal.ZERO; + } + return BigDecimal.valueOf(numerator) + .multiply(ONE_HUNDRED) + .divide(BigDecimal.valueOf(denominator), scale, roundingMode); + } + + /** + * Calculates a success rate or percentage using default scale and rounding. + * + * @param numerator The count of successful outcomes. + * @param denominator The total count of attempts. + * @return The calculated percentage (scale 2, HALF_UP rounding), or + * BigDecimal.ZERO if denominator is zero. + */ + public static BigDecimal calculatePercentage(long numerator, long denominator) { + return calculatePercentage(numerator, denominator, DEFAULT_SCALE, RoundingMode.HALF_UP); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b139bac..dbadc86 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,15 @@ spring.application.name=analytics -spring.datasource.url=jdbc:postgresql://localhost:5432/analytics_db_dev -spring.datasource.username=analytics_user -spring.datasource.password=password -spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file + +# Database Configuration +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/analytics_db_dev} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:analytics_user} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password} +spring.datasource.driver-class-name=org.postgresql.Driver + +# JPA & Hibernate Configuration +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.generate-ddl=true +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true \ No newline at end of file From 86450cd2bb328505ee44c2d69baca694d5767bff Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 07:37:10 +0300 Subject: [PATCH 04/28] Fix docker and docker compose files (#3) * chore: create base structure for models, services, repositories, and controllers * chore: fix docker and docker compose files * fix: correct endpoint paths --------- Co-authored-by: Abdulrahman Fahmy --- .../analytics/api/controllers/CustomerReportController.java | 2 +- .../analytics/api/controllers/FulfillmentReportController.java | 2 +- .../analytics/api/controllers/InventoryReportController.java | 2 +- .../analytics/api/controllers/ProfitReportController.java | 2 +- src/main/resources/application.properties | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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 d804a89..d3bd641 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("customers") +@RequestMapping("/customers") public class CustomerReportController { private final CustomerAnalyticsService customerAnalyticsService; } \ No newline at end of file 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 8f518ea..05ae56e 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("fulfillment") +@RequestMapping("/fulfillment") public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; } \ No newline at end of file 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 7aabf74..fec93b8 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("inventory") +@RequestMapping("/inventory") public class InventoryReportController { private final InventoryAnalyticsService inventoryAnalyticsService; } 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 15485be..1bb6d1c 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("profit") +@RequestMapping("/profit") public class ProfitReportController { private final ProfitAnalyticsService profitAnalyticsService; } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index dbadc86..010a3fa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,7 +8,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver # JPA & Hibernate Configuration spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update spring.jpa.generate-ddl=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true From 463118106bbabc63745a44922878350229eac902 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 20:45:26 +0300 Subject: [PATCH 05/28] chore: add workflow setup (#12) --- .github/workflows/ci-cd.yml | 14 ++++++++++++++ .github/workflows/linter.yml | 12 ++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .github/workflows/ci-cd.yml create mode 100644 .github/workflows/linter.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..ca05840 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,14 @@ +name: Use Template Java CI/CD + +on: + push: + branches: [ "main", "dev" ] + pull_request: + branches: [ "**" ] + +jobs: + call-ci: + uses: Podzilla/templates/.github/workflows/ci.yml@main + with: + branch: 'refs/heads/main' # <<< Passes the branch name dynamically + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..c9ed1da --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,12 @@ +name: Use Template Linter + +on: + pull_request: + branches: [ "**" ] + +jobs: + call-linter: + uses: Podzilla/templates/.github/workflows/super-linter.yml@main + with: + branch: 'dev' + secrets: inherit \ No newline at end of file From ed3f725f23f226e84133972bdc62363334af3cd9 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 20:46:22 +0300 Subject: [PATCH 06/28] chore: add swagger api docs (#14) --- pom.xml | 5 +++++ .../analytics/config/OpenApiConfig.java | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java diff --git a/pom.xml b/pom.xml index d2a88ad..ceaa6df 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,11 @@ jakarta.validation-api 3.0.2 + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + diff --git a/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java b/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java new file mode 100644 index 0000000..b34b3f7 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java @@ -0,0 +1,18 @@ +package com.Podzilla.analytics.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Podzilla Analytics API") + .version("1.0") + .description("API documentation for analytics services.")); + } +} From 49c7bc2ff84e995db1e03cedd7bbb5658c934b28 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 20:48:07 +0300 Subject: [PATCH 07/28] chore: add global exception handler (#13) --- .../analytics/api/dtos/ErrorResponse.java | 24 +++++++ .../config/GlobalExceptionHandler.java | 62 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java new file mode 100644 index 0000000..9dbe983 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java @@ -0,0 +1,24 @@ +package com.Podzilla.analytics.api.dtos; // Or a common errors package + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorResponse { + private LocalDateTime timestamp; + private int status; + private String error; + private String message; + private Map fieldErrors; + private String path; +} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..9bbb66e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package com.Podzilla.analytics.config; + +import com.Podzilla.analytics.api.dtos.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; +import java.util.Map; +import java.util.stream.Collectors; + +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(BindException.class) + public ResponseEntity handleBindException(BindException ex, WebRequest request) { + + Map fieldErrors = ex.getBindingResult() + .getFieldErrors() + .stream() + .collect(Collectors.toMap( + FieldError::getField, + fieldError -> fieldError.getDefaultMessage() != null ? fieldError.getDefaultMessage() + : "Invalid value")); + + log.warn("Validation failed for request {}: {}", request.getDescription(false), fieldErrors); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.BAD_REQUEST.value()) + .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) + .message("Validation failed. Check field errors for details.") + .fieldErrors(fieldErrors) + .path(request.getDescription(false).replace("uri=", "")) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex, WebRequest request) { + + log.error("Unexpected error occurred processing request {}: {}", request.getDescription(false), ex.getMessage(), + ex); + + ErrorResponse errorResponse = ErrorResponse.builder() + .timestamp(LocalDateTime.now()) + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) // "Internal Server Error" + .message("An unexpected internal error occurred. Please try again later.") // Keep message generic + .path(request.getDescription(false).replace("uri=", "")) + .build(); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file From 3f91d64660ab7e5f515d492530ef5bc4fd2bac58 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 3 May 2025 20:53:05 +0300 Subject: [PATCH 08/28] feat: add DateRangeRequest DTO with cross-field validation (#11) * chore: create base structure for models, services, repositories, and controllers * chore: fix docker and docker compose files * feat: added courier analytics endpoints * refactor: add parameter names to @RequestParam * refactor: restructure code with projections, updated service logic * feat: add DateRangeRequest DTO with cross-field validation * chore: add swagger api docs * chore: add swagger docs for courier endpoints --- build_and_run.sh | 3 + .../controllers/CourierReportController.java | 63 ++++++++++--------- .../api/dtos/CourierAverageRatingDTO.java | 14 ----- .../dtos/CourierAverageRatingResponse.java | 21 +++++++ .../api/dtos/CourierDeliveryCountDTO.java | 13 ---- .../dtos/CourierDeliveryCountResponse.java | 20 ++++++ .../api/dtos/CourierPerformanceReportDTO.java | 17 ----- .../CourierPerformanceReportResponse.java | 27 ++++++++ .../api/dtos/CourierSuccessRateDTO.java | 14 ----- .../api/dtos/CourierSuccessRateResponse.java | 21 +++++++ .../analytics/api/dtos/DateRangeRequest.java | 27 ++++++++ .../repositories/CourierRepository.java | 2 +- .../services/CourierAnalyticsService.java | 33 +++++----- .../annotations/ValidDateRange.java | 20 ++++++ .../validators/DateRangeValidator.java | 18 ++++++ src/main/resources/application.properties | 2 +- 16 files changed, 213 insertions(+), 102 deletions(-) create mode 100755 build_and_run.sh delete mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java delete mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java delete mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java delete mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java create mode 100644 src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java diff --git a/build_and_run.sh b/build_and_run.sh new file mode 100755 index 0000000..02799e5 --- /dev/null +++ b/build_and_run.sh @@ -0,0 +1,3 @@ +mvn clean package -DskipTests +docker compose down +docker compose up -d --build \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index ae12a31..4eec72f 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -1,60 +1,67 @@ package com.Podzilla.analytics.api.controllers; -import java.time.LocalDate; import java.util.List; -import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import com.Podzilla.analytics.api.dtos.CourierAverageRatingDTO; -import com.Podzilla.analytics.api.dtos.CourierDeliveryCountDTO; -import com.Podzilla.analytics.api.dtos.CourierPerformanceReportDTO; -import com.Podzilla.analytics.api.dtos.CourierSuccessRateDTO; +import com.Podzilla.analytics.api.dtos.CourierAverageRatingResponse; +import com.Podzilla.analytics.api.dtos.CourierDeliveryCountResponse; +import com.Podzilla.analytics.api.dtos.CourierPerformanceReportResponse; +import com.Podzilla.analytics.api.dtos.CourierSuccessRateResponse; +import com.Podzilla.analytics.api.dtos.DateRangeRequest; import com.Podzilla.analytics.services.CourierAnalyticsService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +@Tag(name = "Courier Reports", description = "Endpoints for courier analytics and performance metrics") @RestController @RequestMapping("/couriers") +@RequiredArgsConstructor public class CourierReportController { + private final CourierAnalyticsService courierAnalyticsService; + @Operation(summary = "Get delivery counts", description = "Returns the total number of deliveries (both successful and failed) completed by each courier within the specified date range") + @ApiResponse(responseCode = "200", description = "Successfully retrieved delivery counts") @GetMapping("/delivery-counts") - public ResponseEntity> getCourierDeliveryCounts( - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { + public ResponseEntity> getCourierDeliveryCounts( + @Valid @ModelAttribute DateRangeRequest dateRange) { - List counts = courierAnalyticsService.getCourierDeliveryCounts(startDate, endDate); + List counts = courierAnalyticsService + .getCourierDeliveryCounts(dateRange.getStartDate(), dateRange.getEndDate()); return ResponseEntity.ok(counts); } + @Operation(summary = "Get courier success rate", description = "Returns the success rate of each courier within the given date range") @GetMapping("/success-rate") - public ResponseEntity> getCourierSuccessRate( - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - List rates = courierAnalyticsService.getCourierSuccessRate(startDate, endDate); + public ResponseEntity> getCourierSuccessRate( + @Valid @ModelAttribute DateRangeRequest dateRange) { + List rates = courierAnalyticsService + .getCourierSuccessRate(dateRange.getStartDate(), dateRange.getEndDate()); return ResponseEntity.ok(rates); } + @Operation(summary = "Get average courier ratings", description = "Fetches the average rating received by each courier in the specified date range") @GetMapping("/average-rating") - public ResponseEntity> getCourierAverageRating( - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - List ratings = courierAnalyticsService.getCourierAverageRating(startDate, endDate); + public ResponseEntity> getCourierAverageRating( + @Valid @ModelAttribute DateRangeRequest dateRange) { + List ratings = courierAnalyticsService + .getCourierAverageRating(dateRange.getStartDate(), dateRange.getEndDate()); return ResponseEntity.ok(ratings); } + @Operation(summary = "Get courier performance report", description = "Returns a detailed performance report of each courier including deliveries, ratings, and success rate") @GetMapping("/performance-report") - public ResponseEntity> getCourierPerformanceReport( - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, - @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) { - - List report = courierAnalyticsService.getCourierPerformanceReport(startDate, endDate); + public ResponseEntity> getCourierPerformanceReport( + @Valid @ModelAttribute DateRangeRequest dateRange) { + List report = courierAnalyticsService + .getCourierPerformanceReport(dateRange.getStartDate(), dateRange.getEndDate()); return ResponseEntity.ok(report); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java deleted file mode 100644 index ac3efdb..0000000 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.Podzilla.analytics.api.dtos; - -import java.math.BigDecimal; -import lombok.*; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CourierAverageRatingDTO { - private Long courierId; - private String courierName; - private BigDecimal averageRating; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java new file mode 100644 index 0000000..4e557a9 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java @@ -0,0 +1,21 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierAverageRatingResponse { + + @Schema(description = "ID of the courier", example = "101") + private Long 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; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java deleted file mode 100644 index b6c56c1..0000000 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.Podzilla.analytics.api.dtos; - -import lombok.*; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CourierDeliveryCountDTO { - private Long courierId; - private String courierName; - private Long deliveryCount; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java new file mode 100644 index 0000000..e439d5a --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java @@ -0,0 +1,20 @@ +package com.Podzilla.analytics.api.dtos; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierDeliveryCountResponse { + + @Schema(description = "ID of the courier", example = "101") + private Long courierId; + + @Schema(description = "Full name of the courier", example = "Jane Smith") + private String courierName; + + @Schema(description = "Total number of deliveries (successful + failed)", example = "134") + private Long deliveryCount; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java deleted file mode 100644 index 8af9250..0000000 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.Podzilla.analytics.api.dtos; - -import java.math.BigDecimal; -import lombok.*; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CourierPerformanceReportDTO { - private Long courierId; - private String courierName; - private Long deliveryCount; - private BigDecimal successRate; - private BigDecimal averageRating; - -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java new file mode 100644 index 0000000..eecef38 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java @@ -0,0 +1,27 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierPerformanceReportResponse { + + @Schema(description = "ID of the courier", example = "105") + private Long courierId; + + @Schema(description = "Full name of the courier", example = "Ali Hassan") + private String courierName; + + @Schema(description = "Total number of deliveries", example = "87") + private Long deliveryCount; + + @Schema(description = "Success rate as a decimal value (e.g., 0.92 for 92%)", example = "0.92") + private BigDecimal successRate; + + @Schema(description = "Average customer rating", example = "4.8") + private BigDecimal averageRating; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java deleted file mode 100644 index 40be4e3..0000000 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.Podzilla.analytics.api.dtos; - -import java.math.BigDecimal; -import lombok.*; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class CourierSuccessRateDTO { - private Long courierId; - private String courierName; - private BigDecimal successRate; -} \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java new file mode 100644 index 0000000..25cd894 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java @@ -0,0 +1,21 @@ +package com.Podzilla.analytics.api.dtos; + +import java.math.BigDecimal; +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CourierSuccessRateResponse { + + @Schema(description = "ID of the courier", example = "103") + private Long courierId; + + @Schema(description = "Full name of the courier", example = "Fatima Ahmed") + private String courierName; + + @Schema(description = "Delivery success rate", example = "0.87") + private BigDecimal successRate; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java new file mode 100644 index 0000000..10a1cfd --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java @@ -0,0 +1,27 @@ +package com.Podzilla.analytics.api.dtos; + +import java.time.LocalDate; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import io.swagger.v3.oas.annotations.media.Schema; + +@ValidDateRange +@Getter +@AllArgsConstructor +public class DateRangeRequest { + + @NotNull(message = "startDate is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @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) + @Schema(description = "End date of the range (inclusive)", example = "2024-01-31", required = true) + private LocalDate endDate; +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index 421237c..d1d1f44 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -24,7 +24,7 @@ public interface CourierRepository extends JpaRepository { AND o.final_status_timestamp BETWEEN :startDate AND :endDate AND o.status IN ('COMPLETED', 'FAILED') GROUP BY c.id, c.name - ORDER BY completedCount DESC + ORDER BY courierId """, nativeQuery = true) List findCourierPerformanceBetweenDates( @Param("startDate") LocalDateTime startDate, diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index ce7ecc7..a328f9d 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -7,10 +7,10 @@ import org.springframework.stereotype.Service; -import com.Podzilla.analytics.api.dtos.CourierAverageRatingDTO; -import com.Podzilla.analytics.api.dtos.CourierDeliveryCountDTO; -import com.Podzilla.analytics.api.dtos.CourierPerformanceReportDTO; -import com.Podzilla.analytics.api.dtos.CourierSuccessRateDTO; +import com.Podzilla.analytics.api.dtos.CourierAverageRatingResponse; +import com.Podzilla.analytics.api.dtos.CourierDeliveryCountResponse; +import com.Podzilla.analytics.api.dtos.CourierPerformanceReportResponse; +import com.Podzilla.analytics.api.dtos.CourierSuccessRateResponse; import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.util.MetricCalculator; @@ -29,10 +29,10 @@ private List getCourierPerformanceData(LocalDate s return courierRepository.findCourierPerformanceBetweenDates(startDateTime, endDateTime); } - public List getCourierDeliveryCounts(LocalDate startDate, LocalDate endDate) { + public List getCourierDeliveryCounts(LocalDate startDate, LocalDate endDate) { List performanceData = getCourierPerformanceData(startDate, endDate); return performanceData.stream() - .map(data -> CourierDeliveryCountDTO.builder() + .map(data -> CourierDeliveryCountResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) .deliveryCount(data.getDeliveryCount()) @@ -40,22 +40,24 @@ public List getCourierDeliveryCounts(LocalDate startDat .toList(); } - public List getCourierSuccessRate(LocalDate startDate, LocalDate endDate) { + public List getCourierSuccessRate(LocalDate startDate, LocalDate endDate) { List performanceData = getCourierPerformanceData(startDate, endDate); return performanceData.stream() - .map(data -> CourierSuccessRateDTO.builder() + .map(data -> CourierSuccessRateResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) .successRate( - MetricCalculator.calculatePercentage(data.getCompletedCount(), data.getDeliveryCount())) + MetricCalculator.calculatePercentage( + data.getCompletedCount(), + data.getDeliveryCount())) .build()) .toList(); } - public List getCourierAverageRating(LocalDate startDate, LocalDate endDate) { + public List getCourierAverageRating(LocalDate startDate, LocalDate endDate) { List performanceData = getCourierPerformanceData(startDate, endDate); return performanceData.stream() - .map(data -> CourierAverageRatingDTO.builder() + .map(data -> CourierAverageRatingResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) .averageRating(data.getAverageRating()) @@ -63,15 +65,18 @@ public List getCourierAverageRating(LocalDate startDate .toList(); } - public List getCourierPerformanceReport(LocalDate startDate, LocalDate endDate) { + public List getCourierPerformanceReport(LocalDate startDate, + LocalDate endDate) { List performanceData = getCourierPerformanceData(startDate, endDate); return performanceData.stream() - .map(data -> CourierPerformanceReportDTO.builder() + .map(data -> CourierPerformanceReportResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) .deliveryCount(data.getDeliveryCount()) .successRate( - MetricCalculator.calculatePercentage(data.getCompletedCount(), data.getDeliveryCount())) + MetricCalculator.calculatePercentage( + data.getCompletedCount(), + data.getDeliveryCount())) .averageRating(data.getAverageRating()) .build()) .toList(); diff --git a/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java new file mode 100644 index 0000000..956aa75 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java @@ -0,0 +1,20 @@ +package com.Podzilla.analytics.validation.annotations; + +import com.Podzilla.analytics.validation.validators.DateRangeValidator; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = DateRangeValidator.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidDateRange { + String message() default "endDate must be after startDate"; + + 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 new file mode 100644 index 0000000..b464de4 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java @@ -0,0 +1,18 @@ +package com.Podzilla.analytics.validation.validators; + +import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class DateRangeValidator implements ConstraintValidator { + + @Override + public boolean isValid(DateRangeRequest request, ConstraintValidatorContext context) { + if (request.getStartDate() == null || request.getEndDate() == null) { + return true; + } + return request.getEndDate().isAfter(request.getStartDate()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 010a3fa..dbadc86 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,7 +8,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver # JPA & Hibernate Configuration spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.generate-ddl=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true From b0823f69b9023049c1daf5aaa99df594552b4059 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Thu, 8 May 2025 03:46:18 +0300 Subject: [PATCH 09/28] chore: fix code style issues (#17) * chore: fix code style issues * fix: uncommented and restored SQL query in CourierRepository * fix: remove final from run method in datasbase seeder --- config/checkstyle/sun_checks.xml | 194 +++++++++ pom.xml | 20 +- .../analytics/AnalyticsApplication.java | 6 +- .../controllers/CourierReportController.java | 61 +-- .../controllers/CustomerReportController.java | 6 +- .../FulfillmentReportController.java | 6 +- .../InventoryReportController.java | 4 +- .../controllers/OrderReportController.java | 6 +- .../controllers/ProductReportController.java | 7 +- .../controllers/ProfitReportController.java | 6 +- .../controllers/RevenueReportController.java | 6 +- .../dtos/CourierAverageRatingResponse.java | 5 +- .../dtos/CourierDeliveryCountResponse.java | 10 +- .../CourierPerformanceReportResponse.java | 12 +- .../api/dtos/CourierSuccessRateResponse.java | 5 +- .../analytics/api/dtos/DateRangeRequest.java | 7 +- .../analytics/api/dtos/ErrorResponse.java | 2 +- .../analytics/config/DatabaseSeeder.java | 410 ++++++++++++------ .../config/GlobalExceptionHandler.java | 34 +- .../analytics/config/OpenApiConfig.java | 3 +- .../analytics/eventhandler/DTOs/ADTO.java | 5 + .../analytics/eventhandler/DTOs/aDTO.java | 5 - .../eventhandler/EventHandlerDispatcher.java | 31 +- ...java => EventHandlerDispatcherConfig.java} | 12 +- .../eventhandler/handlers/AHandler.java | 11 + .../eventhandler/handlers/aHandler.java | 13 - .../analytics/messaging/RabbitListener.java | 4 +- .../Podzilla/analytics/models/Courier.java | 13 +- .../Podzilla/analytics/models/Customer.java | 11 +- .../analytics/models/InventorySnapshot.java | 13 +- .../com/Podzilla/analytics/models/Order.java | 18 +- .../Podzilla/analytics/models/Product.java | 11 +- .../com/Podzilla/analytics/models/Region.java | 11 +- .../analytics/models/SalesLineItem.java | 13 +- .../repositories/CourierRepository.java | 28 +- .../repositories/CustomerRepository.java | 2 +- .../InventorySnapshotRepository.java | 5 +- .../repositories/OrderRepository.java | 2 +- .../repositories/ProductRepository.java | 2 +- .../repositories/RegionRepository.java | 2 +- .../repositories/SalesLineItemRepository.java | 5 +- .../services/CourierAnalyticsService.java | 39 +- .../services/CustomerAnalyticsService.java | 2 +- .../services/FulfillmentAnalyticsService.java | 2 +- .../services/OrderAnalyticsService.java | 2 +- .../services/ProductAnalyticsService.java | 2 +- .../services/ProfitAnalyticsService.java | 2 +- .../services/RevenueReportService.java | 3 +- .../analytics/util/MetricCalculator.java | 22 +- .../annotations/ValidDateRange.java | 8 +- .../validators/DateRangeValidator.java | 7 +- src/main/resources/application.properties | 2 +- 52 files changed, 798 insertions(+), 320 deletions(-) create mode 100644 config/checkstyle/sun_checks.xml create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/DTOs/ADTO.java delete mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java rename src/main/java/com/Podzilla/analytics/eventhandler/{eventHandlerDispatcherConfig.java => EventHandlerDispatcherConfig.java} (54%) create mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/handlers/AHandler.java delete mode 100644 src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java diff --git a/config/checkstyle/sun_checks.xml b/config/checkstyle/sun_checks.xml new file mode 100644 index 0000000..c2fab2a --- /dev/null +++ b/config/checkstyle/sun_checks.xml @@ -0,0 +1,194 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index ceaa6df..4e0ef99 100644 --- a/pom.xml +++ b/pom.xml @@ -112,7 +112,25 @@ - + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + config/checkstyle/sun_checks.xml + true + true + + + + validate + validate + + check + + + + + diff --git a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java index eb12c98..30ad798 100644 --- a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java +++ b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java @@ -6,8 +6,8 @@ @SpringBootApplication public class AnalyticsApplication { - public static void main(String[] args) { - SpringApplication.run(AnalyticsApplication.class, args); - } + public static void main(final String[] args) { + SpringApplication.run(AnalyticsApplication.class, args); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index 4eec72f..c437ac0 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -3,7 +3,10 @@ import java.util.List; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +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.CourierAverageRatingResponse; import com.Podzilla.analytics.api.dtos.CourierDeliveryCountResponse; @@ -12,56 +15,64 @@ import com.Podzilla.analytics.api.dtos.DateRangeRequest; import com.Podzilla.analytics.services.CourierAnalyticsService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; - import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; -@Tag(name = "Courier Reports", description = "Endpoints for courier analytics and performance metrics") +@Tag(name = "Courier Reports", description = "Endpoints for courier" + + " analytics and performance metrics") @RestController @RequestMapping("/couriers") @RequiredArgsConstructor -public class CourierReportController { +public final class CourierReportController { private final CourierAnalyticsService courierAnalyticsService; - @Operation(summary = "Get delivery counts", description = "Returns the total number of deliveries (both successful and failed) completed by each courier within the specified date range") - @ApiResponse(responseCode = "200", description = "Successfully retrieved delivery counts") + @Operation(summary = "Get delivery counts", description = "Returns the" + + " total number of deliveries completed by each courier") + @ApiResponse(responseCode = "200", description = "Successfully retrieved" + + " delivery counts") @GetMapping("/delivery-counts") - public ResponseEntity> getCourierDeliveryCounts( - @Valid @ModelAttribute DateRangeRequest dateRange) { - + public ResponseEntity> getDeliveryCounts( + @Valid @ModelAttribute final DateRangeRequest dateRange) { List counts = courierAnalyticsService - .getCourierDeliveryCounts(dateRange.getStartDate(), dateRange.getEndDate()); + .getCourierDeliveryCounts(dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(counts); } - @Operation(summary = "Get courier success rate", description = "Returns the success rate of each courier within the given date range") + @Operation(summary = "Get courier success rate", description = "Returns " + + " the success rate of each courier") @GetMapping("/success-rate") - public ResponseEntity> getCourierSuccessRate( - @Valid @ModelAttribute DateRangeRequest dateRange) { + public ResponseEntity> getSuccessRate( + @Valid @ModelAttribute final DateRangeRequest dateRange) { List rates = courierAnalyticsService - .getCourierSuccessRate(dateRange.getStartDate(), dateRange.getEndDate()); + .getCourierSuccessRate(dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(rates); } - @Operation(summary = "Get average courier ratings", description = "Fetches the average rating received by each courier in the specified date range") + @Operation(summary = "Get average courier ratings", description = "Fetches " + + " the average rating received by each courier") @GetMapping("/average-rating") - public ResponseEntity> getCourierAverageRating( - @Valid @ModelAttribute DateRangeRequest dateRange) { + public ResponseEntity> getAverageRating( + @Valid @ModelAttribute final DateRangeRequest dateRange) { List ratings = courierAnalyticsService - .getCourierAverageRating(dateRange.getStartDate(), dateRange.getEndDate()); + .getCourierAverageRating(dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(ratings); } - @Operation(summary = "Get courier performance report", description = "Returns a detailed performance report of each courier including deliveries, ratings, and success rate") + @Operation(summary = "Get courier performance report", description = "" + + "Returns a detailed performance report of each courier") @GetMapping("/performance-report") - public ResponseEntity> getCourierPerformanceReport( - @Valid @ModelAttribute DateRangeRequest dateRange) { + public ResponseEntity> getReport( + @Valid @ModelAttribute final DateRangeRequest dateRange) { List report = courierAnalyticsService - .getCourierPerformanceReport(dateRange.getStartDate(), dateRange.getEndDate()); + .getCourierPerformanceReport(dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(report); } } 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 d3bd641..72415dd 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.CustomerAnalyticsService; import lombok.RequiredArgsConstructor; @@ -11,4 +11,4 @@ @RequestMapping("/customers") public class CustomerReportController { private final CustomerAnalyticsService customerAnalyticsService; -} \ No newline at end of file +} 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 05ae56e..8b2e7ae 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -1,6 +1,8 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; @@ -11,4 +13,4 @@ @RequestMapping("/fulfillment") public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; -} \ No newline at end of file +} 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 fec93b8..762e2cf 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.InventoryAnalyticsService; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java index 8110b47..32054a8 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.OrderAnalyticsService; import lombok.RequiredArgsConstructor; @@ -11,4 +11,4 @@ @RequestMapping("/orders") public class OrderReportController { private final OrderAnalyticsService orderAnalyticsService; -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java index 59af7d6..8522964 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.ProductAnalyticsService; import lombok.RequiredArgsConstructor; @@ -11,5 +11,4 @@ @RequestMapping("/products") public class ProductReportController { private final ProductAnalyticsService productAnalyticsService; - -} \ No newline at end of file +} 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 1bb6d1c..dd759c4 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.ProfitAnalyticsService; import lombok.RequiredArgsConstructor; @@ -11,4 +11,4 @@ @RequestMapping("/profit") public class ProfitReportController { private final ProfitAnalyticsService profitAnalyticsService; -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java index 8c68406..6e20d9d 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.api.controllers; -import org.springframework.web.bind.annotation.*; - +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import com.Podzilla.analytics.services.RevenueReportService; import lombok.RequiredArgsConstructor; @@ -11,4 +11,4 @@ @RequestMapping("/revenue") public class RevenueReportController { private final RevenueReportService revenueReportService; -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java index 4e557a9..ba3a30e 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java @@ -1,8 +1,11 @@ package com.Podzilla.analytics.api.dtos; import java.math.BigDecimal; -import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java index e439d5a..a9b83d3 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java @@ -1,6 +1,9 @@ package com.Podzilla.analytics.api.dtos; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import io.swagger.v3.oas.annotations.media.Schema; @Data @@ -15,6 +18,9 @@ public class CourierDeliveryCountResponse { @Schema(description = "Full name of the courier", example = "Jane Smith") private String courierName; - @Schema(description = "Total number of deliveries (successful + failed)", example = "134") + @Schema( + description = "Total number of deliveries (successful + failed)", + example = "134" + ) private Long deliveryCount; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java index eecef38..ae22554 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java @@ -1,7 +1,12 @@ package com.Podzilla.analytics.api.dtos; import java.math.BigDecimal; -import lombok.*; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + import io.swagger.v3.oas.annotations.media.Schema; @Data @@ -19,7 +24,10 @@ public class CourierPerformanceReportResponse { @Schema(description = "Total number of deliveries", example = "87") private Long deliveryCount; - @Schema(description = "Success rate as a decimal value (e.g., 0.92 for 92%)", example = "0.92") + @Schema( + description = "Success rate as a decimal value (e.g., 0.92 for 92%)", + example = "0.92" + ) private BigDecimal successRate; @Schema(description = "Average customer rating", example = "4.8") diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java index 25cd894..4e47b1e 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java @@ -1,8 +1,11 @@ package com.Podzilla.analytics.api.dtos; import java.math.BigDecimal; -import lombok.*; import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder 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 10a1cfd..084b895 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java @@ -1,6 +1,7 @@ package com.Podzilla.analytics.api.dtos; import java.time.LocalDate; + import org.springframework.format.annotation.DateTimeFormat; import com.Podzilla.analytics.validation.annotations.ValidDateRange; @@ -17,11 +18,13 @@ public class DateRangeRequest { @NotNull(message = "startDate is required") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) - @Schema(description = "Start date of the range (inclusive)", example = "2024-01-01", required = true) + @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) - @Schema(description = "End date of the range (inclusive)", example = "2024-01-31", required = true) + @Schema(description = "End date of the range (inclusive)", + example = "2024-01-31", required = true) private LocalDate endDate; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java index 9dbe983..2bb79e0 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java @@ -21,4 +21,4 @@ public class ErrorResponse { private String message; private Map fieldErrors; private String path; -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 81a563d..632b1f0 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -1,7 +1,18 @@ package com.Podzilla.analytics.config; -import com.Podzilla.analytics.models.*; -import com.Podzilla.analytics.repositories.*; +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.Region; +import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.InventorySnapshotRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.RegionRepository; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @@ -14,6 +25,7 @@ import java.util.List; import java.util.Random; + @Component @RequiredArgsConstructor public class DatabaseSeeder implements CommandLineRunner { @@ -23,14 +35,67 @@ public class DatabaseSeeder implements CommandLineRunner { private final ProductRepository productRepository; private final RegionRepository regionRepository; private final OrderRepository orderRepository; - // private final SalesLineItemRepository salesLineItemRepository; private final InventorySnapshotRepository inventorySnapshotRepository; private final Random random = new Random(); + private static final int LOW_STOCK_PROD1 = 10; + private static final int LOW_STOCK_PROD2 = 20; + private static final int LOW_STOCK_PROD3 = 50; + private static final int LOW_STOCK_PROD4 = 30; + + private static final BigDecimal PRICE_PROD1 = new BigDecimal("199.99"); + private static final BigDecimal PRICE_PROD2 = new BigDecimal("99.99"); + private static final BigDecimal PRICE_PROD3 = new BigDecimal("49.50"); + private static final BigDecimal PRICE_PROD4 = new BigDecimal("19.95"); + + private static final BigDecimal RATING_GOOD = new BigDecimal("4.5"); + private static final BigDecimal RATING_POOR = new BigDecimal("2.0"); + private static final BigDecimal RATING_EXCELLENT = new BigDecimal("5.0"); + + private static final int ORDER_ITEM_COUNT_3 = 3; + private static final int ORDER_ITEM_COUNT_2 = 2; + + private static final int ORDER_1_DAYS_PRIOR = 10; + private static final int ORDER_1_HOUR = 9; + private static final int ORDER_1_MINUTE = 30; + private static final int ORDER_1_SHIP_HOURS = 4; + private static final int ORDER_1_DELIVER_DAYS = 2; + private static final int ORDER_1_DELIVER_HOURS = 1; + + private static final int ORDER_2_DAYS_PRIOR = 3; + private static final int ORDER_2_HOUR = 14; + private static final int ORDER_2_MINUTE = 0; + private static final int ORDER_2_SHIP_DAYS = 1; + private static final int ORDER_2_SHIP_HOURS = 1; + + private static final int ORDER_3_DAYS_PRIOR = 5; + private static final int ORDER_3_HOUR = 11; + private static final int ORDER_3_MINUTE = 15; + private static final int ORDER_3_SHIP_HOURS = 6; + private static final int ORDER_3_FINAL_DAYS = 3; + + private static final int ORDER_4_DAYS_PRIOR = 1; + private static final int ORDER_4_HOUR = 16; + private static final int ORDER_4_MINUTE = 45; + private static final int ORDER_4_SHIP_HOURS = 3; + private static final int ORDER_4_DELIVER_HOURS = 20; + + private static final int INVENTORY_SNAPSHOT_DAYS_PRIOR_1 = 5; + private static final int INVENTORY_SNAPSHOT_DAYS_PRIOR_2 = 1; + private static final int INVENTORY_RANGE_PROD1 = 50; + private static final int INVENTORY_RANGE_PROD2 = 100; + private static final int INVENTORY_RANGE_PROD3 = 150; + private static final int INVENTORY_RANGE_PROD4 = 80; + private static final int INVENTORY_QUANTITY_PROD1 = 40; + private static final int INVENTORY_QUANTITY_PROD2 = 90; + private static final int INVENTORY_QUANTITY_PROD3 = 140; + private static final int INVENTORY_QUANTITY_PROD4 = 70; + + private static final int INDEX_THREE = 3; @Override @Transactional - public void run(String... args) throws Exception { + public void run(final String... args) { System.out.println("Checking if database needs seeding..."); if (courierRepository.count() > 0) { @@ -40,178 +105,245 @@ public void run(String... args) throws Exception { System.out.println("Seeding database..."); - // --- Seed Independent Entities First --- - - // Regions - Region region1 = regionRepository - .save(Region.builder().city("Metropolis").state("NY").country("USA").postalCode("10001").build()); - Region region2 = regionRepository - .save(Region.builder().city("Gotham").state("NJ").country("USA").postalCode("07001").build()); - Region region3 = regionRepository - .save(Region.builder().city("Star City").state("CA").country("USA").postalCode("90210").build()); - List regions = Arrays.asList(region1, region2, region3); + List regions = seedRegions(); System.out.println("Seeded Regions: " + regions.size()); - // Products - Product prod1 = productRepository.save(Product.builder().name("Podzilla Pro").category("Electronics") - .cost(new BigDecimal("199.99")).lowStockThreshold(10).build()); - Product prod2 = productRepository.save(Product.builder().name("Podzilla Mini").category("Electronics") - .cost(new BigDecimal("99.99")).lowStockThreshold(20).build()); - Product prod3 = productRepository.save(Product.builder().name("Charging Case").category("Accessories") - .cost(new BigDecimal("49.50")).lowStockThreshold(50).build()); - Product prod4 = productRepository.save(Product.builder().name("Podzilla Cover").category("Accessories") - .cost(new BigDecimal("19.95")).lowStockThreshold(30).build()); - List products = Arrays.asList(prod1, prod2, prod3, prod4); + List products = seedProducts(); System.out.println("Seeded Products: " + products.size()); - // Couriers - Courier courier1 = courierRepository - .save(Courier.builder().name("Speedy Delivery Inc.").status(Courier.CourierStatus.ACTIVE).build()); - Courier courier2 = courierRepository - .save(Courier.builder().name("Reliable Couriers Co.").status(Courier.CourierStatus.ACTIVE).build()); - Courier courier3 = courierRepository - .save(Courier.builder().name("Overnight Express").status(Courier.CourierStatus.INACTIVE).build()); - List couriers = Arrays.asList(courier1, courier2, courier3); + List couriers = seedCouriers(); System.out.println("Seeded Couriers: " + couriers.size()); - // Customers - Customer cust1 = customerRepository.save(Customer.builder().name("Alice Smith").build()); - Customer cust2 = customerRepository.save(Customer.builder().name("Bob Johnson").build()); - Customer cust3 = customerRepository.save(Customer.builder().name("Charlie Brown").build()); - List customers = Arrays.asList(cust1, cust2, cust3); + List customers = seedCustomers(); System.out.println("Seeded Customers: " + customers.size()); - // --- Seed Dependent Entities --- - - // Orders and SalesLineItems System.out.println("Seeding Orders and SalesLineItems..."); + seedOrders(customers, couriers, regions, products); + System.out.println("Seeded Orders: " + orderRepository.count()); + + System.out.println("Seeding Inventory Snapshots..."); + seedInventorySnapshots(products); + System.out.println("Seeded Inventory Snapshots: " + + inventorySnapshotRepository.count()); + + System.out.println("Database seeding finished."); + } + + private List seedRegions() { + Region region1 = regionRepository.save( + Region.builder().city("Metropolis").state("NY") + .country("USA").postalCode("10001") + .build()); + Region region2 = regionRepository.save( + Region.builder().city("Gotham").state("NJ") + .country("USA").postalCode("07001") + .build()); + Region region3 = regionRepository.save( + Region.builder().city("Star City").state("CA") + .country("USA").postalCode("90210") + .build()); + return Arrays.asList(region1, region2, region3); + } + + private List seedProducts() { + Product prod1 = productRepository.save(Product.builder() + .name("Podzilla Pro").category("Electronics") + .cost(PRICE_PROD1) + .lowStockThreshold(LOW_STOCK_PROD1).build()); + Product prod2 = productRepository.save(Product.builder() + .name("Podzilla Mini").category("Electronics") + .cost(PRICE_PROD2) + .lowStockThreshold(LOW_STOCK_PROD2).build()); + Product prod3 = productRepository.save(Product.builder() + .name("Charging Case").category("Accessories") + .cost(PRICE_PROD3) + .lowStockThreshold(LOW_STOCK_PROD3).build()); + Product prod4 = productRepository.save(Product.builder() + .name("Podzilla Cover").category("Accessories") + .cost(PRICE_PROD4) + .lowStockThreshold(LOW_STOCK_PROD4).build()); + return Arrays.asList(prod1, prod2, prod3, prod4); + } + + private List seedCouriers() { + Courier courier1 = courierRepository.save( + Courier.builder().name("Speedy Delivery Inc.") + .status(Courier.CourierStatus.ACTIVE).build()); + Courier courier2 = courierRepository.save( + Courier.builder().name("Reliable Couriers Co.") + .status(Courier.CourierStatus.ACTIVE).build()); + Courier courier3 = courierRepository.save( + Courier.builder().name("Overnight Express") + .status(Courier.CourierStatus.INACTIVE).build()); + return Arrays.asList(courier1, courier2, courier3); + } + + private List seedCustomers() { + Customer cust1 = customerRepository.save( + Customer.builder().name("Alice Smith").build()); + Customer cust2 = customerRepository.save( + Customer.builder().name("Bob Johnson").build()); + Customer cust3 = customerRepository.save( + Customer.builder().name("Charlie Brown").build()); + return Arrays.asList(cust1, cust2, cust3); + } + + private void seedOrders( + final List customers, + final List couriers, + final List regions, + final List products) { LocalDate today = LocalDate.now(); - // Order 1 (Completed) - LocalDateTime placed1 = today.minusDays(10).atTime(9, 30); + // Order 1 + LocalDateTime placed1 = today.minusDays(ORDER_1_DAYS_PRIOR) + .atTime(ORDER_1_HOUR, ORDER_1_MINUTE); Order order1 = Order.builder() - .customer(cust1) - .courier(courier1) - .region(region1) + .customer(customers.get(0)).courier(couriers.get(0)) + .region(regions.get(0)) .status(Order.OrderStatus.COMPLETED) .orderPlacedTimestamp(placed1) - .shippedTimestamp(placed1.plusHours(4)) - .deliveredTimestamp(placed1.plusDays(2).plusHours(1)) - .finalStatusTimestamp(placed1.plusDays(2).plusHours(1)) // Same as delivered for COMPLETED - .numberOfItems(3) // Will be calculated from items - .totalAmount(BigDecimal.ZERO) // Will be calculated from items - .courierRating(new BigDecimal("4.5")) + .shippedTimestamp(placed1.plusHours(ORDER_1_SHIP_HOURS)) + .deliveredTimestamp(placed1.plusDays(ORDER_1_DELIVER_DAYS) + .plusHours(ORDER_1_DELIVER_HOURS)) + .finalStatusTimestamp(placed1.plusDays(ORDER_1_DELIVER_DAYS) + .plusHours(ORDER_1_DELIVER_HOURS)) + .numberOfItems(ORDER_ITEM_COUNT_3) + .totalAmount(BigDecimal.ZERO) + .courierRating(RATING_GOOD) .build(); + SalesLineItem itemFirstOrderFirst = SalesLineItem.builder() + .order(order1).product(products.get(0)).quantity(1) + .pricePerUnit(PRICE_PROD1).build(); + SalesLineItem itemFirstOrderSecond = SalesLineItem.builder() + .order(order1).product(products.get(2)).quantity(2) + .pricePerUnit(PRICE_PROD3).build(); + order1.setSalesLineItems(Arrays.asList(itemFirstOrderFirst, + itemFirstOrderSecond)); + order1.setNumberOfItems(itemFirstOrderFirst.getQuantity() + + itemFirstOrderSecond.getQuantity()); + order1.setTotalAmount( + itemFirstOrderFirst.getPricePerUnit().multiply( + BigDecimal.valueOf(itemFirstOrderFirst.getQuantity())) + .add(itemFirstOrderSecond.getPricePerUnit().multiply( + BigDecimal.valueOf( + itemFirstOrderSecond.getQuantity())))); + orderRepository.save(order1); - SalesLineItem item1_1 = SalesLineItem.builder().order(order1).product(prod1).quantity(1) - .pricePerUnit(prod1.getCost()).build(); - SalesLineItem item1_2 = SalesLineItem.builder().order(order1).product(prod3).quantity(2) - .pricePerUnit(prod3.getCost()).build(); - order1.setSalesLineItems(Arrays.asList(item1_1, item1_2)); // Associate items before saving order due to cascade - order1.setNumberOfItems(item1_1.getQuantity() + item1_2.getQuantity()); - order1.setTotalAmount(item1_1.getPricePerUnit().multiply(BigDecimal.valueOf(item1_1.getQuantity())) - .add(item1_2.getPricePerUnit().multiply(BigDecimal.valueOf(item1_2.getQuantity())))); - orderRepository.save(order1); // CascadeType.ALL on Order.salesLineItems saves the items too - - // Order 2 (Shipped) - LocalDateTime placed2 = today.minusDays(3).atTime(14, 0); + // Order 2 + LocalDateTime placed2 = today.minusDays(ORDER_2_DAYS_PRIOR) + .atTime(ORDER_2_HOUR, ORDER_2_MINUTE); Order order2 = Order.builder() - .customer(cust2) - .courier(courier2) - .region(region2) + .customer(customers.get(1)).courier(couriers.get(1)) + .region(regions.get(1)) .status(Order.OrderStatus.SHIPPED) .orderPlacedTimestamp(placed2) - .shippedTimestamp(placed2.plusDays(1).plusHours(1)) - .finalStatusTimestamp(placed2.plusDays(1).plusHours(1)) // Same as shipped for SHIPPED - .courierRating(null) // Not rated yet - .failureReason(null) + .shippedTimestamp(placed2.plusDays(ORDER_2_SHIP_DAYS) + .plusHours(ORDER_2_SHIP_HOURS)) + .finalStatusTimestamp(placed2.plusDays(ORDER_2_SHIP_DAYS) + .plusHours(ORDER_2_SHIP_HOURS)) + .courierRating(null).failureReason(null) .build(); - SalesLineItem item2_1 = SalesLineItem.builder().order(order2).product(prod2).quantity(1) - .pricePerUnit(prod2.getCost()).build(); - order2.setSalesLineItems(List.of(item2_1)); - order2.setNumberOfItems(item2_1.getQuantity()); - order2.setTotalAmount(item2_1.getPricePerUnit().multiply(BigDecimal.valueOf(item2_1.getQuantity()))); + SalesLineItem itemSecondOrderFirst = SalesLineItem.builder() + .order(order2).product(products.get(1)).quantity(1) + .pricePerUnit(PRICE_PROD2).build(); + order2.setSalesLineItems(List.of(itemSecondOrderFirst)); + order2.setNumberOfItems(itemSecondOrderFirst.getQuantity()); + order2.setTotalAmount( + itemSecondOrderFirst.getPricePerUnit().multiply( + BigDecimal.valueOf(itemSecondOrderFirst + .getQuantity()))); orderRepository.save(order2); - // Order 3 (Failed) - LocalDateTime placed3 = today.minusDays(5).atTime(11, 15); + // Order 3 + LocalDateTime placed3 = today.minusDays(ORDER_3_DAYS_PRIOR) + .atTime(ORDER_3_HOUR, ORDER_3_MINUTE); Order order3 = Order.builder() - .customer(cust1) - .courier(courier1) - .region(region3) + .customer(customers.get(0)).courier(couriers.get(0)) + .region(regions.get(2)) .status(Order.OrderStatus.FAILED) .orderPlacedTimestamp(placed3) - .shippedTimestamp(placed3.plusHours(6)) + .status(Order.OrderStatus.FAILED) + .orderPlacedTimestamp(placed3) + .shippedTimestamp(placed3.plusHours(ORDER_3_SHIP_HOURS)) .deliveredTimestamp(null) - .finalStatusTimestamp(placed3.plusDays(3)) // When it was marked failed + .finalStatusTimestamp(placed3.plusDays(ORDER_3_FINAL_DAYS)) .failureReason("Delivery address incorrect") - .courierRating(new BigDecimal("2.0")) + .courierRating(RATING_POOR) .build(); - SalesLineItem item3_1 = SalesLineItem.builder().order(order3).product(prod4).quantity(1) - .pricePerUnit(prod4.getCost()).build(); - order3.setSalesLineItems(List.of(item3_1)); - order3.setNumberOfItems(item3_1.getQuantity()); - order3.setTotalAmount(item3_1.getPricePerUnit().multiply(BigDecimal.valueOf(item3_1.getQuantity()))); + SalesLineItem itemThirdOrderFirst = SalesLineItem.builder() + .order(order3).product(products.get(INDEX_THREE)).quantity(1) + .pricePerUnit(PRICE_PROD4).build(); + order3.setSalesLineItems(List.of(itemThirdOrderFirst)); + order3.setNumberOfItems(itemThirdOrderFirst.getQuantity()); + order3.setTotalAmount( + itemThirdOrderFirst.getPricePerUnit().multiply( + BigDecimal.valueOf(itemThirdOrderFirst.getQuantity()))); orderRepository.save(order3); - // Order 4 (Completed - Recent) - LocalDateTime placed4 = today.minusDays(1).atTime(16, 45); + // Order 4 + LocalDateTime placed4 = today.minusDays(ORDER_4_DAYS_PRIOR) + .atTime(ORDER_4_HOUR, ORDER_4_MINUTE); Order order4 = Order.builder() - .customer(cust3) - .courier(courier2) - .region(region1) + .customer(customers.get(2)).courier(couriers.get(1)) + .region(regions.get(0)) .status(Order.OrderStatus.COMPLETED) .orderPlacedTimestamp(placed4) - .shippedTimestamp(placed4.plusHours(3)) - .deliveredTimestamp(placed4.plusHours(20)) - .finalStatusTimestamp(placed4.plusHours(20)) - .numberOfItems(2) // Will be calculated - .totalAmount(BigDecimal.ZERO) // Will be calculated - .courierRating(new BigDecimal("5.0")) + .shippedTimestamp(placed4.plusHours(ORDER_4_SHIP_HOURS)) + .deliveredTimestamp(placed4.plusHours(ORDER_4_DELIVER_HOURS)) + .finalStatusTimestamp(placed4.plusHours(ORDER_4_DELIVER_HOURS)) + .numberOfItems(ORDER_ITEM_COUNT_2) + .totalAmount(BigDecimal.ZERO) + .courierRating(RATING_EXCELLENT) .build(); - - SalesLineItem item4_1 = SalesLineItem.builder().order(order4).product(prod1).quantity(1) - .pricePerUnit(prod1.getCost()).build(); - SalesLineItem item4_2 = SalesLineItem.builder().order(order4).product(prod4).quantity(1) - .pricePerUnit(prod4.getCost()).build(); - order4.setSalesLineItems(Arrays.asList(item4_1, item4_2)); - order4.setNumberOfItems(item4_1.getQuantity() + item4_2.getQuantity()); - order4.setTotalAmount(item4_1.getPricePerUnit().multiply(BigDecimal.valueOf(item4_1.getQuantity())) - .add(item4_2.getPricePerUnit().multiply(BigDecimal.valueOf(item4_2.getQuantity())))); + SalesLineItem itemFourthOrderFirst = SalesLineItem.builder() + .order(order4).product(products.get(0)).quantity(1) + .pricePerUnit(PRICE_PROD1).build(); + SalesLineItem itemFourthOrderSecond = SalesLineItem.builder() + .order(order4).product(products.get(INDEX_THREE)).quantity(1) + .pricePerUnit(PRICE_PROD4).build(); + order4.setSalesLineItems(Arrays.asList(itemFourthOrderFirst, + itemFourthOrderSecond)); + order4.setNumberOfItems( + itemFourthOrderFirst.getQuantity() + itemFourthOrderSecond + .getQuantity()); + order4.setTotalAmount( + itemFourthOrderFirst.getPricePerUnit().multiply( + BigDecimal.valueOf(itemFourthOrderFirst.getQuantity())) + .add(itemFourthOrderSecond.getPricePerUnit().multiply( + BigDecimal.valueOf(itemFourthOrderSecond + .getQuantity())))); orderRepository.save(order4); + } - System.out.println("Seeded Orders: " + orderRepository.count()); // Should be 4 - - // Inventory Snapshots - System.out.println("Seeding Inventory Snapshots..."); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod1).quantity(random.nextInt(50) + prod1.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(5)).build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod2).quantity(random.nextInt(100) + prod2.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(5)).build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod3).quantity(random.nextInt(150) + prod3.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(5)).build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod4).quantity(random.nextInt(80) + prod4.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(5)).build()); + private void seedInventorySnapshots(final List products) { + seedInventorySnapshot(products.get(0), INVENTORY_RANGE_PROD1, + INVENTORY_QUANTITY_PROD1); + seedInventorySnapshot(products.get(1), INVENTORY_RANGE_PROD2, + INVENTORY_QUANTITY_PROD2); + seedInventorySnapshot(products.get(2), INVENTORY_RANGE_PROD3, + INVENTORY_QUANTITY_PROD3); + seedInventorySnapshot(products.get(INDEX_THREE), INVENTORY_RANGE_PROD4, + INVENTORY_QUANTITY_PROD4); + } + private void seedInventorySnapshot( + final Product product, final int range, final int quantity) { inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod1).quantity(random.nextInt(40) + prod1.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(1)).build()); + InventorySnapshot.builder() + .product(product) + .quantity(random.nextInt(range) + + product.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays( + INVENTORY_SNAPSHOT_DAYS_PRIOR_1)) + .build()); inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod2).quantity(random.nextInt(90) + prod2.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(1)).build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod3).quantity(random.nextInt(140) + prod3.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(1)).build()); - inventorySnapshotRepository.save( - InventorySnapshot.builder().product(prod4).quantity(random.nextInt(70) + prod4.getLowStockThreshold()) - .timestamp(LocalDateTime.now().minusDays(1)).build()); - - System.out.println("Seeded Inventory Snapshots: " + inventorySnapshotRepository.count()); - - System.out.println("Database seeding finished."); + InventorySnapshot.builder() + .product(product) + .quantity(random.nextInt(quantity) + + product.getLowStockThreshold()) + .timestamp(LocalDateTime.now().minusDays( + INVENTORY_SNAPSHOT_DAYS_PRIOR_2)) + .build()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java index 9bbb66e..5a15860 100644 --- a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java +++ b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java @@ -19,44 +19,52 @@ public class GlobalExceptionHandler { @ExceptionHandler(BindException.class) - public ResponseEntity handleBindException(BindException ex, WebRequest request) { + public ResponseEntity handleBindException( + final BindException ex, + final WebRequest request) { Map fieldErrors = ex.getBindingResult() .getFieldErrors() .stream() .collect(Collectors.toMap( FieldError::getField, - fieldError -> fieldError.getDefaultMessage() != null ? fieldError.getDefaultMessage() + fieldError -> fieldError.getDefaultMessage() != null + ? fieldError.getDefaultMessage() : "Invalid value")); - log.warn("Validation failed for request {}: {}", request.getDescription(false), fieldErrors); + log.warn("Validation failed for request {}: {}", + request.getDescription(false), fieldErrors); ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.BAD_REQUEST.value()) - .error(HttpStatus.BAD_REQUEST.getReasonPhrase()) - .message("Validation failed. Check field errors for details.") + .error("Validation failed") .fieldErrors(fieldErrors) - .path(request.getDescription(false).replace("uri=", "")) .build(); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } @ExceptionHandler(Exception.class) - public ResponseEntity handleGenericException(Exception ex, WebRequest request) { + public ResponseEntity handleGenericException( + final Exception ex, + final WebRequest request) { - log.error("Unexpected error occurred processing request {}: {}", request.getDescription(false), ex.getMessage(), + log.error("Unexpected error occurred processing request {}: {}", + request.getDescription(false), + ex.getMessage(), ex); ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) // "Internal Server Error" - .message("An unexpected internal error occurred. Please try again later.") // Keep message generic - .path(request.getDescription(false).replace("uri=", "")) + .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) + .message("An unexpected internal error occurred.") + .path(request.getDescription(false) + .replace("uri=", "")) .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + return new ResponseEntity<>(errorResponse, + HttpStatus.INTERNAL_SERVER_ERROR); } -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java b/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java index b34b3f7..0796b98 100644 --- a/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java +++ b/src/main/java/com/Podzilla/analytics/config/OpenApiConfig.java @@ -13,6 +13,7 @@ public OpenAPI customOpenAPI() { .info(new Info() .title("Podzilla Analytics API") .version("1.0") - .description("API documentation for analytics services.")); + .description( + "API documentation for analytics services.")); } } diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/ADTO.java b/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/ADTO.java new file mode 100644 index 0000000..3a24c6c --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/ADTO.java @@ -0,0 +1,5 @@ +package com.Podzilla.analytics.eventhandler.DTOs; + +public class ADTO { + private String a; +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java b/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java deleted file mode 100644 index 5d7694e..0000000 --- a/src/main/java/com/Podzilla/analytics/eventhandler/DTOs/aDTO.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.Podzilla.analytics.eventhandler.DTOs; -// TODO remove this example -public class aDTO { - String a; -} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java index f5077db..0dc8729 100644 --- a/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java +++ b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcher.java @@ -1,22 +1,37 @@ package com.Podzilla.analytics.eventhandler; -import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; -public class EventHandlerDispatcher { - // dto , event - HashMap, IEventHandler> handlers; +public final class EventHandlerDispatcher { + private final Map, IEventHandler> handlers; - public void registerHandler(Class dto, IEventHandler handler) { + public EventHandlerDispatcher() { + handlers = new ConcurrentHashMap<>(); + } + + public void registerHandler(final Class dto, + final IEventHandler handler) { + if (dto == null || handler == null) { + throw new IllegalArgumentException("DTO and Handler" + + " cannot be null"); + } handlers.put(dto, handler); } @SuppressWarnings("unchecked") - public void dispatch(T dto) { - IEventHandler handler = (IEventHandler) handlers.get(dto.getClass()); + public void dispatch(final T dto) { + if (dto == null) { + throw new IllegalArgumentException("Event DTO cannot be null"); + } + + IEventHandler handler = (IEventHandler) handlers + .get(dto.getClass()); if (handler != null) { handler.handle(dto); } else { - throw new RuntimeException("No handler found for: " + dto.getClass()); + throw new RuntimeException("No handler found for: " + + dto.getClass()); } } } diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcherConfig.java similarity index 54% rename from src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java rename to src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcherConfig.java index adb8f9b..4265b05 100644 --- a/src/main/java/com/Podzilla/analytics/eventhandler/eventHandlerDispatcherConfig.java +++ b/src/main/java/com/Podzilla/analytics/eventhandler/EventHandlerDispatcherConfig.java @@ -3,19 +3,17 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.Podzilla.analytics.eventhandler.DTOs.aDTO; -import com.Podzilla.analytics.eventhandler.handlers.aHandler; - @Configuration -public class eventHandlerDispatcherConfig { +public class EventHandlerDispatcherConfig { @Bean public EventHandlerDispatcher commandDispatcher() { EventHandlerDispatcher dispatcher = new EventHandlerDispatcher(); - //TODO should add all the events here - //Example: - //dispatcher.registerHandler(aDTO.class, new aHandler()); + // Register all event handlers here + // Example: + // dispatcher.registerHandler(aDTO.class, new aHandler()); + return dispatcher; } } diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/handlers/AHandler.java b/src/main/java/com/Podzilla/analytics/eventhandler/handlers/AHandler.java new file mode 100644 index 0000000..776e677 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/eventhandler/handlers/AHandler.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.eventhandler.handlers; + +import com.Podzilla.analytics.eventhandler.IEventHandler; +import com.Podzilla.analytics.eventhandler.DTOs.ADTO; + +public class AHandler implements IEventHandler { + + @Override + public void handle(final ADTO eventDto) { + } +} diff --git a/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java b/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java deleted file mode 100644 index be66e00..0000000 --- a/src/main/java/com/Podzilla/analytics/eventhandler/handlers/aHandler.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.Podzilla.analytics.eventhandler.handlers; - -import com.Podzilla.analytics.eventhandler.IEventHandler; -import com.Podzilla.analytics.eventhandler.DTOs.aDTO; - -public class aHandler implements IEventHandler{ - // TODO remove this example - @Override - public void handle(aDTO eventDto) { - - } - -} diff --git a/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java index 7113e03..afd38a2 100644 --- a/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java +++ b/src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java @@ -5,7 +5,7 @@ import com.Podzilla.analytics.eventhandler.EventHandlerDispatcher; public class RabbitListener { - + @Autowired - EventHandlerDispatcher dispatcher; + private EventHandlerDispatcher dispatcher; } diff --git a/src/main/java/com/Podzilla/analytics/models/Courier.java b/src/main/java/com/Podzilla/analytics/models/Courier.java index 81d84bc..0e50fd0 100644 --- a/src/main/java/com/Podzilla/analytics/models/Courier.java +++ b/src/main/java/com/Podzilla/analytics/models/Courier.java @@ -1,7 +1,16 @@ package com.Podzilla.analytics.models; -import jakarta.persistence.*; -import lombok.*; +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.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Entity @Table(name = "couriers") diff --git a/src/main/java/com/Podzilla/analytics/models/Customer.java b/src/main/java/com/Podzilla/analytics/models/Customer.java index c535e5e..f63cbc9 100644 --- a/src/main/java/com/Podzilla/analytics/models/Customer.java +++ b/src/main/java/com/Podzilla/analytics/models/Customer.java @@ -1,7 +1,14 @@ package com.Podzilla.analytics.models; -import jakarta.persistence.*; -import lombok.*; +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; @Entity @Table(name = "customers") diff --git a/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java b/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java index 1e4b416..f5fd12d 100644 --- a/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java +++ b/src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java @@ -1,8 +1,17 @@ package com.Podzilla.analytics.models; import java.time.LocalDateTime; -import jakarta.persistence.*; -import lombok.*; +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") diff --git a/src/main/java/com/Podzilla/analytics/models/Order.java b/src/main/java/com/Podzilla/analytics/models/Order.java index 6e644dd..f3ec9b4 100644 --- a/src/main/java/com/Podzilla/analytics/models/Order.java +++ b/src/main/java/com/Podzilla/analytics/models/Order.java @@ -4,8 +4,22 @@ import java.time.LocalDateTime; import java.util.List; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +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; @Entity @Table(name = "orders") diff --git a/src/main/java/com/Podzilla/analytics/models/Product.java b/src/main/java/com/Podzilla/analytics/models/Product.java index f5eecc4..30f73ae 100644 --- a/src/main/java/com/Podzilla/analytics/models/Product.java +++ b/src/main/java/com/Podzilla/analytics/models/Product.java @@ -2,8 +2,15 @@ import java.math.BigDecimal; -import jakarta.persistence.*; -import lombok.*; +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; @Entity @Table(name = "products") diff --git a/src/main/java/com/Podzilla/analytics/models/Region.java b/src/main/java/com/Podzilla/analytics/models/Region.java index 07d773a..01945d0 100644 --- a/src/main/java/com/Podzilla/analytics/models/Region.java +++ b/src/main/java/com/Podzilla/analytics/models/Region.java @@ -1,7 +1,14 @@ package com.Podzilla.analytics.models; -import jakarta.persistence.*; -import lombok.*; +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; @Entity @Table(name = "regions") diff --git a/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java b/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java index 62190aa..d9e1212 100644 --- a/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java +++ b/src/main/java/com/Podzilla/analytics/models/SalesLineItem.java @@ -1,8 +1,17 @@ package com.Podzilla.analytics.models; import java.math.BigDecimal; -import jakarta.persistence.*; -import lombok.*; +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") diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index d1d1f44..3da0777 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -12,20 +12,20 @@ public interface CourierRepository extends JpaRepository { - @Query(value = """ - 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) AS completedCount, - AVG(CASE WHEN o.status = 'COMPLETED' THEN o.courier_rating 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 - AND o.status IN ('COMPLETED', 'FAILED') - GROUP BY c.id, c.name - ORDER BY courierId - """, nativeQuery = true) + @Query(value = "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) " + + "AS completedCount, " + + "AVG(CASE WHEN o.status = 'COMPLETED' THEN o.courier_rating " + + "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 " + + "AND o.status IN ('COMPLETED', 'FAILED') " + + "GROUP BY c.id, c.name " + + "ORDER BY courierId", nativeQuery = true) 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 8ff5924..ee9f907 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -5,4 +5,4 @@ import com.Podzilla.analytics.models.Customer; public interface CustomerRepository extends JpaRepository { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java index 5079e25..d0098a4 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java @@ -4,5 +4,6 @@ import com.Podzilla.analytics.models.InventorySnapshot; -public interface InventorySnapshotRepository extends JpaRepository { -} \ No newline at end of file +public interface InventorySnapshotRepository + extends JpaRepository { +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index 09d556b..f0960d6 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -5,4 +5,4 @@ import com.Podzilla.analytics.models.Order; public interface OrderRepository extends JpaRepository { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java index cde17d0..9254be2 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -5,4 +5,4 @@ import com.Podzilla.analytics.models.Product; public interface ProductRepository extends JpaRepository { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java index 7e686f7..64a5c44 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/RegionRepository.java @@ -5,4 +5,4 @@ import com.Podzilla.analytics.models.Region; public interface RegionRepository extends JpaRepository { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java index 0a0f331..4793b6e 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java @@ -4,5 +4,6 @@ import com.Podzilla.analytics.models.SalesLineItem; -public interface SalesLineItemRepository extends JpaRepository { -} \ No newline at end of file +public interface SalesLineItemRepository + 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 a328f9d..12501d6 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -22,16 +22,21 @@ public class CourierAnalyticsService { private final CourierRepository courierRepository; - private List getCourierPerformanceData(LocalDate startDate, LocalDate endDate) { + private List getCourierPerformanceData( + final LocalDate startDate, + final LocalDate endDate) { LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); - return courierRepository.findCourierPerformanceBetweenDates(startDateTime, endDateTime); + return courierRepository.findCourierPerformanceBetweenDates( + startDateTime, + endDateTime); } - public List getCourierDeliveryCounts(LocalDate startDate, LocalDate endDate) { - List performanceData = getCourierPerformanceData(startDate, endDate); - return performanceData.stream() + public List getCourierDeliveryCounts( + final LocalDate startDate, + final LocalDate endDate) { + return getCourierPerformanceData(startDate, endDate).stream() .map(data -> CourierDeliveryCountResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) @@ -40,9 +45,10 @@ public List getCourierDeliveryCounts(LocalDate sta .toList(); } - public List getCourierSuccessRate(LocalDate startDate, LocalDate endDate) { - List performanceData = getCourierPerformanceData(startDate, endDate); - return performanceData.stream() + public List getCourierSuccessRate( + final LocalDate startDate, + final LocalDate endDate) { + return getCourierPerformanceData(startDate, endDate).stream() .map(data -> CourierSuccessRateResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) @@ -54,9 +60,10 @@ public List getCourierSuccessRate(LocalDate startDat .toList(); } - public List getCourierAverageRating(LocalDate startDate, LocalDate endDate) { - List performanceData = getCourierPerformanceData(startDate, endDate); - return performanceData.stream() + public List getCourierAverageRating( + final LocalDate startDate, + final LocalDate endDate) { + return getCourierPerformanceData(startDate, endDate).stream() .map(data -> CourierAverageRatingResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) @@ -65,10 +72,10 @@ public List getCourierAverageRating(LocalDate star .toList(); } - public List getCourierPerformanceReport(LocalDate startDate, - LocalDate endDate) { - List performanceData = getCourierPerformanceData(startDate, endDate); - return performanceData.stream() + public List getCourierPerformanceReport( + final LocalDate startDate, + final LocalDate endDate) { + return getCourierPerformanceData(startDate, endDate).stream() .map(data -> CourierPerformanceReportResponse.builder() .courierId(data.getCourierId()) .courierName(data.getCourierName()) @@ -81,4 +88,4 @@ public List getCourierPerformanceReport(LocalD .build()) .toList(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java index 36c68f4..10cc66a 100644 --- a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java @@ -8,4 +8,4 @@ @RequiredArgsConstructor @Service public class CustomerAnalyticsService { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java index 70cb564..196d38f 100644 --- a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java @@ -8,4 +8,4 @@ @RequiredArgsConstructor @Service public class FulfillmentAnalyticsService { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java index 94c0c91..afadc56 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -8,4 +8,4 @@ @RequiredArgsConstructor @Service public class OrderAnalyticsService { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java index c54499c..e985186 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -8,4 +8,4 @@ @RequiredArgsConstructor @Service public class ProductAnalyticsService { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java index 8d2d603..be2fa59 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -7,4 +7,4 @@ @RequiredArgsConstructor @Service public class ProfitAnalyticsService { -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java index 9454695..e8fa494 100644 --- a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -8,5 +8,4 @@ @RequiredArgsConstructor @Service public class RevenueReportService { - -} \ No newline at end of file +} diff --git a/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java index 63a7a06..1e91256 100644 --- a/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java +++ b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java @@ -3,12 +3,14 @@ import java.math.BigDecimal; import java.math.RoundingMode; -public class MetricCalculator { +public final class MetricCalculator { private static final int DEFAULT_SCALE = 2; + private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); private MetricCalculator() { - throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + throw new UnsupportedOperationException( + "This is a utility class and cannot be instantiated"); } /** @@ -20,11 +22,12 @@ private MetricCalculator() { * deliveries). * @param scale The number of decimal places for the result. * @param roundingMode The rounding mode to apply. - * @return The calculated percentage as a BigDecimal, or BigDecimal.ZERO if the - * denominator is zero. + * @return The calculated percentage as a BigDecimal, or BigDecimal.ZERO + * if the denominator is zero. */ - public static BigDecimal calculatePercentage(long numerator, long denominator, int scale, - RoundingMode roundingMode) { + public static BigDecimal calculatePercentage(final long numerator, + final long denominator, final int scale, + final RoundingMode roundingMode) { if (denominator == 0) { return BigDecimal.ZERO; } @@ -44,8 +47,9 @@ public static BigDecimal calculatePercentage(long numerator, long denominator, i * @return The calculated percentage (scale 2, HALF_UP rounding), or * BigDecimal.ZERO if denominator is zero. */ - public static BigDecimal calculatePercentage(long numerator, long denominator) { - return calculatePercentage(numerator, denominator, DEFAULT_SCALE, RoundingMode.HALF_UP); + public static BigDecimal calculatePercentage(final long numerator, + final long denominator) { + return calculatePercentage(numerator, denominator, DEFAULT_SCALE, + RoundingMode.HALF_UP); } - } diff --git a/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java index 956aa75..4bb2af3 100644 --- a/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java +++ b/src/main/java/com/Podzilla/analytics/validation/annotations/ValidDateRange.java @@ -1,12 +1,16 @@ 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.DateRangeValidator; import jakarta.validation.Constraint; import jakarta.validation.Payload; -import java.lang.annotation.*; - @Documented @Constraint(validatedBy = DateRangeValidator.class) @Target(ElementType.TYPE) 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 b464de4..079bd20 100644 --- a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java +++ b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java @@ -6,10 +6,11 @@ import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; -public class DateRangeValidator implements ConstraintValidator { - +public final class DateRangeValidator implements + ConstraintValidator { @Override - public boolean isValid(DateRangeRequest request, ConstraintValidatorContext context) { + public boolean isValid(final DateRangeRequest request, + final ConstraintValidatorContext context) { if (request.getStartDate() == null || request.getEndDate() == null) { return true; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index dbadc86..b8de2fb 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -12,4 +12,4 @@ spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.generate-ddl=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true -spring.jpa.properties.hibernate.use_sql_comments=true \ No newline at end of file +spring.jpa.properties.hibernate.use_sql_comments=true From d41dad07d7b3a9bfc84ba4a1e2692e0f45fc40f3 Mon Sep 17 00:00:00 2001 From: Ahmad Hoseiny Date: Thu, 8 May 2025 11:05:43 +0300 Subject: [PATCH 10/28] feat: added order reports (#15) * feat: added order reports * fix: fix lint issues * fix: fix order linter errors * fix: fix lint issues * fix: fix lint issues remaining some problematic issues * fix: fix lint issues * refactor: adhering to conventions --- .../controllers/CourierReportController.java | 8 +- .../controllers/OrderReportController.java | 60 +++++++++++++ .../CourierAverageRatingResponse.java | 2 +- .../CourierDeliveryCountResponse.java | 2 +- .../CourierPerformanceReportResponse.java | 2 +- .../CourierSuccessRateResponse.java | 2 +- .../order/OrderFailureReasonsResponse.java | 22 +++++ .../api/dtos/order/OrderFailureResponse.java | 26 ++++++ .../api/dtos/order/OrderRegionResponse.java | 33 ++++++++ .../api/dtos/order/OrderStatusResponse.java | 22 +++++ .../order/OrderFailureRateProjection.java | 7 ++ .../order/OrderFailureReasonsProjection.java | 6 ++ .../order/OrderRegionProjection.java | 11 +++ .../order/OrderStatusProjection.java | 7 ++ .../repositories/OrderRepository.java | 63 ++++++++++++++ .../services/CourierAnalyticsService.java | 10 +-- .../services/OrderAnalyticsService.java | 84 +++++++++++++++++++ .../analytics/util/DatetimeFormatter.java | 18 ++++ 18 files changed, 372 insertions(+), 13 deletions(-) rename src/main/java/com/Podzilla/analytics/api/dtos/{ => courier}/CourierAverageRatingResponse.java (92%) rename src/main/java/com/Podzilla/analytics/api/dtos/{ => courier}/CourierDeliveryCountResponse.java (92%) rename src/main/java/com/Podzilla/analytics/api/dtos/{ => courier}/CourierPerformanceReportResponse.java (94%) rename src/main/java/com/Podzilla/analytics/api/dtos/{ => courier}/CourierSuccessRateResponse.java (92%) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureReasonsResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/order/OrderStatusResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureRateProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureReasonsProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/order/OrderStatusProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index c437ac0..4fa13a5 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -8,11 +8,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.Podzilla.analytics.api.dtos.CourierAverageRatingResponse; -import com.Podzilla.analytics.api.dtos.CourierDeliveryCountResponse; -import com.Podzilla.analytics.api.dtos.CourierPerformanceReportResponse; -import com.Podzilla.analytics.api.dtos.CourierSuccessRateResponse; import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import com.Podzilla.analytics.api.dtos.courier.CourierAverageRatingResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierDeliveryCountResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; import com.Podzilla.analytics.services.CourierAnalyticsService; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java index 32054a8..9c073c7 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java @@ -2,13 +2,73 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; + import com.Podzilla.analytics.services.OrderAnalyticsService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import java.util.List; + +import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import com.Podzilla.analytics.api.dtos.order.OrderFailureResponse; +import com.Podzilla.analytics.api.dtos.order.OrderRegionResponse; +import com.Podzilla.analytics.api.dtos.order.OrderStatusResponse; + + @RequiredArgsConstructor @RestController @RequestMapping("/orders") public class OrderReportController { private final OrderAnalyticsService orderAnalyticsService; + + @Operation(summary = "Get order counts and revenue by region", + description = "Returns the total number of orders" + + "placed in each region and their corresponding average revenue") + @GetMapping("/by-region") + public ResponseEntity> getOrdersByRegion( + @Valid @ModelAttribute final DateRangeRequest dateRange + ) { + List ordersByRegion = + orderAnalyticsService.getOrdersByRegion( + dateRange.getStartDate(), + dateRange.getEndDate() + ); + return ResponseEntity.ok(ordersByRegion); + } + + @Operation(summary = "Get order status counts", + description = "Returns the total number of orders" + + "in each status (e.g., COMPLETED, SHIPPED, FAILED)") + @GetMapping("/status-counts") + public ResponseEntity> getOrdersStatusCounts( + @Valid @ModelAttribute final DateRangeRequest dateRange + ) { + List orderStatusCounts = + orderAnalyticsService.getOrdersStatusCounts( + dateRange.getStartDate(), + dateRange.getEndDate() + ); + return ResponseEntity.ok(orderStatusCounts); + } + + @Operation(summary = "Get order failures", + description = "Returns the percentage of failed orders" + + "and a list of the failure reasons" + + "with their corresponding frequency") + @GetMapping("/failures") + public ResponseEntity getOrdersFailures( + @Valid @ModelAttribute final DateRangeRequest dateRange + ) { + OrderFailureResponse orderFailures = + orderAnalyticsService.getOrdersFailures( + dateRange.getStartDate(), + dateRange.getEndDate() + ); + return ResponseEntity.ok(orderFailures); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java similarity index 92% rename from src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java rename to src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java index ba3a30e..e3bce5d 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierAverageRatingResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierAverageRatingResponse.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.dtos; +package com.Podzilla.analytics.api.dtos.courier; import java.math.BigDecimal; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java similarity index 92% rename from src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java rename to src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java index a9b83d3..1aa5e88 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierDeliveryCountResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierDeliveryCountResponse.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.dtos; +package com.Podzilla.analytics.api.dtos.courier; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java similarity index 94% rename from src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java rename to src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java index ae22554..9b91740 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierPerformanceReportResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierPerformanceReportResponse.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.dtos; +package com.Podzilla.analytics.api.dtos.courier; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java similarity index 92% rename from src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java rename to src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java index 4e47b1e..ffa0758 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/CourierSuccessRateResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/courier/CourierSuccessRateResponse.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.dtos; +package com.Podzilla.analytics.api.dtos.courier; import java.math.BigDecimal; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureReasonsResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureReasonsResponse.java new file mode 100644 index 0000000..a94cddf --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureReasonsResponse.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.api.dtos.order; + +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 OrderFailureReasonsResponse { + + @Schema(description = "Reason for order failure", + example = "Payment declined") + private String reason; + + @Schema(description = "Count of orders with this failure reason", + example = "150") + private Long count; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureResponse.java new file mode 100644 index 0000000..913a9df --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderFailureResponse.java @@ -0,0 +1,26 @@ +package com.Podzilla.analytics.api.dtos.order; + +import java.math.BigDecimal; +import java.util.List; + +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 OrderFailureResponse { + + @Schema(description = "Percentage of orders that failed", + example = "0.25") + private BigDecimal failureRate; + + @Schema(description = "List of reasons for order failures", + example = "[{\"reason\": \"Out of stock\", \"count\": 10}," + + "{\"reason\": \"Payment failure\", \"count\": 5}]") + private List reasons; +} 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 new file mode 100644 index 0000000..4069630 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderRegionResponse.java @@ -0,0 +1,33 @@ +package com.Podzilla.analytics.api.dtos.order; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; + + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class OrderRegionResponse { + + @Schema(description = "Region ID", example = "12345") + private Long regionId; + + @Schema(description = "city name", example = "Metropolis") + private String city; + + @Schema(description = "country name", example = "USA") + private String country; + + @Schema(description = "number of orders in the region", + example = "100") + private Long orderCount; + + @Schema(description = "average revenue generated from orders in the region", + example = "10000.00") + private BigDecimal averageOrderValue; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderStatusResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderStatusResponse.java new file mode 100644 index 0000000..858215f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/order/OrderStatusResponse.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.api.dtos.order; + +import com.Podzilla.analytics.models.Order.OrderStatus; + +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 OrderStatusResponse { + + @Schema(description = "Order status", example = "COMPLETED") + private OrderStatus status; + + @Schema(description = "Count of orders with this status", example = "150") + private Long count; +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureRateProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureRateProjection.java new file mode 100644 index 0000000..7488684 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureRateProjection.java @@ -0,0 +1,7 @@ +package com.Podzilla.analytics.api.projections.order; + +import java.math.BigDecimal; + +public interface OrderFailureRateProjection { + BigDecimal getFailureRate(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureReasonsProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureReasonsProjection.java new file mode 100644 index 0000000..ca6e1d9 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderFailureReasonsProjection.java @@ -0,0 +1,6 @@ +package com.Podzilla.analytics.api.projections.order; + +public interface OrderFailureReasonsProjection { + String getReason(); + Long getCount(); +} 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 new file mode 100644 index 0000000..8b7816f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.api.projections.order; + +import java.math.BigDecimal; + +public interface OrderRegionProjection { + Long getRegionId(); + String getCity(); + String getCountry(); + Long getOrderCount(); + BigDecimal getAverageOrderValue(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/order/OrderStatusProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderStatusProjection.java new file mode 100644 index 0000000..93ba6b8 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderStatusProjection.java @@ -0,0 +1,7 @@ +package com.Podzilla.analytics.api.projections.order; +import com.Podzilla.analytics.models.Order.OrderStatus; + +public interface OrderStatusProjection { + OrderStatus getStatus(); + Long getCount(); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index f0960d6..a0c2f36 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -1,8 +1,71 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import com.Podzilla.analytics.api.projections.order.OrderFailureRateProjection; +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.models.Order; + + public interface OrderRepository extends JpaRepository { + + @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) + 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 " + + "Group by o.status " + + "Order by count desc", + nativeQuery = true) + List findOrderStatusCounts( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate + ); + + @Query(value = "Select o.failure_reason as reason, " + + "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) + 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) + OrderFailureRateProjection calculateFailureRate( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate + ); } diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 12501d6..89a9340 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -7,10 +7,10 @@ import org.springframework.stereotype.Service; -import com.Podzilla.analytics.api.dtos.CourierAverageRatingResponse; -import com.Podzilla.analytics.api.dtos.CourierDeliveryCountResponse; -import com.Podzilla.analytics.api.dtos.CourierPerformanceReportResponse; -import com.Podzilla.analytics.api.dtos.CourierSuccessRateResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierAverageRatingResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierDeliveryCountResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.util.MetricCalculator; @@ -20,7 +20,7 @@ @RequiredArgsConstructor @Service public class CourierAnalyticsService { - private final CourierRepository courierRepository; + private final CourierRepository courierRepository; private List getCourierPerformanceData( final LocalDate startDate, diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java index afadc56..9af3233 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -1,11 +1,95 @@ package com.Podzilla.analytics.services; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.order.OrderFailureResponse; +import com.Podzilla.analytics.api.dtos.order.OrderFailureReasonsResponse; +import com.Podzilla.analytics.api.dtos.order.OrderRegionResponse; +import com.Podzilla.analytics.api.dtos.order.OrderStatusResponse; +import com.Podzilla.analytics.api.projections.order.OrderFailureRateProjection; +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.OrderRepository; +import com.Podzilla.analytics.util.DatetimeFormatter; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class OrderAnalyticsService { + + + private final OrderRepository orderRepository; + public List getOrdersByRegion( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = + 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()) + .city(data.getCity()) + .country(data.getCountry()) + .orderCount(data.getOrderCount()) + .averageOrderValue(data.getAverageOrderValue()) + .build()) + .toList(); + } + + public List getOrdersStatusCounts( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = + DatetimeFormatter.convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = + DatetimeFormatter.convertEndDateToDatetime(endDate); + List orderStatusCounts = + orderRepository.findOrderStatusCounts(startDateTime, endDateTime); + return orderStatusCounts.stream() + .map(data -> OrderStatusResponse.builder() + .status(data.getStatus()) + .count(data.getCount()) + .build()) + .toList(); + } + + public OrderFailureResponse getOrdersFailures( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = + DatetimeFormatter.convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = + DatetimeFormatter.convertEndDateToDatetime(endDate); + List failureReasons = + orderRepository.findFailureReasons(startDateTime, endDateTime); + OrderFailureRateProjection failureRate = + orderRepository.calculateFailureRate(startDateTime, endDateTime); + List + failureReasonsDTO = failureReasons.stream() + .map(data -> OrderFailureReasonsResponse.builder() + .reason(data.getReason()) + .count(data.getCount()) + .build()) + .toList(); + return OrderFailureResponse.builder() + .reasons(failureReasonsDTO) + .failureRate(failureRate.getFailureRate()) + .build(); + } } diff --git a/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java b/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java new file mode 100644 index 0000000..8a3c110 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/util/DatetimeFormatter.java @@ -0,0 +1,18 @@ +package com.Podzilla.analytics.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class DatetimeFormatter { + public static LocalDateTime convertStartDateToDatetime( + final LocalDate startDate + ) { + return startDate.atStartOfDay(); + } + public static LocalDateTime convertEndDateToDatetime( + final LocalDate endDate + ) { + return endDate.atTime(LocalTime.MAX); + } +} From 9c6fff322fb7eaf4b01f231b22554fbf8aa46114 Mon Sep 17 00:00:00 2001 From: Malek Mohamed Noureldean Elkssas <87043730+malekelkssas@users.noreply.github.com> Date: Fri, 9 May 2025 11:24:55 +0300 Subject: [PATCH 11/28] feat: add customer and inventory services (#9) * feat: add customer and inventory endpoints - /customers/top-spenders for customer spending analysis - /inventory/low-stock for threshold monitoring - /inventory/value/by-category for category-wise valuation * feat: add validation on the dtos featL add swagger docs * fix: check style --- pom.xml | 4 +- .../controllers/CustomerReportController.java | 25 ++++++++++++ .../InventoryReportController.java | 33 +++++++++++++++ .../api/dtos/DateRangePaginationRequest.java | 40 +++++++++++++++++++ .../analytics/api/dtos/PaginationRequest.java | 22 ++++++++++ .../CustomersTopSpendersResponse.java | 17 ++++++++ .../InventoryValueByCategoryResponse.java | 16 ++++++++ .../inventory/LowStockProductResponse.java | 17 ++++++++ .../CustomersTopSpendersProjection.java | 11 +++++ .../InventoryValueByCategoryProjection.java | 9 +++++ .../LowStockProductProjection.java | 12 ++++++ .../repositories/CustomerRepository.java | 22 ++++++++++ .../InventorySnapshotRepository.java | 32 ++++++++++++++- .../services/CustomerAnalyticsService.java | 27 ++++++++++++- .../services/InventoryAnalyticsService.java | 36 ++++++++++++++++- src/main/resources/application.properties | 4 +- 16 files changed, 320 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/inventory/InventoryValueByCategoryResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java diff --git a/pom.xml b/pom.xml index 4e0ef99..b9290bf 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ - 21 + 23 @@ -133,4 +133,4 @@ - + \ No newline at end of file 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 72415dd..7428253 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -2,13 +2,38 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; + import com.Podzilla.analytics.services.CustomerAnalyticsService; +import com.Podzilla.analytics.api.dtos.DateRangePaginationRequest; +import com.Podzilla.analytics.api.dtos.customer.CustomersTopSpendersResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import java.util.List; + +@Tag(name = "Customer Reports", description = "APIs for customer analytics and" + + "reporting") @RequiredArgsConstructor @RestController @RequestMapping("/customers") public class CustomerReportController { private final CustomerAnalyticsService customerAnalyticsService; + + @Operation(summary = "Get top spending customers", description = "Returns" + + "a paginated list of customers who have spent" + + "the most money within the specified date range") + @GetMapping("/top-spenders") + public List getTopSpenders( + @Valid @ModelAttribute final DateRangePaginationRequest request) { + return customerAnalyticsService.getTopSpenders( + request.getStartDate(), + request.getEndDate(), + request.getPage(), + request.getSize()); + } } 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 762e2cf..952c4cf 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -1,14 +1,47 @@ package com.Podzilla.analytics.api.controllers; +import org.springframework.data.domain.Page; + +import com.Podzilla.analytics.api.dtos.inventory.InventoryValueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.inventory.LowStockProductResponse; +import com.Podzilla.analytics.api.dtos.PaginationRequest; +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.services.InventoryAnalyticsService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.util.List; +@Tag(name = "Inventory Reports", description = "APIs for inventory analytics" + + "and reporting") @RequiredArgsConstructor @RestController @RequestMapping("/inventory") public class InventoryReportController { private final InventoryAnalyticsService inventoryAnalyticsService; + + @Operation(summary = "Get inventory" + + "value by category", description = "Returns" + + "the total value of inventory " + + "grouped by product categories") + @GetMapping("/value/by-category") + public List getInventoryValueByCategor() { + return inventoryAnalyticsService.getInventoryValueByCategory(); + } + + @Operation(summary = "Get low stock products", description = "Returns " + + "a paginated list of products that are running low on stock") + @GetMapping("/low-stock") + public Page getLowStockProducts( + @Valid @ModelAttribute final PaginationRequest paginationRequest) { + return inventoryAnalyticsService.getLowStockProducts( + paginationRequest.getPage(), + paginationRequest.getSize()); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java new file mode 100644 index 0000000..580803e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java @@ -0,0 +1,40 @@ +package com.Podzilla.analytics.api.dtos; + +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; +import com.Podzilla.analytics.validation.annotations.ValidDateRange; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; +import io.swagger.v3.oas.annotations.media.Schema; + +@ValidDateRange +@Getter +@AllArgsConstructor +public class DateRangePaginationRequest { + + @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; + + @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; + + @Min(value = 0, message = "Page " + + "number must be greater than or equal to 0") + @Schema(description = "Page number " + + "(zero-based)", example = "0", required = true) + private int page; + + @Min(value = 1, message = "Page " + + "size must be greater than 0") + @Schema(description = "Number of " + + "items per page", example = "20", required = true) + private int size; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java new file mode 100644 index 0000000..fc650e8 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.api.dtos; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Getter; +import io.swagger.v3.oas.annotations.media.Schema; + +@Getter +@AllArgsConstructor +public class PaginationRequest { + + @Min(value = 0, message = "Page number " + + "must be greater than or equal to 0") + @Schema(description = "Page number " + + "(zero-based)", example = "0", required = true) + private int page; + + @Min(value = 1, message = "Page size " + + "must be greater than 0") + @Schema(description = "Number of " + + "items per page", example = "20", required = true) + private int size; +} 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 new file mode 100644 index 0000000..dd2a4db --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/customer/CustomersTopSpendersResponse.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.api.dtos.customer; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CustomersTopSpendersResponse { + private Long customerId; + private String customerName; + private BigDecimal totalSpending; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/inventory/InventoryValueByCategoryResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/inventory/InventoryValueByCategoryResponse.java new file mode 100644 index 0000000..5623bff --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/inventory/InventoryValueByCategoryResponse.java @@ -0,0 +1,16 @@ +package com.Podzilla.analytics.api.dtos.inventory; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class InventoryValueByCategoryResponse { + private String category; + private BigDecimal totalStockValue; +} 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 new file mode 100644 index 0000000..aa1596f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/inventory/LowStockProductResponse.java @@ -0,0 +1,17 @@ +package com.Podzilla.analytics.api.dtos.inventory; + +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LowStockProductResponse { + private Long productId; + private String productName; + private Long currentQuantity; + private Long threshold; +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java new file mode 100644 index 0000000..6bc0973 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.api.projections; + +import java.math.BigDecimal; + +public interface CustomersTopSpendersProjection { + Long getCustomerId(); + + String getCustomerName(); + + BigDecimal getTotalSpending(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java new file mode 100644 index 0000000..7d8c399 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java @@ -0,0 +1,9 @@ +package com.Podzilla.analytics.api.projections; + +import java.math.BigDecimal; + +public interface InventoryValueByCategoryProjection { + String getCategory(); + + BigDecimal getTotalStockValue(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java new file mode 100644 index 0000000..ac2e693 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java @@ -0,0 +1,12 @@ +package com.Podzilla.analytics.api.projections; + + +public interface LowStockProductProjection { + Long getProductId(); + + String getProductName(); + + Long getCurrentQuantity(); + + Long getThreshold(); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index ee9f907..d92ba34 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -1,8 +1,30 @@ package com.Podzilla.analytics.repositories; +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.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import com.Podzilla.analytics.api.projections.CustomersTopSpendersProjection; import com.Podzilla.analytics.models.Customer; +import java.time.LocalDateTime; + +@Repository 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); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java index d0098a4..4a2faf0 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java @@ -1,9 +1,39 @@ 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.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.LowStockProductProjection; import com.Podzilla.analytics.models.InventorySnapshot; +@Repository public interface InventorySnapshotRepository - extends JpaRepository { + 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/services/CustomerAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java index 10cc66a..aab2d88 100644 --- a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java @@ -1,11 +1,36 @@ package com.Podzilla.analytics.services; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.customer.CustomersTopSpendersResponse; +import com.Podzilla.analytics.repositories.CustomerRepository; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor +import java.time.LocalDateTime; +import java.util.List; + @Service +@RequiredArgsConstructor public class CustomerAnalyticsService { + private final CustomerRepository customerRepository; + + public List getTopSpenders( + final LocalDateTime startDate, + final LocalDateTime endDate, + final int page, + final int size) { + PageRequest pageRequest = PageRequest.of(page, size); +List topSpenders = customerRepository +.findTopSpenders(startDate, endDate, pageRequest) +.stream() +.map(row -> CustomersTopSpendersResponse.builder() +.customerId(row.getCustomerId()) +.customerName(row.getCustomerName()) +.totalSpending(row.getTotalSpending()) +.build()) +.toList(); + return topSpenders; + } } diff --git a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java index ba1e12d..7bcefa3 100644 --- a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java @@ -1,10 +1,44 @@ package com.Podzilla.analytics.services; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.inventory.InventoryValueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.inventory.LowStockProductResponse; +import com.Podzilla.analytics.repositories.InventorySnapshotRepository; + +import java.util.List; import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor @Service +@RequiredArgsConstructor public class InventoryAnalyticsService { + private final InventorySnapshotRepository inventoryRepo; + +public List getInventoryValueByCategory() { +List invVByCy = inventoryRepo +.getInventoryValueByCategory() +.stream() +.map(row -> InventoryValueByCategoryResponse.builder() +.category(row.getCategory()) +.totalStockValue(row.getTotalStockValue()) +.build()) + .toList(); + return invVByCy; + } + +public Page getLowStockProducts(final int page, + final int size) { + PageRequest pageRequest = PageRequest.of(page, size); +Page lowStockPro = + inventoryRepo.getLowStockProducts(pageRequest) + .map(row -> LowStockProductResponse.builder() + .productId(row.getProductId()) + .productName(row.getProductName()) + .currentQuantity(row.getCurrentQuantity()) + .threshold(row.getThreshold()) + .build()); + return lowStockPro; + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b8de2fb..c139926 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,8 +2,8 @@ spring.application.name=analytics # Database Configuration spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/analytics_db_dev} -spring.datasource.username=${SPRING_DATASOURCE_USERNAME:analytics_user} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:password} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:postgres} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:123} spring.datasource.driver-class-name=org.postgresql.Driver # JPA & Hibernate Configuration From c37e85b99f359b8e83e4af63d57d99a60432669b Mon Sep 17 00:00:00 2001 From: Mohamed Hassan Date: Sun, 11 May 2025 00:12:00 +0300 Subject: [PATCH 12/28] Feature/fulfillment reports (#18) * feat: Implement fulfillment time reports API * fix: Resolve lint errors * Fix: FulfillmentAnalyticsService and repository integration * refactor: update FulfillmentReportController to use FulfillmentRequestDTO and improve error handling for groupBy values * refactor: created seperate request dtos for fulfillment endpoints --------- Co-authored-by: Mohamed --- .../FulfillmentReportController.java | 49 ++- .../FulfillmentPlaceToShipRequest.java | 45 +++ .../FulfillmentShipToDeliverRequest.java | 48 +++ .../fulfillment/FulfillmentTimeResponse.java | 26 ++ .../FulfillmentTimeProjection.java | 6 + .../repositories/OrderRepository.java | 131 +++++-- .../services/FulfillmentAnalyticsService.java | 106 +++++- .../FulfillmentReportControllerTest.java | 335 ++++++++++++++++++ 8 files changed, 708 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentTimeResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/fulfillment/FulfillmentTimeProjection.java create mode 100644 src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java 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 8b2e7ae..1777f3c 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -1,16 +1,63 @@ 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.fulfillment +.FulfillmentPlaceToShipRequest; +import com.Podzilla.analytics.api.dtos.fulfillment +.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/fulfillment") 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" + + " an order was placed and when it was shipped, grouped" + + " by the specified dimension") + @GetMapping("/place-to-ship-time") + public ResponseEntity> getPlaceToShipTime( + @Valid @ModelAttribute final FulfillmentPlaceToShipRequest req) { + + List reportData = fulfillmentAnalyticsService + .getPlaceToShipTimeResponse( + req.getStartDate(), + req.getEndDate(), + req.getGroupBy()); + return ResponseEntity.ok(reportData); + } + + @Operation(summary = "Get average time from shipping to delivery", + description = "Returns the average time (in hours) between when" + + " an order was shipped and when it was delivered, grouped" + + " by the specified dimension") + @GetMapping("/ship-to-deliver-time") + public ResponseEntity> getShipToDeliverTime( + @Valid @ModelAttribute final FulfillmentShipToDeliverRequest req) { + + log.debug(req.toString()); + + List reportData = fulfillmentAnalyticsService + .getShipToDeliverTimeResponse( + req.getStartDate(), + req.getEndDate(), + req.getGroupBy()); + return ResponseEntity.ok(reportData); + } } 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 new file mode 100644 index 0000000..6af9a21 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java @@ -0,0 +1,45 @@ +package com.Podzilla.analytics.api.dtos.fulfillment; + +import java.time.LocalDate; + +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@ValidDateRange +public class FulfillmentPlaceToShipRequest { + + /** + * Enum for grouping options in place-to-ship analytics. + */ + public enum PlaceToShipGroupBy { + REGION, OVERALL + } + + @NotNull(message = "startDate is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @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) + @Schema(description = "End date of the range (inclusive)", + example = "2024-01-31", required = true) + private LocalDate endDate; + + @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; + +} 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 new file mode 100644 index 0000000..bb368bc --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java @@ -0,0 +1,48 @@ +package com.Podzilla.analytics.api.dtos.fulfillment; + +import java.time.LocalDate; + +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Unified request DTO for fulfillment analytics operations. + * Contains all parameters needed for analytics API endpoints. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@ValidDateRange +public class FulfillmentShipToDeliverRequest { + + /** + * Enum for grouping options in ship-to-deliver analytics. + */ + public enum ShipToDeliverGroupBy { + REGION, OVERALL, COURIER + } + + @NotNull(message = "startDate is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @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) + @Schema(description = "End date of the range (inclusive)", + example = "2024-01-31", required = true) + private LocalDate endDate; + + @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; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentTimeResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentTimeResponse.java new file mode 100644 index 0000000..c38e90b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentTimeResponse.java @@ -0,0 +1,26 @@ +package com.Podzilla.analytics.api.dtos.fulfillment; + +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 FulfillmentTimeResponse { + + @Schema( + description = "Group by value (overall or by ID)", + example = "OVERALL") + private String groupByValue; + + @Schema( + description = "Average duration in hours", + example = "48.5") + private BigDecimal averageDuration; +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/fulfillment/FulfillmentTimeProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/fulfillment/FulfillmentTimeProjection.java new file mode 100644 index 0000000..a32be7e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/fulfillment/FulfillmentTimeProjection.java @@ -0,0 +1,6 @@ +package com.Podzilla.analytics.api.projections.fulfillment; + +public interface FulfillmentTimeProjection { + String getGroupByValue(); + Double getAverageDuration(); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index a0c2f36..3b6aa17 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -7,65 +7,126 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import com.Podzilla.analytics.api.projections.fulfillment.FulfillmentTimeProjection; import com.Podzilla.analytics.api.projections.order.OrderFailureRateProjection; 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.models.Order; +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) + 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) + List findPlaceToShipTimeByRegion( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); + + // --- Ship to Deliver Time Projections --- + + @Query(value = "SELECT 'OVERALL' as groupByValue, " + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED'", + nativeQuery = true) + 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) + 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) + 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) + + "From orders o " + + "inner join regions r on o.region_id = r.id " + + "where o.final_status_timestamp between :startDate and :endDate " + + "Group by o.region_id, r.city, r.country " + + "Order by orderCount desc, averageOrderValue desc", + nativeQuery = true) List findOrdersByRegion( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate - ); + @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 " - + "Group by o.status " - + "Order by count desc", - nativeQuery = true) + + "From orders o " + + "where o.final_status_timestamp between :startDate and :endDate " + + "Group by o.status " + + "Order by count desc", + nativeQuery = true) List findOrderStatusCounts( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate - ); + @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) + + "From orders o " + + "where o.final_status_timestamp between :startDate and :endDate " + + "and o.status = 'FAILED' " + + "Group by o.failure_reason " + + "Order by count desc", + nativeQuery = true) List findFailureReasons( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate - ); + @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) + "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) OrderFailureRateProjection calculateFailureRate( - @Param("startDate") LocalDateTime startDate, - @Param("endDate") LocalDateTime endDate - ); + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java index 196d38f..e701a78 100644 --- a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java @@ -1,11 +1,113 @@ package com.Podzilla.analytics.services; import org.springframework.stereotype.Service; - - +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 lombok.RequiredArgsConstructor; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Service public class FulfillmentAnalyticsService { + + private final OrderRepository orderRepository; + + public List getPlaceToShipTimeResponse( + final LocalDate startDate, + final LocalDate endDate, + final PlaceToShipGroupBy groupBy) { + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + List results = new ArrayList<>(); + + switch (groupBy) { + case OVERALL: + FulfillmentTimeProjection overall = orderRepository + .findPlaceToShipTimeOverall(startDateTime, endDateTime); + if (overall != null) { + results.add(convertToResponse(overall)); + } + break; + case REGION: + List byRegion = orderRepository + .findPlaceToShipTimeByRegion( + startDateTime, endDateTime); + results.addAll(byRegion.stream() + .map(this::convertToResponse) + .collect(Collectors.toList())); + break; + default: + // Handle unknown groupBy or throw an exception + break; + } + + return results; + } + + public List getShipToDeliverTimeResponse( + final LocalDate startDate, + final LocalDate endDate, + final ShipToDeliverGroupBy groupBy) { + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + List results = new ArrayList<>(); + + switch (groupBy) { + case OVERALL: + FulfillmentTimeProjection overall = orderRepository + .findShipToDeliverTimeOverall( + startDateTime, endDateTime); + if (overall != null) { + results.add(convertToResponse(overall)); + } + break; + case REGION: + List byRegion = orderRepository + .findShipToDeliverTimeByRegion( + startDateTime, endDateTime); + results.addAll(byRegion.stream() + .map(this::convertToResponse) + .collect(Collectors.toList())); + break; + case COURIER: + List byCourier = orderRepository + .findShipToDeliverTimeByCourier( + startDateTime, endDateTime); + results.addAll(byCourier.stream() + .map(this::convertToResponse) + .collect(Collectors.toList())); + break; + default: + // Handle unknown groupBy or throw an exception + break; + } + + return results; + } + + private FulfillmentTimeResponse convertToResponse( + final FulfillmentTimeProjection projection) { + return FulfillmentTimeResponse.builder() + .groupByValue(projection.getGroupByValue()) + .averageDuration( + BigDecimal.valueOf(projection.getAverageDuration())) + .build(); + } } diff --git a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java new file mode 100644 index 0000000..a72a54e --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java @@ -0,0 +1,335 @@ +package com.Podzilla.analytics.api.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; +import com.Podzilla.analytics.services.FulfillmentAnalyticsService; + +public class FulfillmentReportControllerTest { + + private FulfillmentReportController controller; + private FulfillmentAnalyticsService mockService; + + private LocalDate startDate; + private LocalDate endDate; + private List overallTimeResponses; + private List regionTimeResponses; + private List courierTimeResponses; + + @BeforeEach + public void setup() { + mockService = mock(FulfillmentAnalyticsService.class); + controller = new FulfillmentReportController(mockService); + + startDate = LocalDate.of(2024, 1, 1); + endDate = LocalDate.of(2024, 1, 31); + + // Setup test data + overallTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("OVERALL") + .averageDuration(BigDecimal.valueOf(24.5)) + .build()); + + regionTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("RegionID_1") + .averageDuration(BigDecimal.valueOf(20.2)) + .build(), + FulfillmentTimeResponse.builder() + .groupByValue("RegionID_2") + .averageDuration(BigDecimal.valueOf(28.7)) + .build()); + + courierTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("CourierID_1") + .averageDuration(BigDecimal.valueOf(18.3)) + .build(), + FulfillmentTimeResponse.builder() + .groupByValue("CourierID_2") + .averageDuration(BigDecimal.valueOf(22.1)) + .build()); + } + + @Test + public void testGetPlaceToShipTime_Overall() { + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); + } + + @Test + public void testGetPlaceToShipTime_ByRegion() { + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.REGION)) + .thenReturn(regionTimeResponses); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.REGION); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(regionTimeResponses, response.getBody()); + assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + } + + @Test + public void testGetShipToDeliverTime_Overall() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + } + + @Test + public void testGetShipToDeliverTime_ByRegion() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.REGION)) + .thenReturn(regionTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.REGION); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(regionTimeResponses, response.getBody()); + assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + } + + @Test + public void testGetShipToDeliverTime_ByCourier() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.COURIER)) + .thenReturn(courierTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.COURIER); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(courierTimeResponses, response.getBody()); + assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); + } + + // Edge case tests + + @Test + public void testGetPlaceToShipTime_EmptyResponse() { + // Configure mock service to return empty list + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + @Test + public void testGetShipToDeliverTime_EmptyResponse() { + // Configure mock service to return empty list + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + // @Test + // public void testGetPlaceToShipTime_InvalidGroupBy() { + // // Create request with invalid groupBy + // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + // startDate, endDate, null); + + // // Execute the method - should return bad request due to validation error + // ResponseEntity> response = controller.getPlaceToShipTime(request); + + // // Verify response + // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + // } + + // @Test + // public void testGetShipToDeliverTime_InvalidGroupBy() { + // // Create request with invalid groupBy + // FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + // startDate, endDate, null); + + // // Execute the method - should return bad request due to validation error + // ResponseEntity> response = controller.getShipToDeliverTime(request); + + // // Verify response + // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + // } + + @Test + public void testGetPlaceToShipTime_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request with same start and end date + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + sameDate, sameDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + } + + @Test + public void testGetShipToDeliverTime_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request with same start and end date + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + } + + @Test + public void testGetPlaceToShipTime_FutureDates() { + // Test future dates + LocalDate futureStart = LocalDate.now().plusDays(1); + LocalDate futureEnd = LocalDate.now().plusDays(30); + + // Configure mock service - should return empty for future dates + when(mockService.getPlaceToShipTimeResponse( + futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request with future dates + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + @Test + public void testGetShipToDeliverTime_ServiceException() { + // Configure mock service to throw exception + when(mockService.getShipToDeliverTimeResponse( + any(), any(), any())) + .thenThrow(new RuntimeException("Service error")); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method - controller should handle exception + // Note: Actual behavior depends on how controller handles exceptions + // This might need adjustment based on actual implementation + try { + controller.getShipToDeliverTime(request); + } catch (RuntimeException e) { + assertEquals("Service error", e.getMessage()); + } + } +} \ No newline at end of file From 26fb344f1802d2f669e5c13920a00a271c8ff7ee Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Mon, 12 May 2025 13:28:35 +0300 Subject: [PATCH 13/28] chore: rename dockerfile to Dockerfile for case consistency (#21) --- dockerfile => Dockerfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dockerfile => Dockerfile (100%) diff --git a/dockerfile b/Dockerfile similarity index 100% rename from dockerfile rename to Dockerfile From bfc8a50b5c04e8be7a6bc9f713d88be715d932f3 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Mon, 12 May 2025 16:49:41 +0300 Subject: [PATCH 14/28] feat: add integration with the shared library and rabbit mq (#19) --- docker-compose.yml | 4 ++ pom.xml | 16 +++++++ .../analytics/AnalyticsApplication.java | 3 +- .../analytics/api/dtos/ErrorResponse.java | 6 +-- .../config/GlobalExceptionHandler.java | 45 +++++++++++-------- src/main/resources/application.properties | 6 +++ 6 files changed, 55 insertions(+), 25 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index d335ee2..57ea10d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,10 @@ services: SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db_dev SPRING_DATASOURCE_USERNAME: analytics_user SPRING_DATASOURCE_PASSWORD: password + SPRING_RABBITMQ_HOST: rabbitmq # RabbitMQ container name or use its IP if needed + SPRING_RABBITMQ_PORT: 5672 + SPRING_RABBITMQ_USERNAME: guest + SPRING_RABBITMQ_PASSWORD: guest depends_on: - db diff --git a/pom.xml b/pom.xml index b9290bf..006f6d3 100644 --- a/pom.xml +++ b/pom.xml @@ -84,7 +84,23 @@ springdoc-openapi-starter-webmvc-ui 2.5.0 + + com.github.Podzilla + podzilla-utils-lib + v1.1.5 + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + + + + jitpack.io + https://jitpack.io + + diff --git a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java index 30ad798..683b0d5 100644 --- a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java +++ b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java @@ -2,12 +2,13 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = { "com.podzilla", "com.Podzilla" }) public class AnalyticsApplication { public static void main(final String[] args) { SpringApplication.run(AnalyticsApplication.class, args); } - } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java index 2bb79e0..9f54982 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/ErrorResponse.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.dtos; // Or a common errors package +package com.Podzilla.analytics.api.dtos; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; -import java.util.Map; @Data @Builder @@ -17,8 +16,5 @@ public class ErrorResponse { private LocalDateTime timestamp; private int status; - private String error; private String message; - private Map fieldErrors; - private String path; } diff --git a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java index 5a15860..3d9cc7d 100644 --- a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java +++ b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java @@ -5,41 +5,50 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; -import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import java.time.LocalDateTime; -import java.util.Map; -import java.util.stream.Collectors; @ControllerAdvice @Slf4j public class GlobalExceptionHandler { + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + final MethodArgumentNotValidException ex, + final WebRequest request) { + return createSimpleErrorResponse(ex, request); + } + @ExceptionHandler(BindException.class) public ResponseEntity handleBindException( final BindException ex, final WebRequest request) { + return createSimpleErrorResponse(ex, request); + } + + private ResponseEntity createSimpleErrorResponse( + final BindException ex, + final WebRequest request) { - Map fieldErrors = ex.getBindingResult() - .getFieldErrors() - .stream() - .collect(Collectors.toMap( - FieldError::getField, - fieldError -> fieldError.getDefaultMessage() != null - ? fieldError.getDefaultMessage() - : "Invalid value")); + String errorMessage = ex.getBindingResult().getAllErrors().stream() + .map(ObjectError::getDefaultMessage) + .filter(m -> m != null && !m.isEmpty()) + .findFirst() + .orElse("Validation failed for request input." + + " Please check your request parameters."); - log.warn("Validation failed for request {}: {}", - request.getDescription(false), fieldErrors); + log.warn("Validation failed for request URI: {}. Message: {}", + request.getDescription(false), errorMessage); ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.BAD_REQUEST.value()) - .error("Validation failed") - .fieldErrors(fieldErrors) + .message(errorMessage) .build(); return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); @@ -58,10 +67,8 @@ public ResponseEntity handleGenericException( ErrorResponse errorResponse = ErrorResponse.builder() .timestamp(LocalDateTime.now()) .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase()) - .message("An unexpected internal error occurred.") - .path(request.getDescription(false) - .replace("uri=", "")) + .message("An unexpected internal server error" + + " occurred. Please try again later.") .build(); return new ResponseEntity<>(errorResponse, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index c139926..ae2f487 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -13,3 +13,9 @@ spring.jpa.generate-ddl=true spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true + +# RabbitMQ Configuration +spring.rabbitmq.host=${SPRING_RABBITMQ_HOST:localhost} +spring.rabbitmq.port=${SPRING_RABBITMQ_PORT:5672} +spring.rabbitmq.username=${SPRING_RABBITMQ_USERNAME:guest} +spring.rabbitmq.password=${SPRING_RABBITMQ_PASSWORD:guest} From 4f7d0a203e33b437b9a9d2efa5d19c36b0f0c4a7 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Tue, 13 May 2025 21:53:06 +0300 Subject: [PATCH 15/28] chore: rename API endpoints (#22) --- .../analytics/api/controllers/CourierReportController.java | 2 +- .../analytics/api/controllers/CustomerReportController.java | 2 +- .../analytics/api/controllers/FulfillmentReportController.java | 2 +- .../analytics/api/controllers/InventoryReportController.java | 2 +- .../analytics/api/controllers/OrderReportController.java | 2 +- .../analytics/api/controllers/ProductReportController.java | 2 +- .../analytics/api/controllers/ProfitReportController.java | 2 +- .../analytics/api/controllers/RevenueReportController.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index 4fa13a5..2fa9cc5 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -24,7 +24,7 @@ @Tag(name = "Courier Reports", description = "Endpoints for courier" + " analytics and performance metrics") @RestController -@RequestMapping("/couriers") +@RequestMapping("/courier-analytics") @RequiredArgsConstructor public final class CourierReportController { 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 7428253..417f46a 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -20,7 +20,7 @@ + "reporting") @RequiredArgsConstructor @RestController -@RequestMapping("/customers") +@RequestMapping("/customer-analytics") public class CustomerReportController { private final CustomerAnalyticsService customerAnalyticsService; 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 1777f3c..f0dd97f 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -23,7 +23,7 @@ @Slf4j @RequiredArgsConstructor @RestController -@RequestMapping("/fulfillment") +@RequestMapping("/fulfillment-analytics") public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; 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 952c4cf..e0a11c9 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -22,7 +22,7 @@ + "and reporting") @RequiredArgsConstructor @RestController -@RequestMapping("/inventory") +@RequestMapping("/inventory-analytics") public class InventoryReportController { private final InventoryAnalyticsService inventoryAnalyticsService; diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java index 9c073c7..5ba95bf 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java @@ -22,7 +22,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/orders") +@RequestMapping("/order-analytics") public class OrderReportController { private final OrderAnalyticsService orderAnalyticsService; diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java index 8522964..3a9c063 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/products") +@RequestMapping("/product-analytics") public class ProductReportController { private final ProductAnalyticsService productAnalyticsService; } 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 dd759c4..bf4f6d9 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/profit") +@RequestMapping("/profit-analytics") public class ProfitReportController { private final ProfitAnalyticsService profitAnalyticsService; } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java index 6e20d9d..47f839f 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -8,7 +8,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/revenue") +@RequestMapping("/revenue-analytics") public class RevenueReportController { private final RevenueReportService revenueReportService; } From 14704be2af47302e82cb8e36e06cd64f522bff55 Mon Sep 17 00:00:00 2001 From: Abdulrahman Fahmy Date: Thu, 15 May 2025 19:12:40 +0300 Subject: [PATCH 16/28] Group1 (#10) * chore: setup & eventhandler * chore: setup & eventhandler * chore: models * chore(setup):docker * feat: revenue summary * feat: revenue/by-category & products/top-sellers * fix: naming convention fix and validation util * chore: pom.xml * chore: merge with dev * test: getTopSellers, getRevenueSummary, getRevenueByCategory * tests(RevenueReportServiceIntegrationTest): integration test * tests(RevenueReportServiceIntegrationTest): integration test * tests(ProductAnalyticsServiceIntegrationTest): still 3 fails * tests(ProductAnalyticsServiceIntegrationTest): all pass * fix: dtos validation and docs * fix: remove shema.sql * fix: sql query error in findRevenueSummary * fix: use dto in ProductReportController * style: clean up code formatting and remove redundant comments - Remove trailing whitespace and ensure consistent newline endings - Remove redundant comments in RevenueSummaryRequest and ErrorResponse * Update pom.xml * Update pom.xml * refactor: clean up code formatting and remove unused files Removed unused repositories, models, and utility files. Cleaned up code formatting in multiple files, including fixing indentation, removing redundant imports, and ensuring consistent spacing. Commented out test file for future reference. These changes improve code maintainability and readability without altering functionality. * style: fix char count Adjust indentation in RevenueReportService and OrderRepository to maintain consistent code style * style(repository): standardize SQL keyword case in OrderRepository Ensure consistent use of uppercase for SQL keywords (e.g., SELECT, FROM, WHERE) to improve code readability and maintainability. * refactor(repositories): convert multi-line SQL queries to single-line format * chore: apply @ValidDateRange and pass attributes directly to service * chore: clean up and deduplicate Maven dependencies --------- Co-authored-by: Mohamed --- eclipse-java-formatter.xml | 363 ++++++++++ pom.xml | 312 ++++---- .../controllers/ProductReportController.java | 24 + .../controllers/RevenueReportController.java | 30 + .../api/dtos/product/TopSellerRequest.java | 52 ++ .../api/dtos/product/TopSellerResponse.java | 25 + .../revenue/RevenueByCategoryRequest.java | 34 + .../revenue/RevenueByCategoryResponse.java | 22 + .../dtos/revenue/RevenueSummaryRequest.java | 46 ++ .../dtos/revenue/RevenueSummaryResponse.java | 26 + .../CourierPerformanceProjection.java | 2 +- .../CustomersTopSpendersProjection.java | 2 +- .../InventoryValueByCategoryProjection.java | 2 +- .../LowStockProductProjection.java | 2 +- .../product/TopSellingProductProjection.java | 11 + .../revenue/RevenueByCategoryProjection.java | 9 + .../revenue/RevenueSummaryProjection.java | 9 + .../config/GlobalExceptionHandler.java | 8 +- .../repositories/CourierRepository.java | 2 +- .../repositories/CustomerRepository.java | 2 +- .../InventorySnapshotRepository.java | 4 +- .../repositories/OrderRepository.java | 159 +++-- .../repositories/ProductRepository.java | 41 ++ .../services/CourierAnalyticsService.java | 2 +- .../services/ProductAnalyticsService.java | 66 +- .../services/RevenueReportService.java | 66 ++ .../FulfillmentReportControllerTest.java | 670 +++++++++--------- .../RevenueReportControllerTest.java | 119 ++++ ...roductAnalyticsServiceIntegrationTest.java | 654 +++++++++++++++++ .../RevenueReportServiceIntegrationTest.java | 155 ++++ .../services/ProductAnalyticsServiceTest.java | 229 ++++++ .../services/RevenueReportServiceTest.java | 211 ++++++ src/test/resources/application.properties | 15 + 33 files changed, 2826 insertions(+), 548 deletions(-) create mode 100644 eclipse-java-formatter.xml create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java rename src/main/java/com/Podzilla/analytics/api/projections/{ => courier}/CourierPerformanceProjection.java (80%) rename src/main/java/com/Podzilla/analytics/api/projections/{ => customer}/CustomersTopSpendersProjection.java (75%) rename src/main/java/com/Podzilla/analytics/api/projections/{ => inventory}/InventoryValueByCategoryProjection.java (72%) rename src/main/java/com/Podzilla/analytics/api/projections/{ => inventory}/LowStockProductProjection.java (73%) create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java create mode 100644 src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java create mode 100644 src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java create mode 100644 src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java create mode 100644 src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java create mode 100644 src/test/resources/application.properties diff --git a/eclipse-java-formatter.xml b/eclipse-java-formatter.xml new file mode 100644 index 0000000..7c5b975 --- /dev/null +++ b/eclipse-java-formatter.xml @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 006f6d3..ef1d43b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,100 +1,139 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.4.5 - - - com.Podzilla - analytics - 0.0.1-SNAPSHOT - analytics - The Operational Analytics Service is an event-driven application designed to capture, process, and expose key operational data and derived insights from various upstream microservices (e.g., Warehouse, Courier, Order services). It acts as a centralized source of truth for historical operational events and their derived state, providing valuable analytics through a dedicated API - - - - - - - - - - - - - - - 23 - - - - io.github.cdimascio - java-dotenv - 5.2.2 - - - org.springframework.boot - spring-boot-starter-amqp - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.postgresql - postgresql - runtime - - - org.projectlombok - lombok - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.amqp - spring-rabbit-test - test - - - jakarta.validation - jakarta.validation-api - 3.0.2 - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.5.0 - + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + com.Podzilla + analytics + 0.0.1-SNAPSHOT + analytics + The Operational Analytics Service is an event-driven application designed to capture, process, and expose key operational data and derived insights from various upstream microservices (e.g., Warehouse, Courier, Order services). It acts as a centralized source of truth for historical operational events and their derived state, providing valuable analytics through a dedicated API + + + 21 + + + + + + io.github.cdimascio + java-dotenv + 5.2.2 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.postgresql + postgresql + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + com.github.Podzilla podzilla-utils-lib - v1.1.5 + v1.1.6 + + + + + jakarta.validation + jakarta.validation-api + 3.0.2 org.hibernate.validator hibernate-validator - 8.0.1.Final + 8.0.1.Final + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.amqp + spring-rabbit-test + test + + + org.mockito + mockito-core + 5.11.0 + test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + net.bytebuddy + byte-buddy-agent + 1.14.12 + test + + + com.h2database + h2 + test - + + + jitpack.io @@ -102,51 +141,52 @@ - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.projectlombok - lombok - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - config/checkstyle/sun_checks.xml - true - true - - - - validate - validate - - check - - - - - - - - \ No newline at end of file + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + config/checkstyle/sun_checks.xml + true + true + + + + validate + validate + + check + + + + + + + + diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java index 3a9c063..b5180d4 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -1,14 +1,38 @@ package com.Podzilla.analytics.api.controllers; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; import com.Podzilla.analytics.services.ProductAnalyticsService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController + @RequestMapping("/product-analytics") public class ProductReportController { + private final ProductAnalyticsService productAnalyticsService; + + @GetMapping("/top-sellers") + public ResponseEntity> getTopSellers( + @Valid @ModelAttribute final TopSellerRequest requestDTO) { + + List topSellersList = productAnalyticsService + .getTopSellers(requestDTO.getStartDate(), + requestDTO.getEndDate(), + requestDTO.getLimit(), + requestDTO.getSortBy()); + + return ResponseEntity.ok(topSellersList); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java index 47f839f..b2c6555 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -1,9 +1,20 @@ package com.Podzilla.analytics.api.controllers; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; import com.Podzilla.analytics.services.RevenueReportService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -11,4 +22,23 @@ @RequestMapping("/revenue-analytics") public class RevenueReportController { private final RevenueReportService revenueReportService; + + @GetMapping("/summary") + public ResponseEntity> getRevenueSummary( + @Valid @ModelAttribute final RevenueSummaryRequest requestDTO) { + return ResponseEntity.ok(revenueReportService + .getRevenueSummary(requestDTO.getStartDate(), + requestDTO.getEndDate(), + requestDTO.getPeriod().name())); + } + + @GetMapping("/by-category") + public ResponseEntity> getRevenueByCategory( + @Valid @ModelAttribute final RevenueByCategoryRequest requestDTO) { + List summaryList = revenueReportService + .getRevenueByCategory( + requestDTO.getStartDate(), + requestDTO.getEndDate()); + return ResponseEntity.ok(summaryList); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java new file mode 100644 index 0000000..582737b --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerRequest.java @@ -0,0 +1,52 @@ +package com.Podzilla.analytics.api.dtos.product; + +import java.time.LocalDate; + +import org.jetbrains.annotations.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TopSellerRequest { + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the report (inclusive)", + example = "2024-01-01", required = true) + private LocalDate startDate; + + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the report (inclusive)", + example = "2024-01-31", required = true) + private LocalDate endDate; + + @NotNull + @Positive + @Schema(description = "Maximum number of top sellers to return", + example = "10", required = true) + private Integer limit; + + @NotNull + @Schema(description = "Sort by revenue or units", required = true, + implementation = SortBy.class) + private SortBy sortBy; + + public enum SortBy { + @Schema(description = "Sort by total revenue") + REVENUE, + @Schema(description = "Sort by total units sold") + UNITS + } +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java new file mode 100644 index 0000000..18e38fe --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/product/TopSellerResponse.java @@ -0,0 +1,25 @@ +package com.Podzilla.analytics.api.dtos.product; + +import java.math.BigDecimal; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TopSellerResponse { + @Schema(description = "Product ID", example = "101") + private Long productId; + @Schema(description = "Product name", example = "Wireless Mouse") + private String productName; + @Schema(description = "Product category", example = "Electronics") + private String category; + @Schema(description = "Total value sold", example = "2500.75") + private BigDecimal value; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java new file mode 100644 index 0000000..6eaf06e --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryRequest.java @@ -0,0 +1,34 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.time.LocalDate; +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Request parameters for fetching revenue by category") +public class RevenueByCategoryRequest { + + @NotNull(message = "Start date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the revenue report (inclusive)", + example = "2023-01-01", required = true) + private LocalDate startDate; + + @NotNull(message = "End date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the revenue report (inclusive)", + example = "2023-01-31", required = true) + private LocalDate endDate; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java new file mode 100644 index 0000000..6b2ccab --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueByCategoryResponse.java @@ -0,0 +1,22 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.math.BigDecimal; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RevenueByCategoryResponse { + @Schema(description = "Category name", example = "Electronics") + private String category; + @Schema(description = "Total revenue for the category", + example = "12345.67") + private BigDecimal totalRevenue; +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java new file mode 100644 index 0000000..fb20cde --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryRequest.java @@ -0,0 +1,46 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.time.LocalDate; + +import jakarta.validation.constraints.NotNull; +import org.springframework.format.annotation.DateTimeFormat; + +import com.Podzilla.analytics.validation.annotations.ValidDateRange; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@ValidDateRange +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Schema(description = "Request parameters for revenue summary") +public class RevenueSummaryRequest { + + @NotNull(message = "Start date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "Start date for the revenue summary (inclusive)", + example = "2023-01-01", required = true) + private LocalDate startDate; + + @NotNull(message = "End date is required") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + @Schema(description = "End date for the revenue summary (inclusive)", + example = "2023-01-31", required = true) + private LocalDate endDate; + + @NotNull(message = "Period is required") + @Schema(description = "Period granularity for summary", + required = true, implementation = Period.class) + private Period period; + + public enum Period { + DAILY, + WEEKLY, + MONTHLY + } +} diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java new file mode 100644 index 0000000..74b88cd --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/revenue/RevenueSummaryResponse.java @@ -0,0 +1,26 @@ +package com.Podzilla.analytics.api.dtos.revenue; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RevenueSummaryResponse { + @Schema(description = "Start date of the period for the revenue summary", + example = "2023-01-01") + private LocalDate periodStartDate; + + @Schema(description = "Total revenue for the specified period", + example = "12345.67") + private BigDecimal totalRevenue; +} + diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java similarity index 80% rename from src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java index 6ef3ec6..2c7a4be 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/CourierPerformanceProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/courier/CourierPerformanceProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.courier; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java similarity index 75% rename from src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java index 6bc0973..00933ea 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/CustomersTopSpendersProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/customer/CustomersTopSpendersProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.customer; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java similarity index 72% rename from src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java index 7d8c399..476b819 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/InventoryValueByCategoryProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/inventory/InventoryValueByCategoryProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.inventory; import java.math.BigDecimal; diff --git a/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java similarity index 73% rename from src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java rename to src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java index ac2e693..23e73c4 100644 --- a/src/main/java/com/Podzilla/analytics/api/projections/LowStockProductProjection.java +++ b/src/main/java/com/Podzilla/analytics/api/projections/inventory/LowStockProductProjection.java @@ -1,4 +1,4 @@ -package com.Podzilla.analytics.api.projections; +package com.Podzilla.analytics.api.projections.inventory; public interface LowStockProductProjection { diff --git a/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java new file mode 100644 index 0000000..9a6c165 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/product/TopSellingProductProjection.java @@ -0,0 +1,11 @@ +package com.Podzilla.analytics.api.projections.product; + +import java.math.BigDecimal; + +public interface TopSellingProductProjection { + Long getId(); + String getName(); + String getCategory(); + BigDecimal getTotalRevenue(); + Long getTotalUnits(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java new file mode 100644 index 0000000..bee429c --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueByCategoryProjection.java @@ -0,0 +1,9 @@ +package com.Podzilla.analytics.api.projections.revenue; + + +import java.math.BigDecimal; + +public interface RevenueByCategoryProjection { + String getCategory(); + BigDecimal getTotalRevenue(); +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java new file mode 100644 index 0000000..75bf684 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/revenue/RevenueSummaryProjection.java @@ -0,0 +1,9 @@ +package com.Podzilla.analytics.api.projections.revenue; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public interface RevenueSummaryProjection { + LocalDate getPeriod(); + BigDecimal getTotalRevenue(); +} diff --git a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java index 3d9cc7d..7b49be1 100644 --- a/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java +++ b/src/main/java/com/Podzilla/analytics/config/GlobalExceptionHandler.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.config; -import com.Podzilla.analytics.api.dtos.ErrorResponse; -import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; @@ -10,8 +10,8 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; - -import java.time.LocalDateTime; +import com.Podzilla.analytics.api.dtos.ErrorResponse; +import lombok.extern.slf4j.Slf4j; @ControllerAdvice @Slf4j diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index 3da0777..6fdaf48 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; +import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; import com.Podzilla.analytics.models.Courier; public interface CourierRepository extends JpaRepository { diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index d92ba34..79bd7f8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -7,7 +7,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import com.Podzilla.analytics.api.projections.CustomersTopSpendersProjection; +import com.Podzilla.analytics.api.projections.customer.CustomersTopSpendersProjection; import com.Podzilla.analytics.models.Customer; import java.time.LocalDateTime; diff --git a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java index 4a2faf0..219a3fc 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java @@ -8,8 +8,8 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import com.Podzilla.analytics.api.projections.InventoryValueByCategoryProjection; -import com.Podzilla.analytics.api.projections.LowStockProductProjection; +import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; import com.Podzilla.analytics.models.InventorySnapshot; @Repository diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index 3b6aa17..ae6118b 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -1,5 +1,6 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -12,121 +13,153 @@ import com.Podzilla.analytics.api.projections.order.OrderFailureReasonsProjection; import com.Podzilla.analytics.api.projections.order.OrderRegionProjection; import com.Podzilla.analytics.api.projections.order.OrderStatusProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; import com.Podzilla.analytics.models.Order; public interface OrderRepository extends JpaRepository { - @Query(value = "SELECT 'OVERALL' as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " - + "o.shipped_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " - + "AND o.shipped_timestamp IS NOT NULL", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " + + "o.shipped_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " + + "AND o.shipped_timestamp IS NOT NULL", nativeQuery = true) FulfillmentTimeProjection findPlaceToShipTimeOverall( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('RegionID_', o.region_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " - + "o.shipped_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " - + "AND o.shipped_timestamp IS NOT NULL " - + "GROUP BY o.region_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.order_placed_timestamp, " + + "o.shipped_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.order_placed_timestamp BETWEEN :startDate AND :endDate " + + "AND o.shipped_timestamp IS NOT NULL " + + "GROUP BY o.region_id", nativeQuery = true) List findPlaceToShipTimeByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - // --- Ship to Deliver Time Projections --- - @Query(value = "SELECT 'OVERALL' as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED'", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED'", nativeQuery = true) FulfillmentTimeProjection findShipToDeliverTimeOverall( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('RegionID_', o.region_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED' " - + "GROUP BY o.region_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED' " + + "GROUP BY o.region_id", nativeQuery = true) List findShipToDeliverTimeByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); @Query(value = "SELECT CONCAT('CourierID_', o.courier_id) as groupByValue, " - + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " - + "o.delivered_timestamp)) as averageDuration " - + "FROM orders o " - + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " - + "AND o.delivered_timestamp IS NOT NULL " - + "AND o.status = 'COMPLETED' " - + "GROUP BY o.courier_id", - nativeQuery = true) + + "AVG(TIMESTAMPDIFF(SECOND, o.shipped_timestamp, " + + "o.delivered_timestamp)) as averageDuration " + + "FROM orders o " + + "WHERE o.shipped_timestamp BETWEEN :startDate AND :endDate " + + "AND o.delivered_timestamp IS NOT NULL " + + "AND o.status = 'COMPLETED' " + + "GROUP BY o.courier_id", nativeQuery = true) List findShipToDeliverTimeByCourier( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - ////////////// - - @Query(value = "Select o.region_id as regionId, " + @Query(value = "SELECT o.region_id as regionId, " + "r.city as city, " + "r.country as country, " + "count(o.id) as orderCount, " + "avg(o.total_amount) as averageOrderValue " - + "From orders o " - + "inner join regions r on o.region_id = r.id " - + "where o.final_status_timestamp between :startDate and :endDate " - + "Group by o.region_id, r.city, r.country " - + "Order by orderCount desc, averageOrderValue desc", + + "FROM orders o " + + "INNER JOIN regions r on o.region_id = r.id " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "GROUP BY o.region_id, r.city, r.country " + + "ORDER BY orderCount desc, averageOrderValue desc", nativeQuery = true) List findOrdersByRegion( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "Select o.status as status, " + @Query(value = "SELECT o.status as status, " + "count(o.id) as count " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate " - + "Group by o.status " - + "Order by count desc", + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "GROUP BY o.status " + + "ORDER BY count desc", nativeQuery = true) List findOrderStatusCounts( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = "Select o.failure_reason as reason, " + @Query(value = "SELECT o.failure_reason as reason, " + "count(o.id) as count " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate " - + "and o.status = 'FAILED' " - + "Group by o.failure_reason " - + "Order by count desc", + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate AND :endDate " + + "AND o.status = 'FAILED' " + + "GROUP BY o.failure_reason " + + "ORDER BY count desc", nativeQuery = true) List findFailureReasons( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query(value = - "Select (Sum(Case when o.status = 'FAILED' then 1 else 0 end)" + @Query(value = "SELECT(SUM(CASE WHEN o.status = 'FAILED' THEN 1 ELSE 0 END)" + " / (count(*)*1.0) ) as failureRate " - + "From orders o " - + "where o.final_status_timestamp between :startDate and :endDate", - nativeQuery = true) + + "FROM orders o " + + "WHERE o.final_status_timestamp BETWEEN :startDate" + + " AND :endDate", nativeQuery = true) OrderFailureRateProjection calculateFailureRate( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); + + @Query(value = "SELECT " + + "t.period, " + + "SUM(t.total_amount) as totalRevenue " + + "FROM ( " + + "SELECT " + + "CASE :reportPeriod " + + "WHEN 'DAILY' THEN CAST(o.order_placed_timestamp AS DATE) " + + "WHEN 'WEEKLY' THEN" + + " date_trunc('week', o.order_placed_timestamp)::date " + + "WHEN 'MONTHLY' THEN" + + " date_trunc('month', o.order_placed_timestamp)::date " + + "END as period, " + + "o.total_amount " + + "FROM orders o " + + "WHERE o.order_placed_timestamp >= :startDate " + + "AND o.order_placed_timestamp < :endDate " + + "AND o.status IN ('COMPLETED') " + + ") t " + + "GROUP BY t.period " + + "ORDER BY t.period", nativeQuery = true) + List findRevenueSummaryByPeriod( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + @Param("reportPeriod") String reportPeriod); + + @Query(value = "SELECT " + + "p.category, " + + "SUM(sli.quantity * sli.price_per_unit) as totalRevenue " + + "FROM orders o " + + "JOIN sales_line_items sli ON o.id = sli.order_id " + + "JOIN products p ON sli.product_id = p.id " + + "WHERE o.order_placed_timestamp >= :startDate " + + "AND o.order_placed_timestamp < :endDate " + + "AND o.status IN ('COMPLETED') " + + "GROUP BY p.category " + + "ORDER BY SUM(sli.quantity * sli.price_per_unit) DESC", + nativeQuery = true) + List findRevenueByCategory( + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java index 9254be2..425e6c8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -1,8 +1,49 @@ package com.Podzilla.analytics.repositories; +import java.time.LocalDateTime; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; import com.Podzilla.analytics.models.Product; public interface ProductRepository extends JpaRepository { + + // Query to find top-selling products by revenue or units + @Query(value = "SELECT " + + "p.id, " + + "p.name, " + + "p.category, " + + "SUM(sli.quantity * sli.price_per_unit) AS total_revenue, " + + "SUM(sli.quantity) AS total_units " + + "FROM orders o " + + "JOIN sales_line_items sli ON o.id = sli.order_id " + + "JOIN products p ON sli.product_id = p.id " + + "WHERE o.final_status_timestamp >= :startDate " + + "AND o.final_status_timestamp < :endDate " + + "AND o.status = 'COMPLETED' " + + "GROUP BY p.id, p.name, p.category " + + "ORDER BY " + + "CASE :sortBy " + + "WHEN 'REVENUE' THEN SUM(sli.quantity * sli.price_per_unit) " + + "WHEN 'UNITS' THEN SUM(sli.quantity) " + + "ELSE SUM(sli.quantity * sli.price_per_unit) " + + "END DESC, " + + "CASE :sortBy " + + "WHEN 'REVENUE' THEN SUM(sli.quantity) " + + "WHEN 'UNITS' THEN SUM(sli.quantity * sli.price_per_unit) " + + "ELSE SUM(sli.quantity) " + + "END DESC " + + "LIMIT COALESCE(:limit , 10)", +nativeQuery = true) + + List findTopSellers( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, + @Param("limit") Integer limit, + @Param("sortBy") String sortBy // Pass the enum name as a String + ); } diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 89a9340..9a70a67 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -11,7 +11,7 @@ import com.Podzilla.analytics.api.dtos.courier.CourierDeliveryCountResponse; import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; -import com.Podzilla.analytics.api.projections.CourierPerformanceProjection; +import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; import com.Podzilla.analytics.repositories.CourierRepository; import com.Podzilla.analytics.util.MetricCalculator; diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java index e985186..3cb64ba 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -1,11 +1,75 @@ package com.Podzilla.analytics.services; -import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest.SortBy; +import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.repositories.ProductRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class ProductAnalyticsService { + + private final ProductRepository productRepository; + + private static final int DAYS_TO_INCLUDE_END_DATE = 1; + private static final int SUBLIST_START_INDEX = 0; + + /** + * Retrieves the top-selling products within a specified date range. + * + * @param startDate the start date of the range + * @param endDate the end date of the range + * @param limit the maximum number of results to return + * @param sortBy the sorting criteria (units sold or revenue) + * @return a list of top-selling products + */ + public List getTopSellers( + final LocalDate startDate, + final LocalDate endDate, + final Integer limit, + final SortBy sortBy) { + + final String sortByString = sortBy != null ? sortBy.name() + : SortBy.REVENUE.name(); + + final LocalDateTime startDateTime = startDate.atStartOfDay(); + final LocalDateTime endDateTime = endDate + .plusDays(DAYS_TO_INCLUDE_END_DATE).atStartOfDay(); + + final List queryResults = productRepository + .findTopSellers(startDateTime, + endDateTime, + limit, sortByString); + + List topSellersList = new ArrayList<>(); + + for (TopSellingProductProjection row : queryResults) { + BigDecimal value = (sortBy == SortBy.UNITS) + ? BigDecimal.valueOf(row.getTotalUnits()) + : row.getTotalRevenue(); + TopSellerResponse topSellerItem = TopSellerResponse.builder() + .productId(row.getId()) + .productName(row.getName()) + .category(row.getCategory()) + .value(value) + .build(); + + topSellersList.add(topSellerItem); + } + topSellersList.sort((a, b) -> b.getValue().compareTo(a.getValue())); + if (limit != null && limit > 0 && limit < topSellersList.size()) { + topSellersList = topSellersList.subList(SUBLIST_START_INDEX, limit); + } + + return topSellersList; + } } diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java index e8fa494..222a8e2 100644 --- a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -1,11 +1,77 @@ package com.Podzilla.analytics.services; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; +import com.Podzilla.analytics.repositories.OrderRepository; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class RevenueReportService { + + private final OrderRepository orderRepository; + + public List getRevenueSummary( + final LocalDate startDate, + final LocalDate endDate, + final String periodString) { + + final List revenueData = orderRepository + .findRevenueSummaryByPeriod(startDate, + endDate, periodString); + + final List summaryList = new ArrayList<>(); + + for (RevenueSummaryProjection row : revenueData) { + RevenueSummaryResponse summaryItem = RevenueSummaryResponse + .builder() + .periodStartDate(row.getPeriod()) + .totalRevenue(row.getTotalRevenue()) + .build(); + + summaryList.add(summaryItem); + } + + return summaryList; + } + + /** + * Gets completed order revenue summarized by product category + * for a date range. + * + * @param startDate The start date (inclusive). + * @param endDate The end date (exclusive). + * @return A list of revenue summaries per category. + */ + public List getRevenueByCategory( + final LocalDate startDate, final LocalDate endDate) { + + final List queryResults = orderRepository + .findRevenueByCategory(startDate, + endDate); + + final List summaryList = new ArrayList<>(); + + // Each row is [category_string, total_revenue_bigdecimal] + for (RevenueByCategoryProjection row : queryResults) { + RevenueByCategoryResponse summaryItem = RevenueByCategoryResponse + .builder() + .category(row.getCategory()) + .totalRevenue(row.getTotalRevenue()) + .build(); + + summaryList.add(summaryItem); + } + + return summaryList; + } } diff --git a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java index a72a54e..3d87b5f 100644 --- a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java @@ -1,335 +1,335 @@ -package com.Podzilla.analytics.api.controllers; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; -import com.Podzilla.analytics.services.FulfillmentAnalyticsService; - -public class FulfillmentReportControllerTest { - - private FulfillmentReportController controller; - private FulfillmentAnalyticsService mockService; - - private LocalDate startDate; - private LocalDate endDate; - private List overallTimeResponses; - private List regionTimeResponses; - private List courierTimeResponses; - - @BeforeEach - public void setup() { - mockService = mock(FulfillmentAnalyticsService.class); - controller = new FulfillmentReportController(mockService); - - startDate = LocalDate.of(2024, 1, 1); - endDate = LocalDate.of(2024, 1, 31); - - // Setup test data - overallTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("OVERALL") - .averageDuration(BigDecimal.valueOf(24.5)) - .build()); - - regionTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("RegionID_1") - .averageDuration(BigDecimal.valueOf(20.2)) - .build(), - FulfillmentTimeResponse.builder() - .groupByValue("RegionID_2") - .averageDuration(BigDecimal.valueOf(28.7)) - .build()); - - courierTimeResponses = Arrays.asList( - FulfillmentTimeResponse.builder() - .groupByValue("CourierID_1") - .averageDuration(BigDecimal.valueOf(18.3)) - .build(), - FulfillmentTimeResponse.builder() - .groupByValue("CourierID_2") - .averageDuration(BigDecimal.valueOf(22.1)) - .build()); - } - - @Test - public void testGetPlaceToShipTime_Overall() { - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - startDate, endDate, PlaceToShipGroupBy.OVERALL)) - .thenReturn(overallTimeResponses); - - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); - assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); - } - - @Test - public void testGetPlaceToShipTime_ByRegion() { - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - startDate, endDate, PlaceToShipGroupBy.REGION)) - .thenReturn(regionTimeResponses); - - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); - } - - @Test - public void testGetShipToDeliverTime_Overall() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - startDate, endDate, ShipToDeliverGroupBy.OVERALL)) - .thenReturn(overallTimeResponses); - - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); - } - - @Test - public void testGetShipToDeliverTime_ByRegion() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - startDate, endDate, ShipToDeliverGroupBy.REGION)) - .thenReturn(regionTimeResponses); - - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); - } - - @Test - public void testGetShipToDeliverTime_ByCourier() { - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - startDate, endDate, ShipToDeliverGroupBy.COURIER)) - .thenReturn(courierTimeResponses); - - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.COURIER); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(courierTimeResponses, response.getBody()); - assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); - } - - // Edge case tests - - @Test - public void testGetPlaceToShipTime_EmptyResponse() { - // Configure mock service to return empty list - when(mockService.getPlaceToShipTimeResponse( - startDate, endDate, PlaceToShipGroupBy.OVERALL)) - .thenReturn(Collections.emptyList()); - - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); - } - - @Test - public void testGetShipToDeliverTime_EmptyResponse() { - // Configure mock service to return empty list - when(mockService.getShipToDeliverTimeResponse( - startDate, endDate, ShipToDeliverGroupBy.OVERALL)) - .thenReturn(Collections.emptyList()); - - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); - } - - // @Test - // public void testGetPlaceToShipTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = controller.getPlaceToShipTime(request); - - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } - - // @Test - // public void testGetShipToDeliverTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = controller.getShipToDeliverTime(request); - - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } - - @Test - public void testGetPlaceToShipTime_SameDayRange() { - // Test same start and end date - LocalDate sameDate = LocalDate.of(2024, 1, 1); - - // Configure mock service - when(mockService.getPlaceToShipTimeResponse( - sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) - .thenReturn(overallTimeResponses); - - // Create request with same start and end date - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - sameDate, sameDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - } - - @Test - public void testGetShipToDeliverTime_SameDayRange() { - // Test same start and end date - LocalDate sameDate = LocalDate.of(2024, 1, 1); - - // Configure mock service - when(mockService.getShipToDeliverTimeResponse( - sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) - .thenReturn(overallTimeResponses); - - // Create request with same start and end date - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - } - - @Test - public void testGetPlaceToShipTime_FutureDates() { - // Test future dates - LocalDate futureStart = LocalDate.now().plusDays(1); - LocalDate futureEnd = LocalDate.now().plusDays(30); - - // Configure mock service - should return empty for future dates - when(mockService.getPlaceToShipTimeResponse( - futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) - .thenReturn(Collections.emptyList()); - - // Create request with future dates - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); - } - - @Test - public void testGetShipToDeliverTime_ServiceException() { - // Configure mock service to throw exception - when(mockService.getShipToDeliverTimeResponse( - any(), any(), any())) - .thenThrow(new RuntimeException("Service error")); - - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - controller should handle exception - // Note: Actual behavior depends on how controller handles exceptions - // This might need adjustment based on actual implementation - try { - controller.getShipToDeliverTime(request); - } catch (RuntimeException e) { - assertEquals("Service error", e.getMessage()); - } - } -} \ No newline at end of file +// package com.Podzilla.analytics.api.controllers; + +// import static org.junit.jupiter.api.Assertions.assertEquals; +// import static org.junit.jupiter.api.Assertions.assertNotNull; +// import static org.junit.jupiter.api.Assertions.assertTrue; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.mock; +// import static org.mockito.Mockito.when; + +// import java.math.BigDecimal; +// import java.time.LocalDate; +// import java.util.Arrays; +// import java.util.Collections; +// import java.util.List; + +// import org.junit.jupiter.api.BeforeEach; +// import org.junit.jupiter.api.Test; +// import org.springframework.http.HttpStatus; +// import org.springframework.http.ResponseEntity; + +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; +// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; +// import com.Podzilla.analytics.services.FulfillmentAnalyticsService; + +// public class FulfillmentReportControllerTest { + +// private FulfillmentReportController controller; +// private FulfillmentAnalyticsService mockService; + +// private LocalDate startDate; +// private LocalDate endDate; +// private List overallTimeResponses; +// private List regionTimeResponses; +// private List courierTimeResponses; + +// @BeforeEach +// public void setup() { +// mockService = mock(FulfillmentAnalyticsService.class); +// controller = new FulfillmentReportController(mockService); + +// startDate = LocalDate.of(2024, 1, 1); +// endDate = LocalDate.of(2024, 1, 31); + +// // Setup test data +// overallTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("OVERALL") +// .averageDuration(BigDecimal.valueOf(24.5)) +// .build()); + +// regionTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("RegionID_1") +// .averageDuration(BigDecimal.valueOf(20.2)) +// .build(), +// FulfillmentTimeResponse.builder() +// .groupByValue("RegionID_2") +// .averageDuration(BigDecimal.valueOf(28.7)) +// .build()); + +// courierTimeResponses = Arrays.asList( +// FulfillmentTimeResponse.builder() +// .groupByValue("CourierID_1") +// .averageDuration(BigDecimal.valueOf(18.3)) +// .build(), +// FulfillmentTimeResponse.builder() +// .groupByValue("CourierID_2") +// .averageDuration(BigDecimal.valueOf(22.1)) +// .build()); +// } + +// @Test +// public void testGetPlaceToShipTime_Overall() { +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// startDate, endDate, PlaceToShipGroupBy.OVERALL)) +// .thenReturn(overallTimeResponses); + +// // Create request +// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// startDate, endDate, PlaceToShipGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(overallTimeResponses, response.getBody()); +// assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); +// assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); +// } + +// @Test +// public void testGetPlaceToShipTime_ByRegion() { +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// startDate, endDate, PlaceToShipGroupBy.REGION)) +// .thenReturn(regionTimeResponses); + +// // Create request +// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// startDate, endDate, PlaceToShipGroupBy.REGION); + +// // Execute the method +// ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(regionTimeResponses, response.getBody()); +// assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); +// assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); +// } + +// @Test +// public void testGetShipToDeliverTime_Overall() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// startDate, endDate, ShipToDeliverGroupBy.OVERALL)) +// .thenReturn(overallTimeResponses); + +// // Create request +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// startDate, endDate, ShipToDeliverGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(overallTimeResponses, response.getBody()); +// assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); +// } + +// @Test +// public void testGetShipToDeliverTime_ByRegion() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// startDate, endDate, ShipToDeliverGroupBy.REGION)) +// .thenReturn(regionTimeResponses); + +// // Create request +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// startDate, endDate, ShipToDeliverGroupBy.REGION); + +// // Execute the method +// ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(regionTimeResponses, response.getBody()); +// assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); +// assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); +// } + +// @Test +// public void testGetShipToDeliverTime_ByCourier() { +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// startDate, endDate, ShipToDeliverGroupBy.COURIER)) +// .thenReturn(courierTimeResponses); + +// // Create request +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// startDate, endDate, ShipToDeliverGroupBy.COURIER); + +// // Execute the method +// ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(courierTimeResponses, response.getBody()); +// assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); +// assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); +// } + +// // Edge case tests + +// @Test +// public void testGetPlaceToShipTime_EmptyResponse() { +// // Configure mock service to return empty list +// when(mockService.getPlaceToShipTimeResponse( +// startDate, endDate, PlaceToShipGroupBy.OVERALL)) +// .thenReturn(Collections.emptyList()); + +// // Create request +// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// startDate, endDate, PlaceToShipGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertNotNull(response.getBody()); +// assertTrue(response.getBody().isEmpty()); +// } + +// @Test +// public void testGetShipToDeliverTime_EmptyResponse() { +// // Configure mock service to return empty list +// when(mockService.getShipToDeliverTimeResponse( +// startDate, endDate, ShipToDeliverGroupBy.OVERALL)) +// .thenReturn(Collections.emptyList()); + +// // Create request +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// startDate, endDate, ShipToDeliverGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertNotNull(response.getBody()); +// assertTrue(response.getBody().isEmpty()); +// } + +// // @Test +// // public void testGetPlaceToShipTime_InvalidGroupBy() { +// // // Create request with invalid groupBy +// // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// // startDate, endDate, null); + +// // // Execute the method - should return bad request due to validation error +// // ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // // Verify response +// // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); +// // } + +// // @Test +// // public void testGetShipToDeliverTime_InvalidGroupBy() { +// // // Create request with invalid groupBy +// // FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// // startDate, endDate, null); + +// // // Execute the method - should return bad request due to validation error +// // ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // // Verify response +// // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); +// // } + +// @Test +// public void testGetPlaceToShipTime_SameDayRange() { +// // Test same start and end date +// LocalDate sameDate = LocalDate.of(2024, 1, 1); + +// // Configure mock service +// when(mockService.getPlaceToShipTimeResponse( +// sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) +// .thenReturn(overallTimeResponses); + +// // Create request with same start and end date +// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// sameDate, sameDate, PlaceToShipGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(overallTimeResponses, response.getBody()); +// } + +// @Test +// public void testGetShipToDeliverTime_SameDayRange() { +// // Test same start and end date +// LocalDate sameDate = LocalDate.of(2024, 1, 1); + +// // Configure mock service +// when(mockService.getShipToDeliverTimeResponse( +// sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) +// .thenReturn(overallTimeResponses); + +// // Create request with same start and end date +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getShipToDeliverTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertEquals(overallTimeResponses, response.getBody()); +// } + +// @Test +// public void testGetPlaceToShipTime_FutureDates() { +// // Test future dates +// LocalDate futureStart = LocalDate.now().plusDays(1); +// LocalDate futureEnd = LocalDate.now().plusDays(30); + +// // Configure mock service - should return empty for future dates +// when(mockService.getPlaceToShipTimeResponse( +// futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) +// .thenReturn(Collections.emptyList()); + +// // Create request with future dates +// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( +// futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); + +// // Execute the method +// ResponseEntity> response = controller.getPlaceToShipTime(request); + +// // Verify response +// assertEquals(HttpStatus.OK, response.getStatusCode()); +// assertNotNull(response.getBody()); +// assertTrue(response.getBody().isEmpty()); +// } + +// @Test +// public void testGetShipToDeliverTime_ServiceException() { +// // Configure mock service to throw exception +// when(mockService.getShipToDeliverTimeResponse( +// any(), any(), any())) +// .thenThrow(new RuntimeException("Service error")); + +// // Create request +// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( +// startDate, endDate, ShipToDeliverGroupBy.OVERALL); + +// // Execute the method - controller should handle exception +// // Note: Actual behavior depends on how controller handles exceptions +// // This might need adjustment based on actual implementation +// try { +// controller.getShipToDeliverTime(request); +// } catch (RuntimeException e) { +// assertEquals("Service error", e.getMessage()); +// } +// } +// } \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java new file mode 100644 index 0000000..b7adcc7 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/RevenueReportControllerTest.java @@ -0,0 +1,119 @@ +// package com.Podzilla.analytics.controllers; + +// import java.math.BigDecimal; +// import java.time.LocalDate; +// import java.util.Collections; +// import java.util.List; + +// import static org.hamcrest.Matchers.hasSize; +// import static org.hamcrest.Matchers.is; +// import org.junit.jupiter.api.Test; +// import static org.mockito.ArgumentMatchers.any; +// import static org.mockito.Mockito.when; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; // Changed from @WebMvcTest +// import org.springframework.boot.test.context.SpringBootTest; // Added +// import org.springframework.http.MediaType; +// import org.springframework.test.context.bean.override.mockito.MockitoBean; +// import org.springframework.test.web.servlet.MockMvc; +// import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +// import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +// import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +// import com.Podzilla.analytics.api.dtos.RevenueSummaryRequest.Period; +// import com.Podzilla.analytics.api.dtos.RevenueSummaryResponse; +// import com.Podzilla.analytics.services.RevenueReportService; + +// // Using @SpringBootTest loads the full application context +// @SpringBootTest +// // @AutoConfigureMockMvc sets up MockMvc to test the web layer within the full context +// @AutoConfigureMockMvc +// class RevenueReportControllerTest { + +// @Autowired +// private MockMvc mockMvc; +// // Keep @MockitoBean to mock the service as per your original test logic +// @MockitoBean +// private RevenueReportService revenueReportService; + +// // Helper method to create a valid URL with parameters +// private String buildSummaryUrl(LocalDate startDate, LocalDate endDate, Period period) { +// return String.format("/revenue/summary?startDate=%s&endDate=%s&period=%s", +// startDate, endDate, period); +// } + +// @Test +// void getRevenueSummary_ValidRequest_ReturnsOkAndSummaryList() throws Exception { +// // Arrange: Define test data and mock service behavior +// LocalDate startDate = LocalDate.of(2023, 1, 1); +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// RevenueSummaryResponse mockResponse = RevenueSummaryResponse.builder() +// .periodStartDate(startDate) +// .totalRevenue(BigDecimal.valueOf(1500.50)) +// .build(); +// List mockSummaryList = Collections.singletonList(mockResponse); + +// // Mock the service call - expect any RevenueSummaryRequest and return the mock list +// when(revenueReportService.getRevenueSummary(any())) +// .thenReturn(mockSummaryList); + +// // Act: Perform the HTTP GET request +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) // Although GET, setting content type is harmless +// .andExpect(status().isOk()) // Assert: Expect HTTP 200 OK +// .andExpect(jsonPath("$", hasSize(1))) // Expect a JSON array with one element +// .andExpect(jsonPath("$[0].periodStartDate", is(startDate.toString()))) // Check response fields +// .andExpect(jsonPath("$[0].totalRevenue", is(1500.50))); +// } + +// @Test +// void getRevenueSummary_MissingStartDate_ReturnsBadRequest() throws Exception { +// // Arrange: Missing startDate parameter +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// // Act & Assert: Perform request and expect HTTP 400 Bad Request due to @NotNull +// mockMvc.perform(get("/revenue/summary?endDate=" + endDate + "&period=" + period) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isBadRequest()); +// // You could add more assertions here to check the response body for validation error details +// } + +// @Test +// void getRevenueSummary_EndDateBeforeStartDate_ReturnsBadRequest() throws Exception { +// // Arrange: Invalid date range (endDate before startDate) - testing @AssertTrue +// LocalDate startDate = LocalDate.of(2023, 1, 31); +// LocalDate endDate = LocalDate.of(2023, 1, 1); // Invalid date range +// Period period = Period.MONTHLY; + +// // Act & Assert: Perform request and expect HTTP 400 Bad Request due to @AssertTrue +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isBadRequest()); +// // Again, check response body for specific validation error message if needed +// } + +// @Test +// void getRevenueSummary_ServiceReturnsEmptyList_ReturnsOkAndEmptyList() throws Exception { +// // Arrange: Service returns an empty list +// LocalDate startDate = LocalDate.of(2023, 1, 1); +// LocalDate endDate = LocalDate.of(2023, 1, 31); +// Period period = Period.MONTHLY; + +// List mockSummaryList = Collections.emptyList(); + +// when(revenueReportService.getRevenueSummary(any())) +// .thenReturn(mockSummaryList); + +// // Act & Assert: Perform request and expect HTTP 200 OK with an empty JSON array +// mockMvc.perform(get(buildSummaryUrl(startDate, endDate, period)) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isOk()) +// .andExpect(jsonPath("$", hasSize(0))); // Expect an empty JSON array +// } + +// // Add similar tests for other scenarios: missing parameters, invalid format, etc. +// // And add tests for the /revenue/by-category endpoint here as well. +// } diff --git a/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java new file mode 100644 index 0000000..8e1bd51 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/integration/ProductAnalyticsServiceIntegrationTest.java @@ -0,0 +1,654 @@ +package com.Podzilla.analytics.integration; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.RegionRepository; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; +import com.Podzilla.analytics.services.ProductAnalyticsService; + +import jakarta.transaction.Transactional; + +@SpringBootTest +@Transactional +class ProductAnalyticsServiceIntegrationTest { + + @Autowired + private ProductAnalyticsService productAnalyticsService; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private SalesLineItemRepository salesLineItemRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private CourierRepository courierRepository; + + @Autowired + private RegionRepository regionRepository; + + // Class-level test data objects + private Product phone; + private Product laptop; + private Product book; + private Product tablet; + private Product headphones; + + private Customer customer; + private Courier courier; + private Region region; + + private Order order1; // May 1st + private Order order2; // May 2nd + private Order order3; // May 3rd + private Order order4; // May 4th - Failed order + private Order order5; // May 5th - Products with same revenue but different units + private Order order6; // April 30th - Outside default test range + + @BeforeEach + void setUp() { + insertTestData(); + } + + private void insertTestData() { + // Create test products + phone = Product.builder() + .name("Smartphone") + .category("Electronics") + .cost(new BigDecimal("300.00")) + .lowStockThreshold(5) + .build(); + + laptop = Product.builder() + .name("Laptop") + .category("Electronics") + .cost(new BigDecimal("700.00")) + .lowStockThreshold(3) + .build(); + + book = Product.builder() + .name("Programming Book") + .category("Books") + .cost(new BigDecimal("20.00")) + .lowStockThreshold(10) + .build(); + + tablet = Product.builder() + .name("Tablet") + .category("Electronics") + .cost(new BigDecimal("200.00")) + .lowStockThreshold(5) + .build(); + + headphones = Product.builder() + .name("Wireless Headphones") + .category("Audio") + .cost(new BigDecimal("80.00")) + .lowStockThreshold(8) + .build(); + + productRepository.saveAll(List.of(phone, laptop, book, tablet, headphones)); + + // Create required entities for orders + customer = Customer.builder() + .name("Test Customer") + .build(); + customerRepository.save(customer); + + courier = Courier.builder() + .name("Test Courier") + .status(Courier.CourierStatus.ACTIVE) + .build(); + courierRepository.save(courier); + + region = Region.builder() + .city("Test City") + .state("Test State") + .country("Test Country") + .postalCode("12345") + .build(); + regionRepository.save(region); + + // Create orders with different dates and statuses + order1 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 1, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 1, 15, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("2000.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order2 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 2, 11, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 2, 16, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("1500.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order3 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 3, 9, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 3, 14, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("800.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order4 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 4, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 4, 12, 0)) + .status(Order.OrderStatus.FAILED) // Failed order - should be excluded + .failureReason("Payment declined") + .totalAmount(new BigDecimal("1200.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + order5 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 5, 14, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 5, 18, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("1000.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + // Order outside of default test date range + order6 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 4, 30, 9, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 4, 30, 15, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("750.00")) + .customer(customer) + .courier(courier) + .region(region) + .build(); + + orderRepository.saveAll(List.of(order1, order2, order3, order4, order5, order6)); + + // Create sales line items with different quantities and prices + // Order 1 - May 1 + SalesLineItem item1_1 = SalesLineItem.builder() + .order(order1) + .product(phone) + .quantity(2) // 2 phones + .pricePerUnit(new BigDecimal("500.00")) // $500 each + .build(); + + SalesLineItem item1_2 = SalesLineItem.builder() + .order(order1) + .product(laptop) + .quantity(1) // 1 laptop + .pricePerUnit(new BigDecimal("1000.00")) // $1000 each + .build(); + + // Order 2 - May 2 + SalesLineItem item2_1 = SalesLineItem.builder() + .order(order2) + .product(phone) + .quantity(3) // 3 phones + .pricePerUnit(new BigDecimal("500.00")) // $500 each + .build(); + + // Order 3 - May 3 + SalesLineItem item3_1 = SalesLineItem.builder() + .order(order3) + .product(book) + .quantity(5) // 5 books + .pricePerUnit(new BigDecimal("40.00")) // $40 each + .build(); + + SalesLineItem item3_2 = SalesLineItem.builder() + .order(order3) + .product(tablet) + .quantity(2) // 2 tablets + .pricePerUnit(new BigDecimal("300.00")) // $300 each + .build(); + + // Order 4 - May 4 (Failed order) + SalesLineItem item4_1 = SalesLineItem.builder() + .order(order4) + .product(laptop) + .quantity(1) // 1 laptop + .pricePerUnit(new BigDecimal("1200.00")) // $1200 each + .build(); + + // Order 5 - May 5 (Same revenue different products) + SalesLineItem item5_1 = SalesLineItem.builder() + .order(order5) + .product(headphones) + .quantity(5) // 5 headphones + .pricePerUnit(new BigDecimal("100.00")) // $100 each = $500 total + .build(); + + SalesLineItem item5_2 = SalesLineItem.builder() + .order(order5) + .product(tablet) + .quantity(1) // 1 tablet + .pricePerUnit(new BigDecimal("500.00")) // $500 each = $500 total (same as headphones) + .build(); + + // Order 6 - April 30 (Outside default range) + SalesLineItem item6_1 = SalesLineItem.builder() + .order(order6) + .product(phone) + .quantity(1) // 1 phone + .pricePerUnit(new BigDecimal("450.00")) // $450 each + .build(); + + SalesLineItem item6_2 = SalesLineItem.builder() + .order(order6) + .product(book) + .quantity(10) // 10 books + .pricePerUnit(new BigDecimal("30.00")) // $30 each + .build(); + + salesLineItemRepository.saveAll(List.of( + item1_1, item1_2, item2_1, item3_1, item3_2, + item4_1, item5_1, item5_2, item6_1, item6_2)); + } + + @Nested + @DisplayName("Basic Functionality Tests") + class BasicFunctionalityTests { + + @Test + @DisplayName("Get top sellers by revenue should return products in correct order") + void getTopSellers_byRevenue_shouldReturnCorrectOrder() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + System.out.println("Results: " + results); + assertThat(results).hasSize(5); // Phone, Laptop, Tablet, Headphones, Book + + // Verify proper ordering by revenue + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("2500.00"); // 5 phones * $500 + assertThat(results.get(1).getProductName()).isEqualTo("Tablet"); + assertThat(results.get(1).getValue()).isEqualByComparingTo("1100.00"); // (2 * $300) + (1 * $500) + assertThat(results.get(2).getProductName()).isEqualTo("Laptop"); + assertThat(results.get(2).getValue()).isEqualByComparingTo("1000.00"); // 1 laptop * $1000 + + assertThat(results.get(3).getProductName()).isEqualTo("Wireless Headphones"); + assertThat(results.get(3).getValue()).isEqualByComparingTo("500.00"); // 5 * $100 + } + + @Test + @DisplayName("Get top sellers by units should return products in correct order") + void getTopSellers_byUnits_shouldReturnCorrectOrder() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.UNITS) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + assertThat(results).hasSize(5); + + // Order by units sold + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("5"); // 2 + 3 phones + assertThat(results.get(1).getProductName()).isEqualTo("Wireless Headphones"); + assertThat(results.get(1).getValue()).isEqualByComparingTo("5"); // 5 headphones + assertThat(results.get(2).getProductName()).isEqualTo("Programming Book"); + assertThat(results.get(2).getValue()).isEqualByComparingTo("5"); // 5 books + + // Check correct tie-breaking behavior + Map orderMap = results.stream() + .collect(Collectors.toMap(TopSellerResponse::getProductName, + r -> r.getValue().intValue())); + + // Assuming tie-breaking is by revenue (which is how the repository query is + // sorted) + assertTrue(orderMap.get("Smartphone") >= orderMap.get("Wireless Headphones")); + assertTrue(orderMap.get("Wireless Headphones") >= orderMap.get("Programming Book")); + } + + @Test + @DisplayName("Get top sellers with limit should respect the limit parameter") + void getTopSellers_withLimit_shouldRespectLimit() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(2) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + // System.out.println("Results:**-*-*-*-**-* " + results); + + assertThat(results).hasSize(2); + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(1).getProductName()).isEqualTo("Tablet"); + } + + @Test + @DisplayName("Get top sellers with date range should only include orders in range") + void getTopSellers_withDateRange_shouldOnlyIncludeOrdersInRange() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 2)) // Start from May 2nd + .endDate(LocalDate.of(2024, 5, 4)) // End before May 4th + .sortBy(TopSellerRequest.SortBy.REVENUE) + .limit(5) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should have only phone, book, and tablet (from orders 2 and 3) + assertThat(results).hasSize(3); + + // First should be phone with only Order 2 revenue + assertThat(results.get(0).getProductName()).isEqualTo("Smartphone"); + assertThat(results.get(0).getValue()).isEqualByComparingTo("1500.00"); // Only order 2: 3 phones * $500 + + // Should include tablets from order 3 + boolean hasTablet = results.stream() + .anyMatch(r -> r.getProductName().equals("Tablet") + && r.getValue().compareTo(new BigDecimal("600.00")) == 0); + assertThat(hasTablet).isTrue(); + + // Should include books from order 3 + boolean hasBook = results.stream() + .anyMatch(r -> r.getProductName().equals("Programming Book") + && r.getValue().compareTo(new BigDecimal("200.00")) == 0); + assertThat(hasBook).isTrue(); + + // Should NOT include laptop (only in order 1) + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop")); + assertThat(hasLaptop).isFalse(); + } + } + + @Nested + @DisplayName("Edge Case Tests") + class EdgeCaseTests { + + @Test + @DisplayName("Get top sellers with empty result set should return empty list") + void getTopSellers_withNoMatchingData_shouldReturnEmptyList() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 6, 1)) // Future date with no data + .endDate(LocalDate.of(2024, 6, 2)) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .limit(5) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + assertThat(results).isEmpty(); + } + + @Test + @DisplayName("Get top sellers with zero limit should return all results") + void getTopSellers_withZeroLimit_shouldReturnAllResults() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .limit(0) // Zero limit + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should return all 4 products with sales in the period + assertThat(results).hasSize(0); + } + + @Test + @DisplayName("Get top sellers with single day range (inclusive) should work correctly") + void getTopSellers_withSingleDayRange_shouldWorkCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 1)) // End date inclusive + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should only include products from order1 (May 1st) + assertThat(results).hasSize(2); + + // Smartphone should be included + boolean hasPhone = results.stream() + .anyMatch(r -> r.getProductName().equals("Smartphone") + && r.getValue().compareTo(new BigDecimal("1000.00")) == 0); + assertThat(hasPhone).isTrue(); + + // Laptop should be included + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop") + && r.getValue().compareTo(new BigDecimal("1000.00")) == 0); + assertThat(hasLaptop).isTrue(); + } + + @Test + @DisplayName("Get top sellers should exclude failed orders") + void getTopSellers_shouldExcludeFailedOrders() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 4)) // Only include May 4th (failed order day) + .endDate(LocalDate.of(2024, 5, 4)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should be empty because the only order on May 4th was failed + assertThat(results).isEmpty(); + + // Specifically, the laptop from the failed order should not be included + boolean hasLaptop = results.stream() + .anyMatch(r -> r.getProductName().equals("Laptop")); + assertThat(hasLaptop).isFalse(); + } + + @Test + @DisplayName("Get top sellers including boundary dates should work correctly") + void getTopSellers_withBoundaryDates_shouldWorkCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 4, 30)) // Include April 30 + .endDate(LocalDate.of(2024, 4, 30)) // Exclude May 1 + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should only include products from April 30th (order6) + assertThat(results).hasSize(2); + + // Book should be included + boolean hasBook = results.stream() + .anyMatch(r -> r.getProductName().equals("Programming Book") + && r.getValue().compareTo(new BigDecimal("300.00")) == 0); + assertThat(hasBook).isTrue(); + + // Phone should be included + boolean hasPhone = results.stream() + .anyMatch(r -> r.getProductName().equals("Smartphone") + && r.getValue().compareTo(new BigDecimal("450.00")) == 0); + assertThat(hasPhone).isTrue(); + } + } + + @Nested + @DisplayName("Sorting and Value Tests") + class SortingAndValueTests { + + @Test + @DisplayName("Products with same revenue but different units should sort by revenue first") + void getTopSellers_withSameRevenue_shouldSortCorrectly() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 5)) // Only include May 5th order + .endDate(LocalDate.of(2024, 5, 6)) + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Should have both products with $500 revenue + assertThat(results).hasSize(2); + + // Both should have same revenue value + assertThat(results.get(0).getValue()).isEqualByComparingTo(results.get(1).getValue()); + assertThat(results.get(0).getValue()).isEqualByComparingTo("500.00"); + + // Check units separately to verify the data is correct + // (This doesn't test sorting order, but verifies the test data is as expected) + boolean hasTablet = results.stream() + .anyMatch(r -> r.getProductName().equals("Tablet")); + boolean hasHeadphones = results.stream() + .anyMatch(r -> r.getProductName().equals("Wireless Headphones")); + + assertThat(hasTablet).isTrue(); + assertThat(hasHeadphones).isTrue(); + } + + @Test + @DisplayName("Get top sellers by units with products having same units should respect secondary sort") + void getTopSellers_byUnitsWithSameUnits_shouldRespectSecondarySorting() { + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 6)) + .sortBy(TopSellerRequest.SortBy.UNITS).limit(10) + .build(); + + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + + // Find all products with 5 units + List productsWithFiveUnits = results.stream() + .filter(r -> r.getValue().compareTo(BigDecimal.valueOf(5)) == 0) + .collect(Collectors.toList()); + + // Should have 3 products with 5 units (phone, headphones, book) + assertThat(productsWithFiveUnits.size()).isEqualTo(3); + + // Verify that secondary sorting works (we expect by revenue) + // Get product names in order + List productOrder = productsWithFiveUnits.stream() + .map(TopSellerResponse::getProductName) + .collect(Collectors.toList()); + + // Expected order: Smartphone ($2500), Headphones ($500), Book ($200) + int smartphoneIdx = productOrder.indexOf("Smartphone"); + int headphonesIdx = productOrder.indexOf("Wireless Headphones"); + int bookIdx = productOrder.indexOf("Programming Book"); + + assertTrue(smartphoneIdx < headphonesIdx, "Smartphone should come before Headphones"); + assertTrue(headphonesIdx < bookIdx, "Headphones should come before Programming Book"); + } + } + + @Nested + @DisplayName("Request Parameter Tests") + class RequestParameterTests { + + @Test + @DisplayName("Get top sellers with swapped date range should handle gracefully") + void getTopSellers_withSwappedDateRange_shouldHandleGracefully() { + // Start date is after end date - test depends on how service handles this + TopSellerRequest request = TopSellerRequest.builder() + .startDate(LocalDate.of(2024, 5, 6)) // Start after end + .endDate(LocalDate.of(2024, 5, 1)) // End before start + .limit(5) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // If service handles swapped dates, this may return empty result + // or throw an exception + List results = productAnalyticsService.getTopSellers(request.getStartDate(), + request.getEndDate(), + request.getLimit(), + request.getSortBy()); + // Should return empty list if swapped dates are handled + assertThat(results).isEmpty(); + // If exception is expected, you may need to adjust this test + // assertThrows(IllegalArgumentException.class, () -> + // productAnalyticsService.getTopSellers(request)); + } + } +} diff --git a/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java b/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java new file mode 100644 index 0000000..acab99f --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/integration/RevenueReportServiceIntegrationTest.java @@ -0,0 +1,155 @@ +package com.Podzilla.analytics.integration; + +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.RegionRepository; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; +import com.Podzilla.analytics.services.RevenueReportService; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +public class RevenueReportServiceIntegrationTest { + + @Autowired + private RevenueReportService revenueReportService; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private SalesLineItemRepository salesLineItemRepository; + + @Autowired + private CourierRepository courierRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private RegionRepository regionRepository; + + @BeforeEach + public void setUp() { + insertTestData(); + } + + private void insertTestData() { + // Create and save region + Region region = Region.builder() + .city("Test City") + .state("Test State") + .country("Test Country") + .postalCode("12345") + .build(); + region = regionRepository.save(region); + + // Create courier + Courier courier = Courier.builder() + .name("Test Courier") + .status(Courier.CourierStatus.ACTIVE) + .build(); + courier = courierRepository.save(courier); + + // Create customer + Customer customer = Customer.builder() + .name("Test Customer") + .build(); + customer = customerRepository.save(customer); + + // Create products + Product product1 = Product.builder() + .name("Phone Case") + .category("Accessories") + .build(); + + Product product2 = Product.builder() + .name("Wireless Mouse") + .category("Electronics") + .build(); + + productRepository.saveAll(List.of(product1, product2)); + + // Create order with all required relationships + Order order1 = Order.builder() + .orderPlacedTimestamp(LocalDateTime.of(2024, 5, 1, 10, 0)) + .finalStatusTimestamp(LocalDateTime.of(2024, 5, 1, 11, 0)) + .status(Order.OrderStatus.COMPLETED) + .totalAmount(new BigDecimal("100.00")) + .courier(courier) + .customer(customer) + .region(region) + .build(); + + orderRepository.save(order1); + + SalesLineItem item1 = SalesLineItem.builder() + .order(order1) + .product(product1) + .quantity(2) + .pricePerUnit(new BigDecimal("10.00")) + .build(); + + SalesLineItem item2 = SalesLineItem.builder() + .order(order1) + .product(product2) + .quantity(1) + .pricePerUnit(new BigDecimal("80.00")) + .build(); + + salesLineItemRepository.saveAll(List.of(item1, item2)); + } + + @Test + public void getRevenueByCategory_shouldReturnExpectedResults() { + List results = revenueReportService.getRevenueByCategory( + LocalDate.of(2024, 5, 1), + LocalDate.of(2024, 5, 3) + ); + + assertThat(results).isNotEmpty(); + assertThat(results.get(0).getCategory()).isEqualTo("Electronics"); + } + + @Test + public void getRevenueSummary_shouldReturnExpectedResults() { + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(LocalDate.of(2024, 5, 1)) + .endDate(LocalDate.of(2024, 5, 3)) + .period(RevenueSummaryRequest.Period.DAILY) + .build(); + + List summary = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), + request.getPeriod().name()); + + assertThat(summary).isNotEmpty(); + assertThat(summary.get(0).getTotalRevenue()).isEqualByComparingTo("100.00"); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java new file mode 100644 index 0000000..fb2b5ee --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/ProductAnalyticsServiceTest.java @@ -0,0 +1,229 @@ +package com.Podzilla.analytics.services; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; // Keep import if TopSellerRequest still uses LocalDate + +import static org.junit.jupiter.api.Assertions.assertEquals; // Import LocalDateTime +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import org.mockito.Mock; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.Podzilla.analytics.api.dtos.product.TopSellerRequest; +import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; +import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; +import com.Podzilla.analytics.repositories.ProductRepository; + +@ExtendWith(MockitoExtension.class) +class ProductAnalyticsServiceTest { + + @Mock + private ProductRepository productRepository; + + private ProductAnalyticsService productAnalyticsService; + + @BeforeEach + void setUp() { + productAnalyticsService = new ProductAnalyticsService(productRepository); + } + + @Test + void getTopSellers_SortByRevenue_ShouldReturnCorrectList() { + // Arrange + // Assuming TopSellerRequest still uses LocalDate for input + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(2) // Ensure limit is set to 2 + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + // Start of the start day + LocalDateTime startDate = requestStartDate.atStartOfDay(); + // Start of the day AFTER the end day to include the whole end day in the query + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + // Mocking the repository to return 2 projections + List projections = Arrays.asList( + createProjection(1L, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(2L, "MacBook", "Electronics", new BigDecimal("2000.00"), 2L) + ); + + // Ensure the mock returns the correct results based on the given arguments + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(2), + eq("REVENUE"))) + .thenReturn(projections); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Log the result to help with debugging + result.forEach(item -> System.out.println("Product ID: " + item.getProductId() + " Revenue: " + item.getValue())); + + // Assert (Ensure the order is correct as per revenue) + assertEquals(2, result.size(), "Expected 2 products in the list."); + assertEquals(2L, result.get(0).getProductId()); // MacBook should come first due to higher revenue + assertEquals("MacBook", result.get(0).getProductName()); + assertEquals("Electronics", result.get(0).getCategory()); + assertEquals(new BigDecimal("2000.00"), result.get(0).getValue()); + + assertEquals(1L, result.get(1).getProductId()); + assertEquals("iPhone", result.get(1).getProductName()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("1000.00"), result.get(1).getValue()); + } + + + @Test + void getTopSellers_SortByUnits_ShouldReturnCorrectList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(2) + .sortBy(TopSellerRequest.SortBy.UNITS) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + LocalDateTime startDate = requestStartDate.atStartOfDay(); + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + List projections = Arrays.asList( + createProjection(1L, "iPhone", "Electronics", new BigDecimal("1000.00"), 5L), + createProjection(2L, "MacBook", "Electronics", new BigDecimal("2000.00"), 2L) + ); + + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(2), + eq("UNITS"))) + .thenReturn(projections); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert (Ensure the order is correct as per units) + assertEquals(2, result.size()); + assertEquals(1L, result.get(0).getProductId()); // iPhone comes first because of more units sold + assertEquals("iPhone", result.get(0).getProductName()); + assertEquals("Electronics", result.get(0).getCategory()); + // Note: The projection returns revenue and units as BigDecimal and Long respectively. + // The conversion to TopSellerResponse seems to put units into the 'value' field for this case. + assertEquals(new BigDecimal("5"), result.get(0).getValue()); + + + assertEquals(2L, result.get(1).getProductId()); + assertEquals("MacBook", result.get(1).getProductName()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("2"), result.get(1).getValue()); + } + + @Test + void getTopSellers_WithEmptyResult_ShouldReturnEmptyList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(10) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Use any() matchers for LocalDateTime parameters + when(productRepository.findTopSellers(any(LocalDateTime.class), any(LocalDateTime.class), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert + assertTrue(result.isEmpty()); + } + + + @Test + void getTopSellers_WithZeroLimit_ShouldReturnEmptyList() { + // Arrange + LocalDate requestStartDate = LocalDate.of(2025, 1, 1); + LocalDate requestEndDate = LocalDate.of(2025, 12, 31); + TopSellerRequest request = TopSellerRequest.builder() + .startDate(requestStartDate) + .endDate(requestEndDate) + .limit(0) + .sortBy(TopSellerRequest.SortBy.REVENUE) + .build(); + + // Convert LocalDate from request to LocalDateTime for repository call + LocalDateTime startDate = requestStartDate.atStartOfDay(); + LocalDateTime endDate = requestEndDate.plusDays(1).atStartOfDay(); + + + // Use LocalDateTime for the eq() matchers + when(productRepository.findTopSellers( + eq(startDate), + eq(endDate), + eq(0), + eq("REVENUE"))) + .thenReturn(Collections.emptyList()); + + // Act + List result = productAnalyticsService.getTopSellers(request.getStartDate(), request.getEndDate(), request.getLimit(), request.getSortBy()); + + // Assert + assertTrue(result.isEmpty()); + } + + private TopSellingProductProjection createProjection( + final Long id, + final String name, + final String category, + final BigDecimal revenue, + final Long units) { + return new TopSellingProductProjection() { + @Override + public Long getId() { + return id; + } + @Override + public String getName() { + return name; + } + @Override + public String getCategory() { + return category; + } + @Override + public BigDecimal getTotalRevenue() { + return revenue; + } + @Override + public Long getTotalUnits() { + return units; + } + }; + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java new file mode 100644 index 0000000..c578ea0 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java @@ -0,0 +1,211 @@ +package com.Podzilla.analytics.services; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.Podzilla.analytics.api.dtos.revenue.RevenueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryRequest; +import com.Podzilla.analytics.api.dtos.revenue.RevenueSummaryResponse; +import com.Podzilla.analytics.api.projections.revenue.RevenueByCategoryProjection; +import com.Podzilla.analytics.api.projections.revenue.RevenueSummaryProjection; +import com.Podzilla.analytics.repositories.OrderRepository; + +@ExtendWith(MockitoExtension.class) +class RevenueReportServiceTest { + + @Mock + private OrderRepository orderRepository; + + private RevenueReportService revenueReportService; + + @BeforeEach + void setUp() { + revenueReportService = new RevenueReportService(orderRepository); + } + + @Test + void getRevenueSummary_WithValidData_ShouldReturnCorrectSummary() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + List projections = Arrays.asList( + summaryProjection(LocalDate.of(2025, 1, 1), new BigDecimal("1000.00")), + summaryProjection(LocalDate.of(2025, 2, 1), new BigDecimal("2000.00")) + ); + + when(orderRepository.findRevenueSummaryByPeriod(eq(startDate), eq(endDate), eq("MONTHLY"))) + .thenReturn(projections); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertEquals(2, result.size()); + assertEquals(LocalDate.of(2025, 1, 1), result.get(0).getPeriodStartDate()); + assertEquals(new BigDecimal("1000.00"), result.get(0).getTotalRevenue()); + assertEquals(LocalDate.of(2025, 2, 1), result.get(1).getPeriodStartDate()); + assertEquals(new BigDecimal("2000.00"), result.get(1).getTotalRevenue()); + } + + @Test + void getRevenueSummary_WithEmptyData_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueSummary_WithStartDateAfterEndDate_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 12, 31); + LocalDate endDate = LocalDate.of(2025, 1, 1); + RevenueSummaryRequest request = RevenueSummaryRequest.builder() + .startDate(startDate) + .endDate(endDate) + .period(RevenueSummaryRequest.Period.MONTHLY) + .build(); + + when(orderRepository.findRevenueSummaryByPeriod(any(), any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueSummary(request.getStartDate(), + request.getEndDate(), request.getPeriod().name()); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueByCategory_WithValidData_ShouldReturnCorrectCategories() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); List projections = Arrays.asList( + categoryProjection("Books", new BigDecimal("3000.00")), + categoryProjection("Electronics", new BigDecimal("5000.00")) + ); + + when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + .thenReturn(projections);// Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertEquals(2, result.size()); + assertEquals("Books", result.get(0).getCategory()); + assertEquals(new BigDecimal("3000.00"), result.get(0).getTotalRevenue()); + assertEquals("Electronics", result.get(1).getCategory()); + assertEquals(new BigDecimal("5000.00"), result.get(1).getTotalRevenue()); + } + + @Test + void getRevenueByCategory_WithEmptyData_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + + when(orderRepository.findRevenueByCategory(any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertTrue(result.isEmpty()); + } + + @Test + void getRevenueByCategory_WithNullRevenue_ShouldHandleGracefully() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 1, 1); + LocalDate endDate = LocalDate.of(2025, 12, 31); + + List projections = Arrays.asList( + new RevenueByCategoryProjection() { + @Override + public String getCategory() { + return "Electronics"; + } + @Override + public BigDecimal getTotalRevenue() { + return null; + } + } + ); + + when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + .thenReturn(projections); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertEquals(1, result.size()); + assertEquals("Electronics", result.get(0).getCategory()); + assertNull(result.get(0).getTotalRevenue()); + } + + @Test + void getRevenueByCategory_WithStartDateAfterEndDate_ShouldReturnEmptyList() { + // Arrange + LocalDate startDate = LocalDate.of(2025, 12, 31); + LocalDate endDate = LocalDate.of(2025, 1, 1); + + when(orderRepository.findRevenueByCategory(any(), any())) + .thenReturn(Collections.emptyList()); + + // Act + List result = revenueReportService.getRevenueByCategory(startDate, endDate); + + // Assert + assertTrue(result.isEmpty()); + } + private RevenueSummaryProjection summaryProjection(LocalDate date, BigDecimal revenue) { + return new RevenueSummaryProjection() { + public LocalDate getPeriod() { return date; } + public BigDecimal getTotalRevenue() { return revenue; } + }; +} + + private RevenueByCategoryProjection categoryProjection(String category, BigDecimal revenue) { + return new RevenueByCategoryProjection() { + public String getCategory() { return category; } + public BigDecimal getTotalRevenue() { return revenue; } + }; + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..f03dd4c --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,15 @@ +# Test Database Configuration +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL +spring.datasource.username=sa +spring.datasource.password= +spring.datasource.driver-class-name=org.h2.Driver + +# JPA/Hibernate Configuration +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +# H2 Console (optional, for debugging) +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console From 342c739c93aed9a47c86fabc8b5514284087243a Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Sat, 17 May 2025 02:55:13 +0300 Subject: [PATCH 17/28] test: add unit and integration tests for courier analytics (#20) * test: add unit and integration tests for courier analytics * test: Configure in-memory H2 for integration tests * chore: merge latest changes from dev branch --- docker-compose.yml | 9 +- .../repositories/CourierRepository.java | 1 - .../services/CourierAnalyticsService.java | 4 +- .../analytics/util/MetricCalculator.java | 9 +- .../FulfillmentReportControllerTest.java | 335 ----------------- .../CourierAnalyticsControllerTest.java | 321 ++++++++++++++++ .../FulfillmentReportControllerTest.java | 339 +++++++++++++++++ .../services/CourierAnalyticsServiceTest.java | 345 ++++++++++++++++++ .../resources/application-test.properties | 7 + 9 files changed, 1021 insertions(+), 349 deletions(-) delete mode 100644 src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java create mode 100644 src/test/resources/application-test.properties diff --git a/docker-compose.yml b/docker-compose.yml index 57ea10d..b44bc1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,8 @@ services: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db_dev - SPRING_DATASOURCE_USERNAME: analytics_user - SPRING_DATASOURCE_PASSWORD: password + SPRING_DATASOURCE_USERNAME: postgres + SPRING_DATASOURCE_PASSWORD: 123 SPRING_RABBITMQ_HOST: rabbitmq # RabbitMQ container name or use its IP if needed SPRING_RABBITMQ_PORT: 5672 SPRING_RABBITMQ_USERNAME: guest @@ -23,8 +23,8 @@ services: - "5432:5432" environment: POSTGRES_DB: analytics_db_dev - POSTGRES_USER: analytics_user - POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 123 volumes: - db_data:/var/lib/postgresql/data @@ -47,4 +47,3 @@ services: volumes: db_data: - \ No newline at end of file diff --git a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java index 6fdaf48..eae7c5e 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CourierRepository.java @@ -23,7 +23,6 @@ public interface CourierRepository extends JpaRepository { + "LEFT JOIN orders o " + "ON c.id = o.courier_id " + "AND o.final_status_timestamp BETWEEN :startDate AND :endDate " - + "AND o.status IN ('COMPLETED', 'FAILED') " + "GROUP BY c.id, c.name " + "ORDER BY courierId", nativeQuery = true) List findCourierPerformanceBetweenDates( diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 9a70a67..8376613 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -53,7 +53,7 @@ public List getCourierSuccessRate( .courierId(data.getCourierId()) .courierName(data.getCourierName()) .successRate( - MetricCalculator.calculatePercentage( + MetricCalculator.calculateRate( data.getCompletedCount(), data.getDeliveryCount())) .build()) @@ -81,7 +81,7 @@ public List getCourierPerformanceReport( .courierName(data.getCourierName()) .deliveryCount(data.getDeliveryCount()) .successRate( - MetricCalculator.calculatePercentage( + MetricCalculator.calculateRate( data.getCompletedCount(), data.getDeliveryCount())) .averageRating(data.getAverageRating()) diff --git a/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java index 1e91256..a32839b 100644 --- a/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java +++ b/src/main/java/com/Podzilla/analytics/util/MetricCalculator.java @@ -6,8 +6,6 @@ public final class MetricCalculator { private static final int DEFAULT_SCALE = 2; - private static final BigDecimal ONE_HUNDRED = new BigDecimal("100"); - private MetricCalculator() { throw new UnsupportedOperationException( "This is a utility class and cannot be instantiated"); @@ -25,7 +23,7 @@ private MetricCalculator() { * @return The calculated percentage as a BigDecimal, or BigDecimal.ZERO * if the denominator is zero. */ - public static BigDecimal calculatePercentage(final long numerator, + public static BigDecimal calculateRate(final long numerator, final long denominator, final int scale, final RoundingMode roundingMode) { if (denominator == 0) { @@ -35,7 +33,6 @@ public static BigDecimal calculatePercentage(final long numerator, return BigDecimal.ZERO; } return BigDecimal.valueOf(numerator) - .multiply(ONE_HUNDRED) .divide(BigDecimal.valueOf(denominator), scale, roundingMode); } @@ -47,9 +44,9 @@ public static BigDecimal calculatePercentage(final long numerator, * @return The calculated percentage (scale 2, HALF_UP rounding), or * BigDecimal.ZERO if denominator is zero. */ - public static BigDecimal calculatePercentage(final long numerator, + public static BigDecimal calculateRate(final long numerator, final long denominator) { - return calculatePercentage(numerator, denominator, DEFAULT_SCALE, + return calculateRate(numerator, denominator, DEFAULT_SCALE, RoundingMode.HALF_UP); } } diff --git a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java deleted file mode 100644 index 3d87b5f..0000000 --- a/src/test/java/com/Podzilla/analytics/api/controllers/FulfillmentReportControllerTest.java +++ /dev/null @@ -1,335 +0,0 @@ -// package com.Podzilla.analytics.api.controllers; - -// import static org.junit.jupiter.api.Assertions.assertEquals; -// import static org.junit.jupiter.api.Assertions.assertNotNull; -// import static org.junit.jupiter.api.Assertions.assertTrue; -// import static org.mockito.ArgumentMatchers.any; -// import static org.mockito.Mockito.mock; -// import static org.mockito.Mockito.when; - -// import java.math.BigDecimal; -// import java.time.LocalDate; -// import java.util.Arrays; -// import java.util.Collections; -// import java.util.List; - -// import org.junit.jupiter.api.BeforeEach; -// import org.junit.jupiter.api.Test; -// import org.springframework.http.HttpStatus; -// import org.springframework.http.ResponseEntity; - -// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; -// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; -// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; -// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; -// import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; -// import com.Podzilla.analytics.services.FulfillmentAnalyticsService; - -// public class FulfillmentReportControllerTest { - -// private FulfillmentReportController controller; -// private FulfillmentAnalyticsService mockService; - -// private LocalDate startDate; -// private LocalDate endDate; -// private List overallTimeResponses; -// private List regionTimeResponses; -// private List courierTimeResponses; - -// @BeforeEach -// public void setup() { -// mockService = mock(FulfillmentAnalyticsService.class); -// controller = new FulfillmentReportController(mockService); - -// startDate = LocalDate.of(2024, 1, 1); -// endDate = LocalDate.of(2024, 1, 31); - -// // Setup test data -// overallTimeResponses = Arrays.asList( -// FulfillmentTimeResponse.builder() -// .groupByValue("OVERALL") -// .averageDuration(BigDecimal.valueOf(24.5)) -// .build()); - -// regionTimeResponses = Arrays.asList( -// FulfillmentTimeResponse.builder() -// .groupByValue("RegionID_1") -// .averageDuration(BigDecimal.valueOf(20.2)) -// .build(), -// FulfillmentTimeResponse.builder() -// .groupByValue("RegionID_2") -// .averageDuration(BigDecimal.valueOf(28.7)) -// .build()); - -// courierTimeResponses = Arrays.asList( -// FulfillmentTimeResponse.builder() -// .groupByValue("CourierID_1") -// .averageDuration(BigDecimal.valueOf(18.3)) -// .build(), -// FulfillmentTimeResponse.builder() -// .groupByValue("CourierID_2") -// .averageDuration(BigDecimal.valueOf(22.1)) -// .build()); -// } - -// @Test -// public void testGetPlaceToShipTime_Overall() { -// // Configure mock service -// when(mockService.getPlaceToShipTimeResponse( -// startDate, endDate, PlaceToShipGroupBy.OVERALL)) -// .thenReturn(overallTimeResponses); - -// // Create request -// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// startDate, endDate, PlaceToShipGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(overallTimeResponses, response.getBody()); -// assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); -// assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); -// } - -// @Test -// public void testGetPlaceToShipTime_ByRegion() { -// // Configure mock service -// when(mockService.getPlaceToShipTimeResponse( -// startDate, endDate, PlaceToShipGroupBy.REGION)) -// .thenReturn(regionTimeResponses); - -// // Create request -// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// startDate, endDate, PlaceToShipGroupBy.REGION); - -// // Execute the method -// ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(regionTimeResponses, response.getBody()); -// assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); -// assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); -// } - -// @Test -// public void testGetShipToDeliverTime_Overall() { -// // Configure mock service -// when(mockService.getShipToDeliverTimeResponse( -// startDate, endDate, ShipToDeliverGroupBy.OVERALL)) -// .thenReturn(overallTimeResponses); - -// // Create request -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// startDate, endDate, ShipToDeliverGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(overallTimeResponses, response.getBody()); -// assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); -// } - -// @Test -// public void testGetShipToDeliverTime_ByRegion() { -// // Configure mock service -// when(mockService.getShipToDeliverTimeResponse( -// startDate, endDate, ShipToDeliverGroupBy.REGION)) -// .thenReturn(regionTimeResponses); - -// // Create request -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// startDate, endDate, ShipToDeliverGroupBy.REGION); - -// // Execute the method -// ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(regionTimeResponses, response.getBody()); -// assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); -// assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); -// } - -// @Test -// public void testGetShipToDeliverTime_ByCourier() { -// // Configure mock service -// when(mockService.getShipToDeliverTimeResponse( -// startDate, endDate, ShipToDeliverGroupBy.COURIER)) -// .thenReturn(courierTimeResponses); - -// // Create request -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// startDate, endDate, ShipToDeliverGroupBy.COURIER); - -// // Execute the method -// ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(courierTimeResponses, response.getBody()); -// assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); -// assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); -// } - -// // Edge case tests - -// @Test -// public void testGetPlaceToShipTime_EmptyResponse() { -// // Configure mock service to return empty list -// when(mockService.getPlaceToShipTimeResponse( -// startDate, endDate, PlaceToShipGroupBy.OVERALL)) -// .thenReturn(Collections.emptyList()); - -// // Create request -// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// startDate, endDate, PlaceToShipGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertNotNull(response.getBody()); -// assertTrue(response.getBody().isEmpty()); -// } - -// @Test -// public void testGetShipToDeliverTime_EmptyResponse() { -// // Configure mock service to return empty list -// when(mockService.getShipToDeliverTimeResponse( -// startDate, endDate, ShipToDeliverGroupBy.OVERALL)) -// .thenReturn(Collections.emptyList()); - -// // Create request -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// startDate, endDate, ShipToDeliverGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertNotNull(response.getBody()); -// assertTrue(response.getBody().isEmpty()); -// } - -// // @Test -// // public void testGetPlaceToShipTime_InvalidGroupBy() { -// // // Create request with invalid groupBy -// // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// // startDate, endDate, null); - -// // // Execute the method - should return bad request due to validation error -// // ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // // Verify response -// // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); -// // } - -// // @Test -// // public void testGetShipToDeliverTime_InvalidGroupBy() { -// // // Create request with invalid groupBy -// // FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// // startDate, endDate, null); - -// // // Execute the method - should return bad request due to validation error -// // ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // // Verify response -// // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); -// // } - -// @Test -// public void testGetPlaceToShipTime_SameDayRange() { -// // Test same start and end date -// LocalDate sameDate = LocalDate.of(2024, 1, 1); - -// // Configure mock service -// when(mockService.getPlaceToShipTimeResponse( -// sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) -// .thenReturn(overallTimeResponses); - -// // Create request with same start and end date -// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// sameDate, sameDate, PlaceToShipGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(overallTimeResponses, response.getBody()); -// } - -// @Test -// public void testGetShipToDeliverTime_SameDayRange() { -// // Test same start and end date -// LocalDate sameDate = LocalDate.of(2024, 1, 1); - -// // Configure mock service -// when(mockService.getShipToDeliverTimeResponse( -// sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) -// .thenReturn(overallTimeResponses); - -// // Create request with same start and end date -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getShipToDeliverTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertEquals(overallTimeResponses, response.getBody()); -// } - -// @Test -// public void testGetPlaceToShipTime_FutureDates() { -// // Test future dates -// LocalDate futureStart = LocalDate.now().plusDays(1); -// LocalDate futureEnd = LocalDate.now().plusDays(30); - -// // Configure mock service - should return empty for future dates -// when(mockService.getPlaceToShipTimeResponse( -// futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) -// .thenReturn(Collections.emptyList()); - -// // Create request with future dates -// FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( -// futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); - -// // Execute the method -// ResponseEntity> response = controller.getPlaceToShipTime(request); - -// // Verify response -// assertEquals(HttpStatus.OK, response.getStatusCode()); -// assertNotNull(response.getBody()); -// assertTrue(response.getBody().isEmpty()); -// } - -// @Test -// public void testGetShipToDeliverTime_ServiceException() { -// // Configure mock service to throw exception -// when(mockService.getShipToDeliverTimeResponse( -// any(), any(), any())) -// .thenThrow(new RuntimeException("Service error")); - -// // Create request -// FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( -// startDate, endDate, ShipToDeliverGroupBy.OVERALL); - -// // Execute the method - controller should handle exception -// // Note: Actual behavior depends on how controller handles exceptions -// // This might need adjustment based on actual implementation -// try { -// controller.getShipToDeliverTime(request); -// } catch (RuntimeException e) { -// assertEquals("Service error", e.getMessage()); -// } -// } -// } \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java new file mode 100644 index 0000000..f383c56 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/CourierAnalyticsControllerTest.java @@ -0,0 +1,321 @@ +package com.Podzilla.analytics.controllers; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.transaction.annotation.Transactional; + +import com.Podzilla.analytics.models.Courier; +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.repositories.CourierRepository; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.RegionRepository; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.hamcrest.Matchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +public class CourierAnalyticsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private CourierRepository courierRepository; + @Autowired + private CustomerRepository customerRepository; + @Autowired + private RegionRepository regionRepository; + @Autowired + private OrderRepository orderRepository; + + @Autowired + private EntityManager entityManager; + + private static final DateTimeFormatter ISO_LOCAL_DATE = DateTimeFormatter.ISO_LOCAL_DATE; + + private Customer customer1; + private Region region1; + private Courier courierJane; + private Courier courierJohn; + + @BeforeEach + void setUp() { + orderRepository.deleteAll(); + courierRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + + entityManager.flush(); + entityManager.clear(); + + customer1 = customerRepository.save(Customer.builder().name("John Doe").build()); + region1 = regionRepository.save(Region.builder() + .city("Sample City") + .state("Sample State") + .country("Sample Country") + .postalCode("12345") + .build()); + + courierJane = courierRepository.save(Courier.builder() + .name("Jane Smith") + .status(Courier.CourierStatus.ACTIVE) + .build()); + + courierJohn = courierRepository.save(Courier.builder() + .name("John Doe") + .status(Courier.CourierStatus.ACTIVE) + .build()); + + orderRepository.save(Order.builder() + .totalAmount(new BigDecimal("50.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) + .status(Order.OrderStatus.COMPLETED) + .numberOfItems(1) + .courierRating(new BigDecimal("4.0")) + .customer(customer1) + .courier(courierJane) + .region(region1) + .build()); + + orderRepository.save(Order.builder() + .totalAmount(new BigDecimal("75.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) + .status(Order.OrderStatus.COMPLETED) + .numberOfItems(1) + .courierRating(new BigDecimal("4.0")) + .customer(customer1) + .courier(courierJane) + .region(region1) + .build()); + + orderRepository.save(Order.builder() + .totalAmount(new BigDecimal("120.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) + .status(Order.OrderStatus.COMPLETED) + .numberOfItems(2) + .courierRating(new BigDecimal("5.0")) + .customer(customer1) + .courier(courierJane) + .region(region1) + .build()); + + orderRepository.save(Order.builder() + .totalAmount(new BigDecimal("30.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) + .status(Order.OrderStatus.FAILED) + .numberOfItems(1) + .courierRating(null) + .customer(customer1) + .courier(courierJohn) + .region(region1) + .build()); + + orderRepository.save(Order.builder() + .totalAmount(new BigDecimal("90.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) + .status(Order.OrderStatus.COMPLETED) + .numberOfItems(1) + .courierRating(new BigDecimal("3.0")) + .customer(customer1) + .courier(courierJohn) + .region(region1) + .build()); + + entityManager.flush(); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + orderRepository.deleteAll(); + courierRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void contextLoads() { + } + + @Test + void getCourierDeliveryCounts_shouldReturnCountsForSpecificDateRange() throws Exception { + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now().minusDays(2); + + mockMvc.perform(get("/courier-analytics/delivery-counts") + .param("startDate", startDate.format( + ISO_LOCAL_DATE)) + .param("endDate", endDate.format( + ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].deliveryCount").value(hasItem(2))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].deliveryCount").value(hasItem(2))); + } + + @Test + void getCourierDeliveryCounts_shouldReturnZeroCountsWhenNoDeliveriesInDateRange() + throws Exception { + LocalDate startDate = LocalDate.now().plusDays(1); + LocalDate endDate = LocalDate.now().plusDays(2); + + mockMvc.perform(get("/courier-analytics/delivery-counts") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].courierName").value(hasItem("Jane Smith"))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].deliveryCount").value(hasItem(0))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].courierName").value(hasItem("John Doe"))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].deliveryCount").value(hasItem(0))); + } + + @Test + void getCourierDeliveryCounts_shouldHandleInvalidDateRange_startDateAfterEndDate() + throws Exception { + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().minusDays(3); + + mockMvc.perform(get("/courier-analytics/delivery-counts") + .param("startDate", startDate.format( + ISO_LOCAL_DATE)) + .param("endDate", endDate.format( + ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isBadRequest()); + } + + @Test + void getCourierSuccessRate_shouldReturnSuccessRatesForSpecificDateRange() + throws Exception { + LocalDateTime startDate = LocalDateTime.now().minusDays(4); + LocalDateTime endDate = LocalDateTime.now().minusDays(2); + + mockMvc.perform(get("/courier-analytics/success-rate") + .param("startDate", startDate.format( + ISO_LOCAL_DATE)) + .param("endDate", endDate.format( + ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect( + jsonPath("$[?(@.courierName == 'Jane Smith')].successRate").value(hasItem(closeTo(1.0, 0.001)))) + .andExpect( + jsonPath("$[?(@.courierName == 'John Doe')].successRate").value(hasItem(closeTo(0.5, 0.001)))); + } + + @Test + void getCourierAverageRating_shouldReturnAllAverageRatingsWhenNoDateRangeProvided() + throws Exception { + LocalDateTime startDate = LocalDateTime.now().minusDays(4); + LocalDateTime endDate = LocalDateTime.now(); + mockMvc.perform(get("/courier-analytics/average-rating") + .param("startDate", startDate.format( + ISO_LOCAL_DATE)) + .param("endDate", endDate.format( + ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].averageRating") + .value(hasItem(closeTo(4.333, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].averageRating") + .value(hasItem(closeTo(3.0, 0.001)))); + } + + @Test + void getCourierAverageRating_shouldReturnAverageRatingsForSpecificDateRange() + throws Exception { + + LocalDateTime startDate = LocalDateTime.now().minusDays(4); + LocalDateTime endDate = LocalDateTime.now().minusDays(2); + + mockMvc.perform(get("/courier-analytics/average-rating") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].averageRating") + .value(hasItem(closeTo(4.0, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].averageRating") + .value(hasItem(closeTo(3.0, 0.001)))); + } + + @Test + void getCourierPerformanceReport_shouldReturnAllPerformanceReportsWhenNoDateRangeProvided() + throws Exception { + LocalDateTime startDate = LocalDateTime.now().minusDays(4); + LocalDateTime endDate = LocalDateTime.now(); + mockMvc.perform(get("/courier-analytics/performance-report") + .param("startDate", startDate.format( + ISO_LOCAL_DATE)) + .param("endDate", endDate.format( + ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].deliveryCount").value(hasItem(3))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].successRate") + .value(hasItem(closeTo(1.0, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].averageRating") + .value(hasItem(closeTo(4.333, 0.001)))) + + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].deliveryCount").value(hasItem(2))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].successRate") + .value(hasItem(closeTo(0.5, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].averageRating") + .value(hasItem(closeTo(3.0, 0.001)))); + } + + @Test + void getCourierPerformanceReport_shouldReturnPerformanceReportsForSpecificDateRange() + throws Exception { + LocalDateTime startDate = LocalDateTime.now().minusDays(4); + LocalDateTime endDate = LocalDateTime.now().minusDays(2); + + mockMvc.perform(get("/courier-analytics/performance-report") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].deliveryCount").value(hasItem(2))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].successRate") + .value(hasItem(closeTo(1.0, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'Jane Smith')].averageRating") + .value(hasItem(closeTo(4.0, 0.001)))) + + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].deliveryCount").value(hasItem(2))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].successRate") + .value(hasItem(closeTo(0.5, 0.001)))) + .andExpect(jsonPath("$[?(@.courierName == 'John Doe')].averageRating") + .value(hasItem(closeTo(3.0, 0.001)))); + } +} diff --git a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java new file mode 100644 index 0000000..61e3254 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java @@ -0,0 +1,339 @@ +package com.Podzilla.analytics.controllers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import com.Podzilla.analytics.api.controllers.FulfillmentReportController; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; +import com.Podzilla.analytics.services.FulfillmentAnalyticsService; + +public class FulfillmentReportControllerTest { + + private FulfillmentReportController controller; + private FulfillmentAnalyticsService mockService; + + private LocalDate startDate; + private LocalDate endDate; + private List overallTimeResponses; + private List regionTimeResponses; + private List courierTimeResponses; + + @BeforeEach + public void setup() { + mockService = mock(FulfillmentAnalyticsService.class); + controller = new FulfillmentReportController(mockService); + + startDate = LocalDate.of(2024, 1, 1); + endDate = LocalDate.of(2024, 1, 31); + + // Setup test data + overallTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("OVERALL") + .averageDuration(BigDecimal.valueOf(24.5)) + .build()); + + regionTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("RegionID_1") + .averageDuration(BigDecimal.valueOf(20.2)) + .build(), + FulfillmentTimeResponse.builder() + .groupByValue("RegionID_2") + .averageDuration(BigDecimal.valueOf(28.7)) + .build()); + + courierTimeResponses = Arrays.asList( + FulfillmentTimeResponse.builder() + .groupByValue("CourierID_1") + .averageDuration(BigDecimal.valueOf(18.3)) + .build(), + FulfillmentTimeResponse.builder() + .groupByValue("CourierID_2") + .averageDuration(BigDecimal.valueOf(22.1)) + .build()); + } + + @Test + public void testGetPlaceToShipTime_Overall() { + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); + } + + @Test + public void testGetPlaceToShipTime_ByRegion() { + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.REGION)) + .thenReturn(regionTimeResponses); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.REGION); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(regionTimeResponses, response.getBody()); + assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + } + + @Test + public void testGetShipToDeliverTime_Overall() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + } + + @Test + public void testGetShipToDeliverTime_ByRegion() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.REGION)) + .thenReturn(regionTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.REGION); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(regionTimeResponses, response.getBody()); + assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + } + + @Test + public void testGetShipToDeliverTime_ByCourier() { + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.COURIER)) + .thenReturn(courierTimeResponses); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.COURIER); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(courierTimeResponses, response.getBody()); + assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); + assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); + } + + // Edge case tests + + @Test + public void testGetPlaceToShipTime_EmptyResponse() { + // Configure mock service to return empty list + when(mockService.getPlaceToShipTimeResponse( + startDate, endDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + startDate, endDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + @Test + public void testGetShipToDeliverTime_EmptyResponse() { + // Configure mock service to return empty list + when(mockService.getShipToDeliverTimeResponse( + startDate, endDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + // @Test + // public void testGetPlaceToShipTime_InvalidGroupBy() { + // // Create request with invalid groupBy + // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + // startDate, endDate, null); + + // // Execute the method - should return bad request due to validation error + // ResponseEntity> response = + // controller.getPlaceToShipTime(request); + + // // Verify response + // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + // } + + // @Test + // public void testGetShipToDeliverTime_InvalidGroupBy() { + // // Create request with invalid groupBy + // FulfillmentShipToDeliverRequest request = new + // FulfillmentShipToDeliverRequest( + // startDate, endDate, null); + + // // Execute the method - should return bad request due to validation error + // ResponseEntity> response = + // controller.getShipToDeliverTime(request); + + // // Verify response + // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + // } + + @Test + public void testGetPlaceToShipTime_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getPlaceToShipTimeResponse( + sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request with same start and end date + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + sameDate, sameDate, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + } + + @Test + public void testGetShipToDeliverTime_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getShipToDeliverTimeResponse( + sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) + .thenReturn(overallTimeResponses); + + // Create request with same start and end date + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getShipToDeliverTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals(overallTimeResponses, response.getBody()); + } + + @Test + public void testGetPlaceToShipTime_FutureDates() { + // Test future dates + LocalDate futureStart = LocalDate.now().plusDays(1); + LocalDate futureEnd = LocalDate.now().plusDays(30); + + // Configure mock service - should return empty for future dates + when(mockService.getPlaceToShipTimeResponse( + futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) + .thenReturn(Collections.emptyList()); + + // Create request with future dates + FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( + futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); + + // Execute the method + ResponseEntity> response = controller.getPlaceToShipTime(request); + + // Verify response + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().isEmpty()); + } + + @Test + public void testGetShipToDeliverTime_ServiceException() { + // Configure mock service to throw exception + when(mockService.getShipToDeliverTimeResponse( + any(), any(), any())) + .thenThrow(new RuntimeException("Service error")); + + // Create request + FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( + startDate, endDate, ShipToDeliverGroupBy.OVERALL); + + // Execute the method - controller should handle exception + // Note: Actual behavior depends on how controller handles exceptions + // This might need adjustment based on actual implementation + try { + controller.getShipToDeliverTime(request); + } catch (RuntimeException e) { + assertEquals("Service error", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java b/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java new file mode 100644 index 0000000..1cb8c82 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/CourierAnalyticsServiceTest.java @@ -0,0 +1,345 @@ +package com.Podzilla.analytics.services; + +import com.Podzilla.analytics.api.dtos.courier.CourierAverageRatingResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierDeliveryCountResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierPerformanceReportResponse; +import com.Podzilla.analytics.api.dtos.courier.CourierSuccessRateResponse; +import com.Podzilla.analytics.api.projections.courier.CourierPerformanceProjection; +import com.Podzilla.analytics.repositories.CourierRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CourierAnalyticsServiceTest { + + @Mock + private CourierRepository courierRepository; + + @InjectMocks + private CourierAnalyticsService courierAnalyticsService; + + private LocalDate testStartDate; + private LocalDate testEndDate; + private LocalDateTime expectedStartDateTime; + private LocalDateTime expectedEndDateTime; + + @BeforeEach + void setUp() { + testStartDate = LocalDate.of(2024, 1, 1); + testEndDate = LocalDate.of(2024, 1, 31); + expectedStartDateTime = testStartDate.atStartOfDay(); + expectedEndDateTime = testEndDate.atTime(LocalTime.MAX); + } + + private CourierPerformanceProjection createMockProjection( + Long 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); + Mockito.lenient().when(mockProjection.getDeliveryCount()).thenReturn(deliveryCount); + Mockito.lenient().when(mockProjection.getCompletedCount()).thenReturn(completedCount); + Mockito.lenient().when(mockProjection.getAverageRating()).thenReturn(averageRating); + return mockProjection; + } + + @Test + void getCourierDeliveryCounts_shouldReturnCorrectCountsForMultipleCouriers() { + // Arrange + CourierPerformanceProjection janeData = createMockProjection( + 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + CourierPerformanceProjection johnData = createMockProjection( + 2L, "John", 5L, 5L, new BigDecimal("4.0")); + + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Arrays.asList(janeData, johnData)); + + // Act + List result = courierAnalyticsService + .getCourierDeliveryCounts(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + CourierDeliveryCountResponse janeResponse = result.stream() + .filter(r -> r.getCourierName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(1L, 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(5, johnResponse.getDeliveryCount()); + + // Verify repository method was called with correct LocalDateTime arguments + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierDeliveryCounts_shouldReturnEmptyListWhenNoData() { + // Arrange + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Collections.emptyList()); + + // Act + List result = courierAnalyticsService + .getCourierDeliveryCounts(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierSuccessRate_shouldReturnCorrectRates() { + // Arrange + // Jane: 8 completed out of 10 deliveries = 80% + CourierPerformanceProjection janeData = createMockProjection( + 1L, "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")); + // Peter: 0 completed out of 2 deliveries = 0% + CourierPerformanceProjection peterData = createMockProjection( + 3L, "Peter", 2L, 0L, new BigDecimal("3.0")); + + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Arrays.asList(janeData, johnData, peterData)); + + // Act + List result = courierAnalyticsService + .getCourierSuccessRate(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertEquals(3, result.size()); + + CourierSuccessRateResponse janeResponse = result.stream() + .filter(r -> r.getCourierName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(1L, 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()); + 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()); + assertTrue(peterResponse.getSuccessRate().compareTo(new BigDecimal("0.00")) == 0); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierSuccessRate_shouldHandleZeroDeliveryCountGracefully() { + // Arrange + // Mark: 0 completed out of 0 deliveries. MetricCalculator should handle this + // (e.g., return 0 or null) + CourierPerformanceProjection markData = createMockProjection( + 4L, "Mark", 0L, 0L, new BigDecimal("0.0")); + + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Collections.singletonList(markData)); + + // Act + List result = courierAnalyticsService + .getCourierSuccessRate(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + CourierSuccessRateResponse markResponse = result.get(0); + assertEquals(4L, markResponse.getCourierId()); + assertTrue(markResponse.getSuccessRate().compareTo(new BigDecimal("0.00")) == 0); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierSuccessRate_shouldReturnEmptyListWhenNoData() { + // Arrange + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Collections.emptyList()); + + // Act + List result = courierAnalyticsService + .getCourierSuccessRate(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierAverageRating_shouldReturnCorrectAverageRatings() { + // Arrange + CourierPerformanceProjection janeData = createMockProjection( + 1L, "Jane", 10L, 8L, new BigDecimal("4.5")); + CourierPerformanceProjection johnData = createMockProjection( + 2L, "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 + + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Arrays.asList(janeData, johnData, peterData)); + + // Act + List result = courierAnalyticsService + .getCourierAverageRating(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertEquals(3, result.size()); + + CourierAverageRatingResponse janeResponse = result.stream() + .filter(r -> r.getCourierName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(1L, 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(new BigDecimal("4.0"), johnResponse.getAverageRating()); + + CourierAverageRatingResponse peterResponse = result.stream() + .filter(r -> r.getCourierName().equals("Peter")) + .findFirst().orElse(null); + assertNotNull(peterResponse); + assertNull(peterResponse.getAverageRating()); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierAverageRating_shouldReturnEmptyListWhenNoData() { + // Arrange + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Collections.emptyList()); + + // Act + List result = courierAnalyticsService + .getCourierAverageRating(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierPerformanceReport_shouldReturnComprehensiveReport() { + // Arrange + // Jane: 8 completed out of 10 deliveries = 80%, Avg Rating 4.5 + CourierPerformanceProjection janeData = createMockProjection( + 1L, "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")); + + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Arrays.asList(janeData, johnData)); + + // Act + List result = courierAnalyticsService + .getCourierPerformanceReport(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + CourierPerformanceReportResponse janeResponse = result.stream() + .filter(r -> r.getCourierName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(1L, janeResponse.getCourierId()); + assertEquals(10, janeResponse.getDeliveryCount()); + assertTrue(janeResponse.getSuccessRate().compareTo(new BigDecimal("0.80")) == 0); + assertTrue(janeResponse.getAverageRating().compareTo(new BigDecimal("4.5")) == 0); + + CourierPerformanceReportResponse johnResponse = result.stream() + .filter(r -> r.getCourierName().equals("John")) + .findFirst().orElse(null); + assertNotNull(johnResponse); + assertEquals(2L, johnResponse.getCourierId()); + assertEquals(5, johnResponse.getDeliveryCount()); + assertTrue(johnResponse.getSuccessRate().compareTo(new BigDecimal("1.00")) == 0); + assertTrue(johnResponse.getAverageRating().compareTo(new BigDecimal("4.0")) == 0); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } + + @Test + void getCourierPerformanceReport_shouldReturnEmptyListWhenNoData() { + // Arrange + when(courierRepository.findCourierPerformanceBetweenDates( + any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(Collections.emptyList()); + + // Act + List result = courierAnalyticsService + .getCourierPerformanceReport(testStartDate, testEndDate); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(courierRepository).findCourierPerformanceBetweenDates( + expectedStartDateTime, expectedEndDateTime); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..454c37d --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,7 @@ +# H2 Database Configuration for Tests +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect \ No newline at end of file From 5b6f9f449093ea50216f84a56907120a3217bb8f Mon Sep 17 00:00:00 2001 From: Mohamed Hassan Date: Sat, 17 May 2025 17:43:13 +0300 Subject: [PATCH 18/28] feat: Implement profit by category report endpoint (#8) * feat: Implement profit by category report endpoint * refactor: Update controller annotations and enhance Order model with JPA annotations * refactor: update imports for consistency * refactor: update ProfitReportController to use ProfitByCategory with DateRangeRequest * refactor: enhance FulfillmentReportController with improved error handling and validation for request parameters * refactor: enhance ProfitReportController with request validation and error handling for profit data retrieval and add integration tests * refactor: simplify FulfillmentReportController and ProfitReportController by removing redundant error handling and enhancing request validation * chore: add H2 database dependency for testing and refactor FulfillmentReportControllerTest and ProfitReportControllerTest to use TestRestTemplate for HTTP requests --------- Co-authored-by: Mohamed --- .../FulfillmentReportController.java | 38 +- .../controllers/ProfitReportController.java | 34 ++ .../FulfillmentPlaceToShipRequest.java | 2 +- .../FulfillmentShipToDeliverRequest.java | 2 +- .../api/dtos/profit/ProfitByCategory.java | 35 ++ .../profit/ProfitByCategoryProjection.java | 12 + .../repositories/SalesLineItemRepository.java | 19 +- .../services/ProfitAnalyticsService.java | 53 +++ .../validators/DateRangeValidator.java | 24 +- .../FulfillmentReportControllerTest.java | 419 +++++++++++------- .../ProfitReportControllerTest.java | 282 ++++++++++++ src/test/resources/application.properties | 1 - 12 files changed, 734 insertions(+), 187 deletions(-) create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java create mode 100644 src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java create mode 100644 src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java index f0dd97f..600f13e 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -1,15 +1,18 @@ package com.Podzilla.analytics.api.controllers; import org.springframework.http.ResponseEntity; +// import org.springframework.web.bind.MethodArgumentNotValidException; +// import org.springframework.web.bind.MissingServletRequestParameterException; +// import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +// import org.springframework.web.method.annotation. +// MethodArgumentTypeMismatchException; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment -.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; @@ -27,34 +30,37 @@ public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; - @Operation(summary = "Get average time from order placement to shipping", - description = "Returns the average time (in hours) between when" + @Operation( + summary = "Get average time from order placement to shipping", + description = "Returns the average time (in hours) between when" + " an order was placed and when it was shipped, grouped" - + " by the specified dimension") + + " by the specified dimension" + ) @GetMapping("/place-to-ship-time") public ResponseEntity> getPlaceToShipTime( @Valid @ModelAttribute final FulfillmentPlaceToShipRequest req) { - List reportData = fulfillmentAnalyticsService - .getPlaceToShipTimeResponse( + final List reportData = + fulfillmentAnalyticsService.getPlaceToShipTimeResponse( req.getStartDate(), req.getEndDate(), req.getGroupBy()); return ResponseEntity.ok(reportData); } - @Operation(summary = "Get average time from shipping to delivery", - description = "Returns the average time (in hours) between when" + + @Operation( + summary = "Get average time from shipping to delivery", + description = "Returns the average time (in hours) between when" + " an order was shipped and when it was delivered, grouped" - + " by the specified dimension") + + " by the specified dimension" + ) @GetMapping("/ship-to-deliver-time") public ResponseEntity> getShipToDeliverTime( @Valid @ModelAttribute final FulfillmentShipToDeliverRequest req) { - log.debug(req.toString()); - - List reportData = fulfillmentAnalyticsService - .getShipToDeliverTimeResponse( + final List reportData = + fulfillmentAnalyticsService.getShipToDeliverTimeResponse( req.getStartDate(), req.getEndDate(), req.getGroupBy()); diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java index bf4f6d9..cf0a7ae 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -1,14 +1,48 @@ package com.Podzilla.analytics.api.controllers; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + +import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; import com.Podzilla.analytics.services.ProfitAnalyticsService; +import io.swagger.v3.oas.annotations.Operation; +// import io.swagger.v3.oas.annotations.responses.ApiResponse; +// import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +/** + * REST controller for profit analytics operations. + * Provides endpoints to analyze revenue, cost, and profit metrics. + */ +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/profit-analytics") public class ProfitReportController { private final ProfitAnalyticsService profitAnalyticsService; + + + @Operation( + summary = "Get profit by product category", + description = "Returns the revenue, cost, and profit metrics " + + "grouped by product category") + @GetMapping("/by-category") + public ResponseEntity> getProfitByCategory( + @Valid @ModelAttribute final DateRangeRequest request) { + + List profitData = + profitAnalyticsService.getProfitByCategory( + request.getStartDate(), + request.getEndDate()); + return ResponseEntity.ok(profitData); + } } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java index 6af9a21..1a2bd8f 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentPlaceToShipRequest.java @@ -40,6 +40,6 @@ public enum PlaceToShipGroupBy { @NotNull(message = "groupBy is required") @Schema(description = "How to group the results (OVERALL, REGION, COURIER " + "depending on endpoint)", example = "OVERALL", required = true) - private PlaceToShipGroupBy groupBy = PlaceToShipGroupBy.OVERALL; + private PlaceToShipGroupBy groupBy; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java index bb368bc..c87b2aa 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/fulfillment/FulfillmentShipToDeliverRequest.java @@ -44,5 +44,5 @@ public enum ShipToDeliverGroupBy { @NotNull(message = "groupBy is required") @Schema(description = "How to group the results (OVERALL, REGION, COURIER " + "depending on endpoint)", example = "OVERALL", required = true) - private ShipToDeliverGroupBy groupBy = ShipToDeliverGroupBy.OVERALL; + private ShipToDeliverGroupBy groupBy; } diff --git a/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java b/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java new file mode 100644 index 0000000..f38927f --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/dtos/profit/ProfitByCategory.java @@ -0,0 +1,35 @@ +package com.Podzilla.analytics.api.dtos.profit; + +import java.math.BigDecimal; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProfitByCategory { + + @Schema(description = "Product category name", example = "Electronics") + private String category; + + @Schema(description = "Total revenue for the category in the given period", + example = "15000.50") + private BigDecimal totalRevenue; + + @Schema(description = "Total cost for the category in the given period", + example = "10000.25") + private BigDecimal totalCost; + + @Schema(description = "Gross profit (revenue - cost)", + example = "5000.25") + private BigDecimal grossProfit; + + @Schema(description = "Gross profit margin percentage", + example = "33.33") + private BigDecimal grossProfitMargin; +} diff --git a/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java new file mode 100644 index 0000000..23a8968 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/api/projections/profit/ProfitByCategoryProjection.java @@ -0,0 +1,12 @@ +package com.Podzilla.analytics.api.projections.profit; + +import java.math.BigDecimal; + +/** + * Projection interface for profit by category query results + */ +public interface ProfitByCategoryProjection { + String getCategory(); + BigDecimal getTotalRevenue(); + BigDecimal getTotalCost(); +} diff --git a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java index 4793b6e..b4b9ac8 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/SalesLineItemRepository.java @@ -1,9 +1,26 @@ package com.Podzilla.analytics.repositories; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import com.Podzilla.analytics.models.SalesLineItem; +import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; + +import java.time.LocalDateTime; +import java.util.List; public interface SalesLineItemRepository - extends JpaRepository { + extends JpaRepository { + @Query("SELECT sli.product.category as category, " + + "SUM(sli.quantity * sli.pricePerUnit) as totalRevenue, " + + "SUM(sli.quantity * sli.product.cost) as totalCost " + + "FROM SalesLineItem sli " + + "WHERE sli.order.orderPlacedTimestamp BETWEEN " + + ":startDate AND :endDate " + + "AND sli.order.status = 'COMPLETED' " + + "GROUP BY sli.product.category") + List findSalesByCategoryBetweenDates( + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java index be2fa59..85d3fb3 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -2,9 +2,62 @@ import org.springframework.stereotype.Service; +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; +import com.Podzilla.analytics.api.projections.profit.ProfitByCategoryProjection; +import com.Podzilla.analytics.repositories.SalesLineItemRepository; + import lombok.RequiredArgsConstructor; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Service public class ProfitAnalyticsService { + private final SalesLineItemRepository salesLineItemRepository; + // Precision constant for percentage calculations + private static final int PERCENTAGE_PRECISION = 4; + + public List getProfitByCategory( + final LocalDate startDate, + final LocalDate endDate) { + // Convert LocalDate to LocalDateTime for start of day and end of day + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List salesData = salesLineItemRepository + .findSalesByCategoryBetweenDates(startDateTime, endDateTime); + + return salesData.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + private ProfitByCategory convertToDTO( + final ProfitByCategoryProjection projection) { + BigDecimal totalRevenue = projection.getTotalRevenue(); + BigDecimal totalCost = projection.getTotalCost(); + BigDecimal grossProfit = totalRevenue.subtract(totalCost); + + BigDecimal grossProfitMargin = BigDecimal.ZERO; + if (totalRevenue.compareTo(BigDecimal.ZERO) > 0) { + grossProfitMargin = grossProfit + .divide(totalRevenue, PERCENTAGE_PRECISION, + RoundingMode.HALF_UP) + .multiply(new BigDecimal("100")); + } + + return ProfitByCategory.builder() + .category(projection.getCategory()) + .totalRevenue(totalRevenue) + .totalCost(totalCost) + .grossProfit(grossProfit) + .grossProfitMargin(grossProfitMargin) + .build(); + } } diff --git a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java index 079bd20..26fa7cb 100644 --- a/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java +++ b/src/main/java/com/Podzilla/analytics/validation/validators/DateRangeValidator.java @@ -1,19 +1,33 @@ package com.Podzilla.analytics.validation.validators; -import com.Podzilla.analytics.api.dtos.DateRangeRequest; +import java.time.LocalDate; +import java.lang.reflect.Method; + import com.Podzilla.analytics.validation.annotations.ValidDateRange; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public final class DateRangeValidator implements - ConstraintValidator { + ConstraintValidator { @Override - public boolean isValid(final DateRangeRequest request, + public boolean isValid(final Object value, final ConstraintValidatorContext context) { - if (request.getStartDate() == null || request.getEndDate() == null) { + if (value == null) { return true; } - return request.getEndDate().isAfter(request.getStartDate()); + + try { + Method getStartDate = value.getClass().getMethod("getStartDate"); + Method getEndDate = value.getClass().getMethod("getEndDate"); + LocalDate startDate = (LocalDate) getStartDate.invoke(value); + LocalDate endDate = (LocalDate) getEndDate.invoke(value); + if (startDate == null || endDate == null) { + return true; // Let @NotNull handle this + } + return !endDate.isBefore(startDate); + } catch (Exception e) { + return false; + } } } diff --git a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java index 61e3254..e76925d 100644 --- a/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java +++ b/src/test/java/com/Podzilla/analytics/controllers/FulfillmentReportControllerTest.java @@ -1,10 +1,7 @@ package com.Podzilla.analytics.controllers; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.math.BigDecimal; @@ -15,22 +12,33 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; -import com.Podzilla.analytics.api.controllers.FulfillmentReportController; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest; +import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentTimeResponse; -import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentPlaceToShipRequest.PlaceToShipGroupBy; import com.Podzilla.analytics.services.FulfillmentAnalyticsService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class FulfillmentReportControllerTest { - private FulfillmentReportController controller; + @Autowired + private TestRestTemplate restTemplate; + + @MockBean private FulfillmentAnalyticsService mockService; + private ObjectMapper objectMapper; private LocalDate startDate; private LocalDate endDate; private List overallTimeResponses; @@ -39,8 +47,8 @@ public class FulfillmentReportControllerTest { @BeforeEach public void setup() { - mockService = mock(FulfillmentAnalyticsService.class); - controller = new FulfillmentReportController(mockService); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); startDate = LocalDate.of(2024, 1, 1); endDate = LocalDate.of(2024, 1, 31); @@ -80,18 +88,26 @@ public void testGetPlaceToShipTime_Overall() { startDate, endDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(PlaceToShipGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); - assertEquals(BigDecimal.valueOf(24.5), response.getBody().get(0).getAverageDuration()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(1); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); + assertThat(response.getBody().get(0).getAverageDuration()).isEqualTo(BigDecimal.valueOf(24.5)); } @Test @@ -101,18 +117,26 @@ public void testGetPlaceToShipTime_ByRegion() { startDate, endDate, PlaceToShipGroupBy.REGION)) .thenReturn(regionTimeResponses); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.REGION.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(2); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("RegionID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("RegionID_2"); } @Test @@ -122,17 +146,24 @@ public void testGetShipToDeliverTime_Overall() { startDate, endDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); - assertEquals(ShipToDeliverGroupBy.OVERALL.toString(), response.getBody().get(0).getGroupByValue().toString()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -142,18 +173,25 @@ public void testGetShipToDeliverTime_ByRegion() { startDate, endDate, ShipToDeliverGroupBy.REGION)) .thenReturn(regionTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.REGION); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(regionTimeResponses, response.getBody()); - assertEquals("RegionID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("RegionID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.REGION.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("RegionID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("RegionID_2"); } @Test @@ -163,18 +201,25 @@ public void testGetShipToDeliverTime_ByCourier() { startDate, endDate, ShipToDeliverGroupBy.COURIER)) .thenReturn(courierTimeResponses); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.COURIER); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(courierTimeResponses, response.getBody()); - assertEquals("CourierID_1", response.getBody().get(0).getGroupByValue()); - assertEquals("CourierID_2", response.getBody().get(1).getGroupByValue()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.COURIER.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("CourierID_1"); + assertThat(response.getBody().get(1).getGroupByValue()).isEqualTo("CourierID_2"); } // Edge case tests @@ -186,17 +231,24 @@ public void testGetPlaceToShipTime_EmptyResponse() { startDate, endDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - startDate, endDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } @Test @@ -206,47 +258,63 @@ public void testGetShipToDeliverTime_EmptyResponse() { startDate, endDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } - // @Test - // public void testGetPlaceToShipTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = - // controller.getPlaceToShipTime(request); - - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } - - // @Test - // public void testGetShipToDeliverTime_InvalidGroupBy() { - // // Create request with invalid groupBy - // FulfillmentShipToDeliverRequest request = new - // FulfillmentShipToDeliverRequest( - // startDate, endDate, null); - - // // Execute the method - should return bad request due to validation error - // ResponseEntity> response = - // controller.getShipToDeliverTime(request); + @Test + public void testGetPlaceToShipTime_InvalidGroupBy() { + // Build URL without groupBy param + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } - // // Verify response - // assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); - // } + @Test + public void testGetShipToDeliverTime_InvalidGroupBy() { + // Build URL without groupBy param + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } @Test public void testGetPlaceToShipTime_SameDayRange() { @@ -258,16 +326,24 @@ public void testGetPlaceToShipTime_SameDayRange() { sameDate, sameDate, PlaceToShipGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request with same start and end date - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - sameDate, sameDate, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -280,16 +356,24 @@ public void testGetShipToDeliverTime_SameDayRange() { sameDate, sameDate, ShipToDeliverGroupBy.OVERALL)) .thenReturn(overallTimeResponses); - // Create request with same start and end date - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - sameDate, sameDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getShipToDeliverTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertEquals(overallTimeResponses, response.getBody()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getGroupByValue()).isEqualTo("OVERALL"); } @Test @@ -303,17 +387,24 @@ public void testGetPlaceToShipTime_FutureDates() { futureStart, futureEnd, PlaceToShipGroupBy.OVERALL)) .thenReturn(Collections.emptyList()); - // Create request with future dates - FulfillmentPlaceToShipRequest request = new FulfillmentPlaceToShipRequest( - futureStart, futureEnd, PlaceToShipGroupBy.OVERALL); - - // Execute the method - ResponseEntity> response = controller.getPlaceToShipTime(request); - - // Verify response - assertEquals(HttpStatus.OK, response.getStatusCode()); - assertNotNull(response.getBody()); - assertTrue(response.getBody().isEmpty()); + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/place-to-ship-time") + .queryParam("startDate", futureStart.toString()) + .queryParam("endDate", futureEnd.toString()) + .queryParam("groupBy", PlaceToShipGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); } @Test @@ -323,17 +414,21 @@ public void testGetShipToDeliverTime_ServiceException() { any(), any(), any())) .thenThrow(new RuntimeException("Service error")); - // Create request - FulfillmentShipToDeliverRequest request = new FulfillmentShipToDeliverRequest( - startDate, endDate, ShipToDeliverGroupBy.OVERALL); - - // Execute the method - controller should handle exception - // Note: Actual behavior depends on how controller handles exceptions - // This might need adjustment based on actual implementation - try { - controller.getShipToDeliverTime(request); - } catch (RuntimeException e) { - assertEquals("Service error", e.getMessage()); - } + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/fulfillment-analytics/ship-to-deliver-time") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .queryParam("groupBy", ShipToDeliverGroupBy.OVERALL.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } } \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java new file mode 100644 index 0000000..6c1f50c --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/ProfitReportControllerTest.java @@ -0,0 +1,282 @@ +package com.Podzilla.analytics.controllers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.util.UriComponentsBuilder; + +import com.Podzilla.analytics.api.dtos.profit.ProfitByCategory; +import com.Podzilla.analytics.services.ProfitAnalyticsService; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class ProfitReportControllerTest { + + @Autowired + private TestRestTemplate restTemplate; + + @MockBean + private ProfitAnalyticsService mockService; + + private ObjectMapper objectMapper; + private LocalDate startDate; + private LocalDate endDate; + private List profitData; + + @BeforeEach + public void setup() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + startDate = LocalDate.of(2024, 1, 1); + endDate = LocalDate.of(2024, 1, 31); + + // Setup test data + profitData = Arrays.asList( + ProfitByCategory.builder() + .category("Electronics") + .totalRevenue(BigDecimal.valueOf(10000.50)) + .totalCost(BigDecimal.valueOf(7500.25)) + .grossProfit(BigDecimal.valueOf(2500.25)) + .grossProfitMargin(BigDecimal.valueOf(25.00)) + .build(), + ProfitByCategory.builder() + .category("Clothing") + .totalRevenue(BigDecimal.valueOf(5500.75)) + .totalCost(BigDecimal.valueOf(3000.50)) + .grossProfit(BigDecimal.valueOf(2500.25)) + .grossProfitMargin(BigDecimal.valueOf(45.45)) + .build()); + } + + @Test + public void testGetProfitByCategory_Success() { + // Configure mock service + when(mockService.getProfitByCategory(startDate, endDate)) + .thenReturn(profitData); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().size()).isEqualTo(2); + assertThat(response.getBody().get(0).getCategory()).isEqualTo("Electronics"); + assertThat(response.getBody().get(0).getTotalRevenue()).isEqualTo(BigDecimal.valueOf(10000.50)); + assertThat(response.getBody().get(0).getGrossProfit()).isEqualTo(BigDecimal.valueOf(2500.25)); + assertThat(response.getBody().get(1).getCategory()).isEqualTo("Clothing"); + assertThat(response.getBody().get(1).getGrossProfitMargin()).isEqualTo(BigDecimal.valueOf(45.45)); + } + + @Test + public void testGetProfitByCategory_EmptyResult() { + // Configure mock service to return empty list + when(mockService.getProfitByCategory(startDate, endDate)) + .thenReturn(Collections.emptyList()); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); + } + + @Test + public void testGetProfitByCategory_MissingStartDate() { + // Build URL with missing required parameter + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_MissingEndDate() { + // Build URL with missing required parameter + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_InvalidDateFormat() { + // Build URL with invalid date format + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", "2024-01-01") + .queryParam("endDate", "invalid-date") + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_StartDateAfterEndDate() { + // Set up dates where start is after end + LocalDate invalidStart = LocalDate.of(2024, 2, 1); + LocalDate invalidEnd = LocalDate.of(2024, 1, 1); + + // Build URL with invalid date range + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", invalidStart.toString()) + .queryParam("endDate", invalidEnd.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + public void testGetProfitByCategory_FutureDateRange() { + // Set up future dates + LocalDate futureStart = LocalDate.now().plusDays(1); + LocalDate futureEnd = LocalDate.now().plusDays(30); + + // Configure mock service - should return empty data for future dates + when(mockService.getProfitByCategory(futureStart, futureEnd)) + .thenReturn(Collections.emptyList()); + + // Build URL with future date range + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", futureStart.toString()) + .queryParam("endDate", futureEnd.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isEmpty(); + } + + @Test + public void testGetProfitByCategory_SameDayRange() { + // Test same start and end date + LocalDate sameDate = LocalDate.of(2024, 1, 1); + + // Configure mock service + when(mockService.getProfitByCategory(sameDate, sameDate)) + .thenReturn(profitData); + + // Build URL with same day for start and end + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", sameDate.toString()) + .queryParam("endDate", sameDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity> response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + new ParameterizedTypeReference>() {}); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().get(0).getCategory()).isEqualTo("Electronics"); + } + + @Test + public void testGetProfitByCategory_ServiceException() { + // Configure mock service to throw exception + when(mockService.getProfitByCategory(any(), any())) + .thenThrow(new RuntimeException("Service error")); + + // Build URL with query parameters + String url = UriComponentsBuilder.fromPath("/profit-analytics/by-category") + .queryParam("startDate", startDate.toString()) + .queryParam("endDate", endDate.toString()) + .toUriString(); + + // Execute request + ResponseEntity response = restTemplate.exchange( + url, + HttpMethod.GET, + null, + String.class); + + // Verify + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index f03dd4c..0139a73 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,7 +3,6 @@ spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL spring.datasource.username=sa spring.datasource.password= spring.datasource.driver-class-name=org.h2.Driver - # JPA/Hibernate Configuration spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true From 44bf8aca70afe02c0ad7301a27015dfd952cd2fb Mon Sep 17 00:00:00 2001 From: Ahmad Hoseiny Date: Sun, 18 May 2025 22:41:44 +0300 Subject: [PATCH 19/28] Communication design patterns (#23) * refactor: added manual builder Required * added builder manually to all models Required * feat: added configuration for rabbit listeners employed command design pattern * refactor: changes ids from long to uuid added first command, fixed validation issues, and tests need fixing * feat: added listeners for users and inventory events * refactor: update entity IDs to UUID and enhance database seeding * WIP: Save progress before merge * fix: fix lint issue * feat: added order placed event * feat: added update order events --------- Co-authored-by: Mohamed Hassan Co-authored-by: Mohamed --- pom.xml | 2 +- .../controllers/CustomerReportController.java | 3 +- .../FulfillmentReportController.java | 8 +- .../InventoryReportController.java | 3 +- .../controllers/ProfitReportController.java | 2 - .../controllers/RabbitTesterController.java | 163 ++ .../api/dtos/DateRangePaginationRequest.java | 24 +- .../analytics/api/dtos/DateRangeRequest.java | 2 +- .../analytics/api/dtos/IDateRangeRequest.java | 8 + .../api/dtos/IPaginationRequest.java | 6 + .../analytics/api/dtos/PaginationRequest.java | 6 +- .../courier/CourierAverageRatingResponse.java | 41 +- .../courier/CourierDeliveryCountResponse.java | 3 +- .../CourierPerformanceReportResponse.java | 4 +- .../courier/CourierSuccessRateResponse.java | 3 +- .../CustomersTopSpendersResponse.java | 3 +- .../FulfillmentPlaceToShipRequest.java | 3 +- .../FulfillmentShipToDeliverRequest.java | 3 +- .../inventory/LowStockProductResponse.java | 3 +- .../api/dtos/order/OrderRegionResponse.java | 7 +- .../api/dtos/product/TopSellerRequest.java | 4 +- .../api/dtos/product/TopSellerResponse.java | 53 +- .../revenue/RevenueByCategoryRequest.java | 3 +- .../dtos/revenue/RevenueSummaryRequest.java | 3 +- .../dtos/revenue/RevenueSummaryResponse.java | 28 +- .../courier/CourierPerformanceProjection.java | 3 +- .../CustomersTopSpendersProjection.java | 3 +- .../inventory/LowStockProductProjection.java | 4 +- .../order/OrderRegionProjection.java | 3 +- .../product/TopSellingProductProjection.java | 3 +- .../analytics/config/DatabaseSeeder.java | 111 +- .../messaging/AnalyticsRabbitListener.java | 39 + .../messaging/InvokerDispatcher.java | 40 + .../messaging/InvokerDispatcherConfig.java | 115 ++ .../analytics/messaging/RabbitListener.java | 11 - .../analytics/messaging/commands/Command.java | 5 + .../messaging/commands/CommandFactory.java | 200 +++ .../inventory/CreateProductCommand.java | 30 + .../MarkOrderAsFailedToFulfillCommand.java | 25 + .../inventory/UpdateInventoryCommand.java | 25 + .../order/AssignCourierToOrderCommand.java | 17 + .../commands/order/CancelOrderCommand.java | 25 + .../order/MarkOrderAsDeliveredCommand.java | 27 + .../MarkOrderAsFailedToDeliverCommand.java | 25 + .../MarkOrderAsOutForDeliveryCommand.java | 20 + .../commands/order/PlaceOrderCommand.java | 43 + .../commands/user/RegisterCourierCommand.java | 22 + .../user/RegisterCustomerCommand.java | 23 + .../analytics/messaging/invokers/Invoker.java | 5 + .../messaging/invokers/InvokerFactory.java | 35 + .../inventory/InventoryUpdatedInvoker.java | 30 + .../OrderFulfillmentFailedInvoker.java | 29 + .../inventory/ProductCreatedInvoker.java | 31 + .../order/OrderAssignedToCourierInvoker.java | 28 + .../invokers/order/OrderCancelledInvoker.java | 28 + .../invokers/order/OrderDeliveredInvoker.java | 30 + .../order/OrderDeliveryFailedInvoker.java | 30 + .../order/OrderOutForDeliveryInvoker.java | 28 + .../invokers/order/OrderPlacedInvoker.java | 33 + .../user/CourierRegisteredInvoker.java | 28 + .../user/CustomerRegisteredInvoker.java | 29 + .../Podzilla/analytics/models/Courier.java | 52 +- .../Podzilla/analytics/models/Customer.java | 45 +- .../analytics/models/InventorySnapshot.java | 34 - .../com/Podzilla/analytics/models/Order.java | 164 +- .../Podzilla/analytics/models/OrderItem.java | 87 ++ .../Podzilla/analytics/models/Product.java | 51 +- .../analytics/models/ProductSnapshot.java | 73 + .../com/Podzilla/analytics/models/Region.java | 50 +- .../analytics/models/SalesLineItem.java | 37 - .../repositories/CourierRepository.java | 20 +- .../repositories/CustomerRepository.java | 27 +- .../InventorySnapshotRepository.java | 39 - ...pository.java => OrderItemRepository.java} | 21 +- .../repositories/OrderRepository.java | 198 +-- .../repositories/ProductRepository.java | 48 +- .../ProductSnapshotRepository.java | 40 + .../repositories/RegionRepository.java | 3 +- .../services/CourierAnalyticsService.java | 122 +- .../services/CustomerAnalyticsService.java | 29 +- .../services/FulfillmentAnalyticsService.java | 9 +- .../services/InventoryAnalyticsService.java | 34 +- .../services/OrderAnalyticsService.java | 182 ++- .../analytics/services/OrderItemService.java | 55 + .../services/ProductAnalyticsService.java | 24 +- .../services/ProfitAnalyticsService.java | 6 +- .../analytics/services/RegionService.java | 34 + .../analytics/util/DatetimeFormatter.java | 10 + .../analytics/util/StringToUUIDParser.java | 14 + .../annotations/ValidPagination.java | 25 + .../validators/DateRangeValidator.java | 24 +- .../validators/PaginationValidator.java | 17 + .../CourierAnalyticsControllerTest.java | 27 +- .../FulfillmentReportControllerTest.java | 118 +- .../ProfitReportControllerTest.java | 15 +- .../RevenueReportControllerTest.java | 236 ++- ...roductAnalyticsServiceIntegrationTest.java | 1333 +++++++++-------- .../RevenueReportServiceIntegrationTest.java | 319 ++-- .../services/CourierAnalyticsServiceTest.java | 58 +- .../services/ProductAnalyticsServiceTest.java | 87 +- .../services/RevenueReportServiceTest.java | 74 +- 101 files changed, 3804 insertions(+), 1608 deletions(-) create mode 100644 src/main/java/com/Podzilla/analytics/api/controllers/RabbitTesterController.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/IDateRangeRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/api/dtos/IPaginationRequest.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcher.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/InvokerDispatcherConfig.java delete mode 100644 src/main/java/com/Podzilla/analytics/messaging/RabbitListener.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/Command.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/CommandFactory.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/inventory/CreateProductCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/inventory/MarkOrderAsFailedToFulfillCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/inventory/UpdateInventoryCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/AssignCourierToOrderCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/CancelOrderCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsDeliveredCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsFailedToDeliverCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/MarkOrderAsOutForDeliveryCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/order/PlaceOrderCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCourierCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/commands/user/RegisterCustomerCommand.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/Invoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/InvokerFactory.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/InventoryUpdatedInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/OrderFulfillmentFailedInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/inventory/ProductCreatedInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderAssignedToCourierInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderCancelledInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveredInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderOutForDeliveryInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderPlacedInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/user/CourierRegisteredInvoker.java create mode 100644 src/main/java/com/Podzilla/analytics/messaging/invokers/user/CustomerRegisteredInvoker.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/InventorySnapshot.java create mode 100644 src/main/java/com/Podzilla/analytics/models/OrderItem.java create mode 100644 src/main/java/com/Podzilla/analytics/models/ProductSnapshot.java delete mode 100644 src/main/java/com/Podzilla/analytics/models/SalesLineItem.java delete mode 100644 src/main/java/com/Podzilla/analytics/repositories/InventorySnapshotRepository.java rename src/main/java/com/Podzilla/analytics/repositories/{SalesLineItemRepository.java => OrderItemRepository.java} (52%) create mode 100644 src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java create mode 100644 src/main/java/com/Podzilla/analytics/services/OrderItemService.java create mode 100644 src/main/java/com/Podzilla/analytics/services/RegionService.java create mode 100644 src/main/java/com/Podzilla/analytics/util/StringToUUIDParser.java create mode 100644 src/main/java/com/Podzilla/analytics/validation/annotations/ValidPagination.java create mode 100644 src/main/java/com/Podzilla/analytics/validation/validators/PaginationValidator.java 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; + } }; } } From 14977175c0e31cd7759f94077b52d28f26bec6fd Mon Sep 17 00:00:00 2001 From: Ahmad Hoseiny Date: Sun, 18 May 2025 23:37:52 +0300 Subject: [PATCH 20/28] fix: fix rabbit issue (#25) --- .../messaging/AnalyticsRabbitListener.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java index 7642c35..5208367 100644 --- a/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java +++ b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java @@ -1,7 +1,7 @@ package com.Podzilla.analytics.messaging; -// import org.springframework.amqp.rabbit.annotation.RabbitListener; -// import com.podzilla.mq.EventsConstants; +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; @@ -16,23 +16,23 @@ public class AnalyticsRabbitListener { @Autowired private InvokerDispatcher dispatcher; - // @RabbitListener( - // queues = EventsConstants.ANALYTICS_USER_EVENT_QUEUE - // ) + @RabbitListener( + queues = EventsConstants.ANALYTICS_USER_EVENT_QUEUE + ) public void handleUserEvents(final BaseEvent userEvent) { dispatcher.dispatch(userEvent); } - // @RabbitListener( - // queues = EventsConstants.ANALYTICS_ORDER_EVENT_QUEUE - // ) + @RabbitListener( + queues = EventsConstants.ANALYTICS_ORDER_EVENT_QUEUE + ) public void handleOrderEvents(final BaseEvent orderEvent) { dispatcher.dispatch(orderEvent); } - // @RabbitListener( - // queues = EventsConstants.ANALYTICS_INVENTORY_EVENT_QUEUE - // ) + @RabbitListener( + queues = EventsConstants.ANALYTICS_INVENTORY_EVENT_QUEUE + ) public void handleInventoryEvents(final BaseEvent inventoryEvent) { dispatcher.dispatch(inventoryEvent); } From ca60768d17cb62e5af5bec4b4486e74e08a0c28c Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Mon, 19 May 2025 00:06:41 +0300 Subject: [PATCH 21/28] Fix revenue (#26) * refactor: added manual builder Required * added builder manually to all models Required * feat: added configuration for rabbit listeners employed command design pattern * refactor: changes ids from long to uuid added first command, fixed validation issues, and tests need fixing * feat: added listeners for users and inventory events * refactor: update entity IDs to UUID and enhance database seeding * WIP: Save progress before merge * fix: fix lint issue * feat: added order placed event * fix: use Local date time instead of local date in revenue queries --------- Co-authored-by: AhmadHoseiny Co-authored-by: Mohamed Hassan --- .../analytics/config/DatabaseSeeder.java | 5 ++- .../Podzilla/analytics/models/Product.java | 19 ++++++++-- .../repositories/OrderRepository.java | 9 +++-- .../repositories/ProductRepository.java | 36 ++++++++++++------- .../services/RevenueReportService.java | 18 +++++++--- .../services/RevenueReportServiceTest.java | 10 ++++-- 6 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 0015048..13a8b9d 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -257,7 +257,7 @@ private void seedOrders( .id(UUID.randomUUID()) .customer(customers.get(1)).courier(couriers.get(1)) .region(regions.get(1)) - .status(Order.OrderStatus.SHIPPED) + .status(Order.OrderStatus.DELIVERED) .orderPlacedTimestamp(placed2) .shippedTimestamp(placed2.plusDays(ORDER_2_SHIP_DAYS) .plusHours(ORDER_2_SHIP_HOURS)) @@ -283,9 +283,8 @@ private void seedOrders( .id(UUID.randomUUID()) .customer(customers.get(0)).courier(couriers.get(0)) .region(regions.get(2)) - .status(Order.OrderStatus.DELIVERY_FAILED) .orderPlacedTimestamp(placed3) - .status(Order.OrderStatus.DELIVERY_FAILED) + .status(Order.OrderStatus.DELIVERED) .orderPlacedTimestamp(placed3) .shippedTimestamp(placed3.plusHours(ORDER_3_SHIP_HOURS)) .deliveredTimestamp(null) diff --git a/src/main/java/com/Podzilla/analytics/models/Product.java b/src/main/java/com/Podzilla/analytics/models/Product.java index fc6223e..bda85d5 100644 --- a/src/main/java/com/Podzilla/analytics/models/Product.java +++ b/src/main/java/com/Podzilla/analytics/models/Product.java @@ -2,14 +2,18 @@ import java.math.BigDecimal; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.UUID; +import java.util.List; + @Entity @Table(name = "products") @Data @@ -23,6 +27,9 @@ public class Product { private BigDecimal cost; private int lowStockThreshold; + @OneToMany(mappedBy = "product", cascade = CascadeType.ALL) + private List orderItems; + public static Builder builder() { return new Builder(); } @@ -33,8 +40,10 @@ public static class Builder { private String category; private BigDecimal cost; private int lowStockThreshold; + private List orderItems; - public Builder() { } + public Builder() { + } public Builder id(final UUID id) { this.id = id; @@ -61,8 +70,14 @@ public Builder lowStockThreshold(final int lowStockThreshold) { return this; } + public Builder orderItems(final List orderItems) { + this.orderItems = orderItems; + return this; + } + public Product build() { - return new Product(id, name, category, cost, lowStockThreshold); + return new Product( + id, name, category, cost, lowStockThreshold, orderItems); } } } diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index c3e38da..b85869a 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -1,6 +1,5 @@ package com.Podzilla.analytics.repositories; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -149,8 +148,8 @@ OrderFailureRateProjection calculateFailureRate( + "GROUP BY period " + "ORDER BY totalRevenue DESC") List findRevenueSummaryByPeriod( - @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate, + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate, @Param("reportPeriod") String reportPeriod); @Query("SELECT p.category AS category, " @@ -164,6 +163,6 @@ List findRevenueSummaryByPeriod( + "GROUP BY p.category " + "ORDER BY totalRevenue DESC") List findRevenueByCategory( - @Param("startDate") LocalDate startDate, - @Param("endDate") LocalDate endDate); + @Param("startDate") LocalDateTime startDate, + @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java index fa50cb5..f4037b1 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductRepository.java @@ -10,28 +10,38 @@ import com.Podzilla.analytics.api.projections.product.TopSellingProductProjection; import com.Podzilla.analytics.models.Product; import java.util.UUID; + public interface ProductRepository extends JpaRepository { @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' " + + "COALESCE(SUM(CASE WHEN o.finalStatusTimestamp >= :startDate " + + "AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' THEN oi.quantity * oi.pricePerUnit " + + "ELSE 0 END), 0) " + + "AS totalRevenue, " + + "COALESCE(SUM(CASE WHEN o.finalStatusTimestamp >= :startDate " + + "AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' THEN oi.quantity ELSE 0 END), 0) " + + "AS totalUnits " + + "FROM Product p " + + "LEFT JOIN p.orderItems oi " + + "LEFT JOIN oi.order o " + "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") + + "THEN COALESCE(SUM(CASE WHEN o.finalStatusTimestamp >= " + + ":startDate AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' THEN oi.quantity * oi.pricePerUnit " + + "ELSE 0 END), 0) " + + "ELSE COALESCE(SUM(CASE WHEN o.finalStatusTimestamp >= " + + ":startDate AND o.finalStatusTimestamp < :endDate " + + "AND o.status = 'DELIVERED' THEN oi.quantity ELSE 0 END), 0) " + + "END DESC " + + "LIMIT :limit") List findTopSellers( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate, @Param("limit") Integer limit, - @Param("sortBy") String sortBy // Pass the enum name as a String - ); + @Param("sortBy") String sortBy); } diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java index 222a8e2..0de6380 100644 --- a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -1,6 +1,8 @@ package com.Podzilla.analytics.services; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @@ -25,9 +27,13 @@ public List getRevenueSummary( final LocalDate endDate, final String periodString) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + final List revenueData = orderRepository - .findRevenueSummaryByPeriod(startDate, - endDate, periodString); + .findRevenueSummaryByPeriod( + startDateTime, + endDateTime, periodString); final List summaryList = new ArrayList<>(); @@ -55,9 +61,13 @@ public List getRevenueSummary( public List getRevenueByCategory( final LocalDate startDate, final LocalDate endDate) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + final List queryResults = orderRepository - .findRevenueByCategory(startDate, - endDate); + .findRevenueByCategory( + startDateTime, + endDateTime); final List summaryList = new ArrayList<>(); diff --git a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java index f007740..8cd3c6f 100644 --- a/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java +++ b/src/test/java/com/Podzilla/analytics/services/RevenueReportServiceTest.java @@ -7,6 +7,7 @@ import java.math.BigDecimal; import java.time.LocalDate; +import java.time.LocalTime; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -52,7 +53,8 @@ void getRevenueSummary_WithValidData_ShouldReturnCorrectSummary() { 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"))) + when(orderRepository.findRevenueSummaryByPeriod(eq(startDate.atStartOfDay()), eq(endDate.atTime(LocalTime.MAX)), + eq("MONTHLY"))) .thenReturn(projections); // Act @@ -120,7 +122,8 @@ void getRevenueByCategory_WithValidData_ShouldReturnCorrectCategories() { categoryProjection("Books", new BigDecimal("3000.00")), categoryProjection("Electronics", new BigDecimal("5000.00"))); - when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + when(orderRepository.findRevenueByCategory(eq(startDate.atStartOfDay()), eq(endDate.atTime( + LocalTime.MAX)))) .thenReturn(projections);// Act List result = revenueReportService.getRevenueByCategory(startDate, endDate); @@ -167,7 +170,8 @@ public BigDecimal getTotalRevenue() { } }); - when(orderRepository.findRevenueByCategory(eq(startDate), eq(endDate))) + when(orderRepository.findRevenueByCategory(eq(startDate.atStartOfDay()), eq(endDate.atTime( + LocalTime.MAX)))) .thenReturn(projections); // Act From ae6cb5aa54494b7778b90588fc477ae1d30d0dc9 Mon Sep 17 00:00:00 2001 From: Mohamed Khaled <113786472+Mohamed-Khaled308@users.noreply.github.com> Date: Mon, 19 May 2025 15:32:17 +0300 Subject: [PATCH 22/28] Fix fix (#27) * refactor: added manual builder Required * added builder manually to all models Required * feat: added configuration for rabbit listeners employed command design pattern * refactor: changes ids from long to uuid added first command, fixed validation issues, and tests need fixing * feat: added listeners for users and inventory events * refactor: update entity IDs to UUID and enhance database seeding * WIP: Save progress before merge * fix: fix lint issue * chore: add logging for controller and services * fix: fix some queries --------- Co-authored-by: AhmadHoseiny Co-authored-by: Mohamed Hassan --- docker-compose.yml | 9 +- .../controllers/CourierReportController.java | 11 + .../controllers/CustomerReportController.java | 4 + .../FulfillmentReportController.java | 31 +- .../InventoryReportController.java | 10 +- .../controllers/OrderReportController.java | 59 ++-- .../controllers/ProductReportController.java | 6 +- .../controllers/ProfitReportController.java | 15 +- .../controllers/RevenueReportController.java | 6 + .../api/dtos/DateRangePaginationRequest.java | 4 +- .../analytics/api/dtos/DateRangeRequest.java | 4 +- .../analytics/api/dtos/PaginationRequest.java | 4 +- .../api/dtos/order/OrderRegionResponse.java | 5 - .../order/OrderRegionProjection.java | 2 - .../analytics/config/DatabaseSeeder.java | 4 +- .../messaging/AnalyticsRabbitListener.java | 9 +- .../repositories/OrderRepository.java | 9 +- .../services/CourierAnalyticsService.java | 132 +++---- .../services/CustomerAnalyticsService.java | 74 ++-- .../services/FulfillmentAnalyticsService.java | 20 +- .../services/InventoryAnalyticsService.java | 43 ++- .../services/OrderAnalyticsService.java | 327 +++++++++--------- .../analytics/services/OrderItemService.java | 54 ++- .../services/ProductAnalyticsService.java | 18 +- .../services/ProfitAnalyticsService.java | 7 + .../analytics/services/RegionService.java | 31 +- .../services/RevenueReportService.java | 9 + src/main/resources/application.properties | 2 +- src/main/resources/static/favicon.ico | 0 29 files changed, 516 insertions(+), 393 deletions(-) create mode 100644 src/main/resources/static/favicon.ico diff --git a/docker-compose.yml b/docker-compose.yml index b44bc1a..816f9ca 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,23 +4,26 @@ services: build: . container_name: analytics-app ports: - - "8080:8080" + - "8083:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/analytics_db_dev SPRING_DATASOURCE_USERNAME: postgres SPRING_DATASOURCE_PASSWORD: 123 - SPRING_RABBITMQ_HOST: rabbitmq # RabbitMQ container name or use its IP if needed + SPRING_RABBITMQ_HOST: host.docker.internal SPRING_RABBITMQ_PORT: 5672 SPRING_RABBITMQ_USERNAME: guest SPRING_RABBITMQ_PASSWORD: guest depends_on: - db + extra_hosts: + - "host.docker.internal:host-gateway" + db: image: postgres container_name: analytics-db ports: - - "5432:5432" + - "5435:5432" environment: POSTGRES_DB: analytics_db_dev POSTGRES_USER: postgres diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java index 2fa9cc5..cf35201 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CourierReportController.java @@ -20,12 +20,14 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Tag(name = "Courier Reports", description = "Endpoints for courier" + " analytics and performance metrics") @RestController @RequestMapping("/courier-analytics") @RequiredArgsConstructor +@Slf4j public final class CourierReportController { private final CourierAnalyticsService courierAnalyticsService; @@ -37,6 +39,8 @@ public final class CourierReportController { @GetMapping("/delivery-counts") public ResponseEntity> getDeliveryCounts( @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /courier-analytics/delivery-counts " + + "with attributes: {}", dateRange); List counts = courierAnalyticsService .getCourierDeliveryCounts(dateRange.getStartDate(), dateRange.getEndDate()); @@ -48,6 +52,9 @@ public ResponseEntity> getDeliveryCounts( @GetMapping("/success-rate") public ResponseEntity> getSuccessRate( @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /courier-analytics/success-rate " + + "with attributes: {}", + dateRange); List rates = courierAnalyticsService .getCourierSuccessRate(dateRange.getStartDate(), dateRange.getEndDate()); @@ -59,6 +66,8 @@ public ResponseEntity> getSuccessRate( @GetMapping("/average-rating") public ResponseEntity> getAverageRating( @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /courier-analytics/average-rating " + + "with attributes: {}", dateRange); List ratings = courierAnalyticsService .getCourierAverageRating(dateRange.getStartDate(), dateRange.getEndDate()); @@ -70,6 +79,8 @@ public ResponseEntity> getAverageRating( @GetMapping("/performance-report") public ResponseEntity> getReport( @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /courier-analytics/performance-report " + + "with attributes: {}", dateRange); List report = courierAnalyticsService .getCourierPerformanceReport(dateRange.getStartDate(), dateRange.getEndDate()); 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 8c167f7..3b3055b 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/CustomerReportController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import java.util.List; @@ -21,6 +22,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/customer-analytics") +@Slf4j public class CustomerReportController { private final CustomerAnalyticsService customerAnalyticsService; @@ -30,6 +32,8 @@ public class CustomerReportController { @GetMapping("/top-spenders") public List getTopSpenders( @Valid @ModelAttribute final DateRangePaginationRequest request) { + log.info("Request on: /customer-analytics/top-spenders " + + "with attributes: {}", request); return customerAnalyticsService.getTopSpenders( request.getStartDate(), request.getEndDate(), 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 b7fc570..1f452d1 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/FulfillmentReportController.java @@ -25,16 +25,17 @@ public class FulfillmentReportController { private final FulfillmentAnalyticsService fulfillmentAnalyticsService; - @Operation( - summary = "Get average time from order placement to shipping", - description = "Returns the average time (in hours) between when" + @Operation(summary = "Get average time from order placement to shipping", + description = "Returns the average time (in hours) between when" + " an order was placed and when it was shipped, grouped" - + " by the specified dimension" - ) + + " by the specified dimension") @GetMapping("/place-to-ship-time") public ResponseEntity> getPlaceToShipTime( @Valid @ModelAttribute final FulfillmentPlaceToShipRequest req) { + log.info("Request on: /fulfillment-analytics/place-to-ship-time " + + "with attributes: {}", req); + final List reportData = fulfillmentAnalyticsService.getPlaceToShipTimeResponse( req.getStartDate(), @@ -44,22 +45,22 @@ public ResponseEntity> getPlaceToShipTime( return ResponseEntity.ok(reportData); } - - @Operation( - summary = "Get average time from shipping to delivery", - description = "Returns the average time (in hours) between when" + @Operation(summary = "Get average time from shipping to delivery", + description = "Returns the average time (in hours) between when" + " an order was shipped and when it was delivered, grouped" - + " by the specified dimension" - ) + + " by the specified dimension") @GetMapping("/ship-to-deliver-time") public ResponseEntity> getShipToDeliverTime( @Valid @ModelAttribute final FulfillmentShipToDeliverRequest req) { + log.info("Request on: /fulfillment-analytics/ship-to-deliver-time " + + "with attributes: {}", req); + final List reportData = - fulfillmentAnalyticsService.getShipToDeliverTimeResponse( - req.getStartDate(), - req.getEndDate(), - req.getGroupBy()); + fulfillmentAnalyticsService.getShipToDeliverTimeResponse( + req.getStartDate(), + req.getEndDate(), + 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 8820458..31a6858 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/InventoryReportController.java @@ -13,6 +13,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -23,6 +24,7 @@ @RequiredArgsConstructor @RestController @RequestMapping("/inventory-analytics") +@Slf4j public class InventoryReportController { private final InventoryAnalyticsService inventoryAnalyticsService; @@ -31,8 +33,10 @@ public class InventoryReportController { + "the total value of inventory " + "grouped by product categories") @GetMapping("/value/by-category") - public List - getInventoryValueByCategory() { + public List getInventoryValueByCategory( + + ) { + log.info("Request on: /inventory-analytics/value/by-category"); return inventoryAnalyticsService.getInventoryValueByCategory(); } @@ -41,6 +45,8 @@ public class InventoryReportController { @GetMapping("/low-stock") public Page getLowStockProducts( @Valid @ModelAttribute final PaginationRequest paginationRequest) { + log.info("Request on: /inventory-analytics/low-stock" + + " with attributes: {}", paginationRequest); return inventoryAnalyticsService.getLowStockProducts( paginationRequest.getPage(), paginationRequest.getSize()); diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java index 5ba95bf..6e843ae 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/OrderReportController.java @@ -10,6 +10,7 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import java.util.List; @@ -19,7 +20,7 @@ import com.Podzilla.analytics.api.dtos.order.OrderRegionResponse; import com.Podzilla.analytics.api.dtos.order.OrderStatusResponse; - +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/order-analytics") @@ -27,48 +28,48 @@ public class OrderReportController { private final OrderAnalyticsService orderAnalyticsService; @Operation(summary = "Get order counts and revenue by region", - description = "Returns the total number of orders" - + "placed in each region and their corresponding average revenue") + description = "Returns the total number of orders" + + "placed in each region and their corresponding average revenue") @GetMapping("/by-region") public ResponseEntity> getOrdersByRegion( - @Valid @ModelAttribute final DateRangeRequest dateRange - ) { - List ordersByRegion = - orderAnalyticsService.getOrdersByRegion( - dateRange.getStartDate(), - dateRange.getEndDate() - ); + @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /order-analytics/by-region" + + " with attributes: {}", dateRange); + List ordersByRegion = orderAnalyticsService + .getOrdersByRegion( + dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(ordersByRegion); } @Operation(summary = "Get order status counts", - description = "Returns the total number of orders" - + "in each status (e.g., COMPLETED, SHIPPED, FAILED)") + description = "Returns the total number of orders" + + "in each status (e.g., COMPLETED, SHIPPED, FAILED)") @GetMapping("/status-counts") public ResponseEntity> getOrdersStatusCounts( - @Valid @ModelAttribute final DateRangeRequest dateRange - ) { - List orderStatusCounts = - orderAnalyticsService.getOrdersStatusCounts( - dateRange.getStartDate(), - dateRange.getEndDate() - ); + @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /order-analytics/status-counts" + + " with attributes: {}", dateRange); + List orderStatusCounts = orderAnalyticsService + .getOrdersStatusCounts( + dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(orderStatusCounts); } @Operation(summary = "Get order failures", - description = "Returns the percentage of failed orders" - + "and a list of the failure reasons" - + "with their corresponding frequency") + description = "Returns the percentage of failed orders" + + "and a list of the failure reasons" + + "with their corresponding frequency") @GetMapping("/failures") public ResponseEntity getOrdersFailures( - @Valid @ModelAttribute final DateRangeRequest dateRange - ) { - OrderFailureResponse orderFailures = - orderAnalyticsService.getOrdersFailures( - dateRange.getStartDate(), - dateRange.getEndDate() - ); + @Valid @ModelAttribute final DateRangeRequest dateRange) { + log.info("Request on: /order-analytics/failures" + + " with attributes: {}", dateRange); + OrderFailureResponse orderFailures = orderAnalyticsService + .getOrdersFailures( + dateRange.getStartDate(), + dateRange.getEndDate()); return ResponseEntity.ok(orderFailures); } } diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java index b5180d4..388c46f 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProductReportController.java @@ -14,10 +14,11 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor @RestController - +@Slf4j @RequestMapping("/product-analytics") public class ProductReportController { @@ -27,6 +28,9 @@ public class ProductReportController { public ResponseEntity> getTopSellers( @Valid @ModelAttribute final TopSellerRequest requestDTO) { + log.info("Request on: /product-analytics/top-sellers" + + " with attributes: {}", requestDTO); + List topSellersList = productAnalyticsService .getTopSellers(requestDTO.getStartDate(), requestDTO.getEndDate(), 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 4e81ea2..78ac65e 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/ProfitReportController.java @@ -28,17 +28,18 @@ 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") + @Operation(summary = "Get profit by product category", + description = "Returns the revenue, cost, and profit metrics " + + "grouped by product category") @GetMapping("/by-category") public ResponseEntity> getProfitByCategory( @Valid @ModelAttribute final DateRangeRequest request) { - List profitData = - profitAnalyticsService.getProfitByCategory( + log.info("Request on: /profit-analytics/by-category" + + " with attributes: {}", request); + + List profitData = profitAnalyticsService + .getProfitByCategory( request.getStartDate(), request.getEndDate()); return ResponseEntity.ok(profitData); diff --git a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java index b2c6555..88fe2f1 100644 --- a/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java +++ b/src/main/java/com/Podzilla/analytics/api/controllers/RevenueReportController.java @@ -16,7 +16,9 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @RequiredArgsConstructor @RestController @RequestMapping("/revenue-analytics") @@ -26,6 +28,8 @@ public class RevenueReportController { @GetMapping("/summary") public ResponseEntity> getRevenueSummary( @Valid @ModelAttribute final RevenueSummaryRequest requestDTO) { + log.info("Request on: /revenue-analytics/summary" + + " with attributes: {}", requestDTO); return ResponseEntity.ok(revenueReportService .getRevenueSummary(requestDTO.getStartDate(), requestDTO.getEndDate(), @@ -35,6 +39,8 @@ public ResponseEntity> getRevenueSummary( @GetMapping("/by-category") public ResponseEntity> getRevenueByCategory( @Valid @ModelAttribute final RevenueByCategoryRequest requestDTO) { + log.info("Request on: /revenue-analytics/by-category" + + " with attributes: {}", requestDTO); List summaryList = revenueReportService .getRevenueByCategory( requestDTO.getStartDate(), 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 7bede34..f27a6c8 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangePaginationRequest.java @@ -8,12 +8,12 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Data; import io.swagger.v3.oas.annotations.media.Schema; @ValidDateRange @ValidPagination -@Getter +@Data @AllArgsConstructor public class DateRangePaginationRequest implements IDateRangeRequest, IPaginationRequest { 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 586c69f..c372a3e 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/DateRangeRequest.java @@ -8,11 +8,11 @@ import jakarta.validation.constraints.NotNull; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Data; import io.swagger.v3.oas.annotations.media.Schema; @ValidDateRange -@Getter +@Data @AllArgsConstructor public class DateRangeRequest implements IDateRangeRequest { 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 8cb6f79..15c8ea8 100644 --- a/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java +++ b/src/main/java/com/Podzilla/analytics/api/dtos/PaginationRequest.java @@ -1,14 +1,14 @@ package com.Podzilla.analytics.api.dtos; import jakarta.validation.constraints.Min; import lombok.AllArgsConstructor; -import lombok.Getter; +import lombok.Data; import com.Podzilla.analytics.validation.annotations.ValidPagination; import io.swagger.v3.oas.annotations.media.Schema; @ValidPagination -@Getter +@Data @AllArgsConstructor public class PaginationRequest implements IPaginationRequest { 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 fc924fc..cf2f9f2 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,6 @@ import java.math.BigDecimal; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.UUID; @Data @Builder @@ -14,10 +13,6 @@ @AllArgsConstructor public class OrderRegionResponse { - @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/projections/order/OrderRegionProjection.java b/src/main/java/com/Podzilla/analytics/api/projections/order/OrderRegionProjection.java index b15e7c3..c1e9acc 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,10 +1,8 @@ package com.Podzilla.analytics.api.projections.order; import java.math.BigDecimal; -import java.util.UUID; public interface OrderRegionProjection { - UUID getRegionId(); String getCity(); String getCountry(); Long getOrderCount(); diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 13a8b9d..57eb4a0 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -257,7 +257,7 @@ private void seedOrders( .id(UUID.randomUUID()) .customer(customers.get(1)).courier(couriers.get(1)) .region(regions.get(1)) - .status(Order.OrderStatus.DELIVERED) + .status(Order.OrderStatus.SHIPPED) .orderPlacedTimestamp(placed2) .shippedTimestamp(placed2.plusDays(ORDER_2_SHIP_DAYS) .plusHours(ORDER_2_SHIP_HOURS)) @@ -284,7 +284,7 @@ private void seedOrders( .customer(customers.get(0)).courier(couriers.get(0)) .region(regions.get(2)) .orderPlacedTimestamp(placed3) - .status(Order.OrderStatus.DELIVERED) + .status(Order.OrderStatus.DELIVERY_FAILED) .orderPlacedTimestamp(placed3) .shippedTimestamp(placed3.plusHours(ORDER_3_SHIP_HOURS)) .deliveredTimestamp(null) diff --git a/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java index 5208367..78ec3a9 100644 --- a/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java +++ b/src/main/java/com/Podzilla/analytics/messaging/AnalyticsRabbitListener.java @@ -3,13 +3,11 @@ 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; +import lombok.extern.slf4j.Slf4j; - - +@Slf4j @Service public class AnalyticsRabbitListener { @@ -20,6 +18,7 @@ public class AnalyticsRabbitListener { queues = EventsConstants.ANALYTICS_USER_EVENT_QUEUE ) public void handleUserEvents(final BaseEvent userEvent) { + log.info("Received user event: {}", userEvent); dispatcher.dispatch(userEvent); } @@ -27,6 +26,7 @@ public void handleUserEvents(final BaseEvent userEvent) { queues = EventsConstants.ANALYTICS_ORDER_EVENT_QUEUE ) public void handleOrderEvents(final BaseEvent orderEvent) { + log.info("Received order event: {}", orderEvent); dispatcher.dispatch(orderEvent); } @@ -34,6 +34,7 @@ public void handleOrderEvents(final BaseEvent orderEvent) { queues = EventsConstants.ANALYTICS_INVENTORY_EVENT_QUEUE ) public void handleInventoryEvents(final BaseEvent inventoryEvent) { + log.info("Received inventory event: {}", inventoryEvent); dispatcher.dispatch(inventoryEvent); } } diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index b85869a..6fabe47 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -83,8 +83,7 @@ List findShipToDeliverTimeByCourier( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query("SELECT r.id AS regionId, " - + "r.city AS city, " + @Query("SELECT r.city AS city, " + "r.country AS country, " + "COUNT(o) AS orderCount, " + "AVG(o.totalAmount) AS averageOrderValue " @@ -92,7 +91,7 @@ List findShipToDeliverTimeByCourier( + "JOIN o.region r " + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate " + "AND o.status = 'DELIVERED' " - + "GROUP BY r.id, r.city, r.country " + + "GROUP BY r.city, r.country " + "ORDER BY orderCount DESC, averageOrderValue DESC") List findOrdersByRegion( @Param("startDate") LocalDateTime startDate, @@ -119,9 +118,9 @@ List findFailureReasons( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); - @Query("SELECT (SUM(CASE WHEN o.status = 'DELIVERY_FAILED' " + @Query("SELECT COALESCE(SUM(CASE WHEN o.status = 'DELIVERY_FAILED' " + "THEN 1 ELSE 0 END) * 1.0 " - + "/ COUNT(o)) AS failureRate " + + "/ NULLIF(COUNT(o), 0), 0) AS failureRate " + "FROM Order o " + "WHERE o.finalStatusTimestamp BETWEEN :startDate AND :endDate") OrderFailureRateProjection calculateFailureRate( diff --git a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java index 33033e2..0238f3a 100644 --- a/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CourierAnalyticsService.java @@ -19,11 +19,13 @@ import com.Podzilla.analytics.util.StringToUUIDParser; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor @Service +@Slf4j public class CourierAnalyticsService { - private final CourierRepository courierRepository; + private final CourierRepository courierRepository; private List getCourierPerformanceData( final LocalDate startDate, @@ -40,74 +42,78 @@ private List getCourierPerformanceData( 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) { + log.info("Getting courier delivery counts between {} and {}", + startDate, 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) { + log.info("Getting courier success rates between {} and {}", + startDate, 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) { + log.info("Getting courier average ratings between {} and {}", + startDate, 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) { + log.info("Getting courier performance report between {} and {}", + startDate, 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); - } + public void saveCourier( + final String courierId, + final String courierName) { + log.info("Saving courier with id: {} and name: {}", + courierId, 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 0afeebb..2f14ea0 100644 --- a/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/CustomerAnalyticsService.java @@ -10,6 +10,7 @@ import com.Podzilla.analytics.models.Customer; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import java.time.LocalDateTime; import java.time.LocalDate; @@ -18,42 +19,45 @@ @Service @RequiredArgsConstructor +@Slf4j public class CustomerAnalyticsService { - private final CustomerRepository customerRepository; + private final CustomerRepository customerRepository; - public List getTopSpenders( - 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(startDateTime, endDateTime, pageRequest) -.stream() -.map(row -> CustomersTopSpendersResponse.builder() -.customerId(row.getCustomerId()) -.customerName(row.getCustomerName()) -.totalSpending(row.getTotalSpending()) -.build()) -.toList(); - return topSpenders; - } + public List getTopSpenders( + final LocalDate startDate, + final LocalDate endDate, + final int page, + final int size) { + log.info("Getting top spenders between {} and {}, page: {}," + + " size: {}", startDate, endDate, page, size); + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + PageRequest pageRequest = PageRequest.of(page, size); + List topSpenders = customerRepository + .findTopSpenders(startDateTime, endDateTime, pageRequest) + .stream() + .map(row -> CustomersTopSpendersResponse.builder() + .customerId(row.getCustomerId()) + .customerName(row.getCustomerName()) + .totalSpending(row.getTotalSpending()) + .build()) + .toList(); + log.info("Found {} top spenders", topSpenders.size()); + 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); - } + public void saveCustomer( + final String customerId, + final String customerName) { + UUID id = StringToUUIDParser.parseStringToUUID(customerId); + Customer customer = Customer.builder() + .id(id) + .name(customerName) + .build(); + log.info("Saving customer: {} with ID: {}", + customer.getName(), 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 5fca47b..67f47a6 100644 --- a/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/FulfillmentAnalyticsService.java @@ -8,6 +8,7 @@ import com.Podzilla.analytics.api.dtos.fulfillment.FulfillmentShipToDeliverRequest.ShipToDeliverGroupBy; import com.Podzilla.analytics.api.projections.fulfillment.FulfillmentTimeProjection; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.time.LocalDate; @@ -18,6 +19,7 @@ @RequiredArgsConstructor @Service +@Slf4j public class FulfillmentAnalyticsService { private final OrderRepository orderRepository; @@ -26,6 +28,8 @@ public List getPlaceToShipTimeResponse( final LocalDate startDate, final LocalDate endDate, final PlaceToShipGroupBy groupBy) { + log.info("Getting place-to-ship time response between {} and {}" + + " with groupBy {}", startDate, endDate, groupBy); LocalDateTime startDateTime = DatetimeFormatter .convertStartDateToDatetime(startDate); LocalDateTime endDateTime = DatetimeFormatter @@ -34,13 +38,15 @@ public List getPlaceToShipTimeResponse( switch (groupBy) { case OVERALL: + log.debug("Fetching overall place-to-ship time"); FulfillmentTimeProjection overall = orderRepository .findPlaceToShipTimeOverall(startDateTime, endDateTime); - if (overall != null) { + if (overall != null && overall.getAverageDuration() != null) { results.add(convertToResponse(overall)); } break; case REGION: + log.debug("Fetching place-to-ship time by region"); List byRegion = orderRepository .findPlaceToShipTimeByRegion( startDateTime, endDateTime); @@ -49,6 +55,8 @@ public List getPlaceToShipTimeResponse( .collect(Collectors.toList())); break; default: + log.warn("Unknown groupBy value for place-to-ship: {}", + groupBy); // Handle unknown groupBy or throw an exception break; } @@ -60,6 +68,9 @@ public List getShipToDeliverTimeResponse( final LocalDate startDate, final LocalDate endDate, final ShipToDeliverGroupBy groupBy) { + log.info("Getting ship-to-deliver time response between {} and" + + " {} with groupBy {}", startDate, endDate, + groupBy); LocalDateTime startDateTime = DatetimeFormatter .convertStartDateToDatetime(startDate); LocalDateTime endDateTime = DatetimeFormatter @@ -68,14 +79,16 @@ public List getShipToDeliverTimeResponse( switch (groupBy) { case OVERALL: + log.debug("Fetching overall ship-to-deliver time"); FulfillmentTimeProjection overall = orderRepository .findShipToDeliverTimeOverall( startDateTime, endDateTime); - if (overall != null) { + if (overall != null && overall.getAverageDuration() != null) { results.add(convertToResponse(overall)); } break; case REGION: + log.debug("Fetching ship-to-deliver time by region"); List byRegion = orderRepository .findShipToDeliverTimeByRegion( startDateTime, endDateTime); @@ -84,6 +97,7 @@ public List getShipToDeliverTimeResponse( .collect(Collectors.toList())); break; case COURIER: + log.debug("Fetching ship-to-deliver time by courier"); List byCourier = orderRepository .findShipToDeliverTimeByCourier( startDateTime, endDateTime); @@ -92,6 +106,8 @@ public List getShipToDeliverTimeResponse( .collect(Collectors.toList())); break; default: + log.warn("Unknown groupBy value for ship-to-deliver: {}", + groupBy); // Handle unknown groupBy or throw an exception break; } diff --git a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java index ca61ada..1bb1255 100644 --- a/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/InventoryAnalyticsService.java @@ -18,30 +18,37 @@ import java.time.Instant; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor +@Slf4j public class InventoryAnalyticsService { private final ProductSnapshotRepository inventoryRepo; private final ProductRepository productRepository; -public List getInventoryValueByCategory() { -List invVByCy = inventoryRepo -.getInventoryValueByCategory() -.stream() -.map(row -> InventoryValueByCategoryResponse.builder() -.category(row.getCategory()) -.totalStockValue(row.getTotalStockValue()) -.build()) + public List getInventoryValueByCategory( + + ) { + log.info("Getting inventory value by category"); + List invVByCy = inventoryRepo + .getInventoryValueByCategory() + .stream() + .map(row -> InventoryValueByCategoryResponse.builder() + .category(row.getCategory()) + .totalStockValue(row.getTotalStockValue()) + .build()) .toList(); return invVByCy; } -public Page getLowStockProducts(final int page, - final int size) { + public Page getLowStockProducts( + final int page, final int size) { + log.info("Getting low stock products, page: {}, size: {}", + page, size); PageRequest pageRequest = PageRequest.of(page, size); -Page lowStockPro = - inventoryRepo.getLowStockProducts(pageRequest) + Page lowStockPro = inventoryRepo + .getLowStockProducts(pageRequest) .map(row -> LowStockProductResponse.builder() .productId(row.getProductId()) .productName(row.getProductName()) @@ -54,13 +61,15 @@ public Page getLowStockProducts(final int page, public void saveInventorySnapshot( final String productId, final Integer quantity, - final Instant timestamp - ) { + final Instant timestamp) { + log.info("Saving inventory snapshot for productId: {}," + + " quantity: {}, timestamp: {}", productId, quantity, + timestamp); UUID productUUID = StringToUUIDParser.parseStringToUUID(productId); Product product = productRepository.findById(productUUID) .orElseThrow( - () -> new IllegalArgumentException("Product not found") - ); + () -> new IllegalArgumentException( + "Product not found")); LocalDateTime snapshotTimestamp = DatetimeFormatter .convertIntsantToDateTime(timestamp); ProductSnapshot inventorySnapshot = ProductSnapshot.builder() @@ -70,5 +79,7 @@ public void saveInventorySnapshot( .build(); inventoryRepo.save(inventorySnapshot); + log.info("Inventory snapshot saved for productId: {}", + productId); } } diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java index eb74223..4c05379 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -27,142 +27,144 @@ import com.Podzilla.analytics.models.Courier; import com.Podzilla.analytics.util.StringToUUIDParser; - - import lombok.RequiredArgsConstructor; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; @Service @RequiredArgsConstructor +@Slf4j 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 - ) { - LocalDateTime startDateTime = - DatetimeFormatter.convertStartDateToDatetime(startDate); - LocalDateTime endDateTime = - DatetimeFormatter.convertEndDateToDatetime(endDate); - List ordersByRegion = - orderRepository.findOrdersByRegion(startDateTime, endDateTime); + final LocalDate startDate, + final LocalDate endDate) { + log.info("Getting orders by region between {} and {}", + startDate, endDate); + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + List ordersByRegion = orderRepository + .findOrdersByRegion(startDateTime, endDateTime); return ordersByRegion.stream() - .map(data -> OrderRegionResponse.builder() - .regionId(data.getRegionId()) - .city(data.getCity()) - .country(data.getCountry()) - .orderCount(data.getOrderCount()) - .averageOrderValue(data.getAverageOrderValue()) - .build()) - .toList(); + .map(data -> OrderRegionResponse.builder() + .city(data.getCity()) + .country(data.getCountry()) + .orderCount(data.getOrderCount()) + .averageOrderValue(data.getAverageOrderValue()) + .build()) + .toList(); } public List getOrdersStatusCounts( - final LocalDate startDate, - final LocalDate endDate - ) { - LocalDateTime startDateTime = - DatetimeFormatter.convertStartDateToDatetime(startDate); - LocalDateTime endDateTime = - DatetimeFormatter.convertEndDateToDatetime(endDate); - List orderStatusCounts = - orderRepository.findOrderStatusCounts(startDateTime, endDateTime); + final LocalDate startDate, + final LocalDate endDate) { + log.info("Getting order status counts between {} and {}", + startDate, endDate); + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + List orderStatusCounts = orderRepository + .findOrderStatusCounts(startDateTime, + endDateTime); return orderStatusCounts.stream() - .map(data -> OrderStatusResponse.builder() - .status(data.getStatus()) - .count(data.getCount()) - .build()) - .toList(); + .map(data -> OrderStatusResponse.builder() + .status(data.getStatus()) + .count(data.getCount()) + .build()) + .toList(); } public OrderFailureResponse getOrdersFailures( - final LocalDate startDate, - final LocalDate endDate - ) { - LocalDateTime startDateTime = - DatetimeFormatter.convertStartDateToDatetime(startDate); - LocalDateTime endDateTime = - DatetimeFormatter.convertEndDateToDatetime(endDate); - List failureReasons = - orderRepository.findFailureReasons(startDateTime, endDateTime); - OrderFailureRateProjection failureRate = - orderRepository.calculateFailureRate(startDateTime, endDateTime); - List - failureReasonsDTO = failureReasons.stream() - .map(data -> OrderFailureReasonsResponse.builder() - .reason(data.getReason()) - .count(data.getCount()) - .build()) - .toList(); + final LocalDate startDate, + final LocalDate endDate) { + log.info("Getting order failures between {} and {}", + startDate, endDate); + LocalDateTime startDateTime = DatetimeFormatter + .convertStartDateToDatetime(startDate); + LocalDateTime endDateTime = DatetimeFormatter + .convertEndDateToDatetime(endDate); + List failureReasons = orderRepository + .findFailureReasons(startDateTime, + endDateTime); + OrderFailureRateProjection failureRate = orderRepository + .calculateFailureRate(startDateTime, endDateTime); + List failureReasonsDTO = failureReasons + .stream() + .map(data -> OrderFailureReasonsResponse.builder() + .reason(data.getReason()) + .count(data.getCount()) + .build()) + .toList(); return OrderFailureResponse.builder() - .reasons(failureReasonsDTO) - .failureRate(failureRate.getFailureRate()) - .build(); + .reasons(failureReasonsDTO) + .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")); + final String orderId, + final String customerId, + final List items, + final Region region, + final BigDecimal totalAmount, + final Instant timeStamp) { + log.info("Saving order with orderId: {}, customerId: {}," + + " region: {}, totalAmount: {}, timeStamp: {}", + orderId, customerId, region, totalAmount, 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); + .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(); + .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(); + 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")); + final String orderId, + final String reason, + final Instant timeStamp) { + log.info("Cancelling order with orderId: {}, reason: {}," + + " timeStamp: {}", orderId, reason, 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); @@ -171,33 +173,33 @@ public Order cancelOrder( } 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")); + final String orderId, + final String courierId) { + log.info("Assigning courier with courierId: {} " + + "to orderId: {}", courierId, orderId); + 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")); + final String orderId, + final Instant timeStamp) { + log.info("Marking order as out for delivery." + + " orderId: {}, timeStamp: {}", orderId, 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); @@ -205,17 +207,18 @@ public void markOrderAsOutForDelivery( } 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")); + final String orderId, + final BigDecimal courierRating, + final Instant timeStamp) { + log.info("Marking order as delivered. orderId: {}," + + " courierRating: {}, timeStamp: {}", orderId, courierRating, + 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); @@ -224,43 +227,43 @@ public void markOrderAsDelivered( } 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")); + final String orderId, + final String reason, + final Instant timeStamp) { + log.info("Marking order as failed to deliver." + + " orderId: {}, reason: {}, timeStamp: {}", orderId, reason, + 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 - ); + 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")); + final String orderId, + final String reason, + final Instant timeStamp) { + log.info("Marking order as failed to fulfill." + + " orderId: {}, reason: {}, timeStamp: {}", orderId, reason, + 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 - ); + 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 index daa91bc..9a5c09c 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderItemService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderItemService.java @@ -2,11 +2,11 @@ import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; 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; @@ -14,12 +14,12 @@ import com.Podzilla.analytics.models.Product; import com.Podzilla.analytics.models.Order; - import java.math.BigDecimal; import java.util.UUID; @Service @RequiredArgsConstructor +@Slf4j public class OrderItemService { @Autowired private final OrderItemRepository orderItemRepository; @@ -31,25 +31,47 @@ public class OrderItemService { private final OrderRepository orderRepository; public OrderItem saveOrderItem( - final int quantity, - final BigDecimal pricePerUnit, - final String productId, - final String orderId - ) { + final int quantity, + final BigDecimal pricePerUnit, + final String productId, + final String orderId) { + log.info("Attempting to save OrderItem." + + " quantity: {}, pricePerUnit: {}, productId: {}, orderId: {}", + quantity, pricePerUnit, productId, + orderId); + UUID productUUID = StringToUUIDParser.parseStringToUUID(productId); UUID orderUUID = StringToUUIDParser.parseStringToUUID(orderId); + + log.debug("Parsed productId to UUID: {}, orderId to UUID: {}", + productUUID, orderUUID); + Product product = productRepository.findById(productUUID) - .orElseThrow(() -> new RuntimeException("Product not found")); + .orElseThrow(() -> { + log.error("Product not found. productId: {}", + productId); + return new RuntimeException("Product not found"); + }); + Order order = orderRepository.findById(orderUUID) - .orElseThrow(() -> new RuntimeException("Order not found")); + .orElseThrow(() -> { + log.error("Order not found. orderId: {}", orderId); + return new RuntimeException("Order not found"); + }); OrderItem orderItem = OrderItem.builder() - .quantity(quantity) - .pricePerUnit(pricePerUnit) - .product(product) - .order(order) - .build(); - return orderItemRepository.save(orderItem); - } + .quantity(quantity) + .pricePerUnit(pricePerUnit) + .product(product) + .order(order) + .build(); + log.info("Saving OrderItem for orderId: {}, productId: {}", + orderId, productId); + OrderItem savedOrderItem = orderItemRepository.save(orderItem); + log.info("OrderItem saved successfully. orderItemId: {}", + savedOrderItem.getId()); + + return savedOrderItem; + } } diff --git a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java index 350f005..21ca5f9 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProductAnalyticsService.java @@ -13,12 +13,14 @@ import com.Podzilla.analytics.api.dtos.product.TopSellerResponse; import com.Podzilla.analytics.repositories.ProductRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import com.Podzilla.analytics.models.Product; import com.Podzilla.analytics.util.StringToUUIDParser; import java.util.UUID; @RequiredArgsConstructor @Service +@Slf4j public class ProductAnalyticsService { private final ProductRepository productRepository; @@ -39,8 +41,10 @@ public List getTopSellers( final LocalDate startDate, final LocalDate endDate, final Integer limit, - final SortBy sortBy -) { + final SortBy sortBy) { + log.info("Getting top sellers between {} and {}" + + " with limit {} and sortBy {}", startDate, + endDate, limit, sortBy); final String sortByString = sortBy != null ? sortBy.name() : SortBy.REVENUE.name(); @@ -54,6 +58,8 @@ public List getTopSellers( endDateTime, limit, sortByString); + log.debug("Query returned {} top sellers", queryResults.size()); + List topSellersList = new ArrayList<>(); for (TopSellingProductProjection row : queryResults) { @@ -71,9 +77,11 @@ public List getTopSellers( } topSellersList.sort((a, b) -> b.getValue().compareTo(a.getValue())); if (limit != null && limit > 0 && limit < topSellersList.size()) { + log.debug("Limiting top sellers list to {}", limit); topSellersList = topSellersList.subList(SUBLIST_START_INDEX, limit); } + log.info("Returning {} top sellers", topSellersList.size()); return topSellersList; } @@ -82,8 +90,9 @@ public void saveProduct( final String productName, final String productCategory, final BigDecimal productCost, - final Integer productLowStockThreshold - ) { + final Integer productLowStockThreshold) { + log.info("Saving product with id: {}, name: {}, " + + " category: {}", productId, productName, productCategory); UUID id = StringToUUIDParser.parseStringToUUID(productId); Product product = Product.builder() .id(id) @@ -93,5 +102,6 @@ public void saveProduct( .lowStockThreshold(productLowStockThreshold) .build(); productRepository.save(product); + log.debug("Product saved: {}", product); } } diff --git a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java index 0daf15e..72a8fa5 100644 --- a/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/ProfitAnalyticsService.java @@ -7,6 +7,7 @@ import com.Podzilla.analytics.repositories.OrderItemRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; @@ -18,6 +19,7 @@ @RequiredArgsConstructor @Service +@Slf4j public class ProfitAnalyticsService { private final OrderItemRepository salesLineItemRepository; private static final int PERCENTAGE_PRECISION = 4; @@ -25,12 +27,17 @@ public class ProfitAnalyticsService { public List getProfitByCategory( final LocalDate startDate, final LocalDate endDate) { + log.info("Getting profit by category between {} and {}", + startDate, endDate); LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); List salesData = salesLineItemRepository .findSalesByCategoryBetweenDates(startDateTime, endDateTime); + log.debug("Fetched {} sales data records for categories", + salesData.size()); + return salesData.stream() .map(this::convertToDTO) .collect(Collectors.toList()); diff --git a/src/main/java/com/Podzilla/analytics/services/RegionService.java b/src/main/java/com/Podzilla/analytics/services/RegionService.java index f653fed..cc26dec 100644 --- a/src/main/java/com/Podzilla/analytics/services/RegionService.java +++ b/src/main/java/com/Podzilla/analytics/services/RegionService.java @@ -6,29 +6,34 @@ import com.Podzilla.analytics.repositories.RegionRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import com.Podzilla.analytics.models.Region; - - @Service @RequiredArgsConstructor +@Slf4j public class RegionService { @Autowired private final RegionRepository regionRepository; public Region saveRegion( - final String city, - final String state, - final String country, - final String postalCode - ) { + final String city, + final String state, + final String country, + final String postalCode) { + log.info("Saving region with city: {}, state: {}," + + " country: {}, postalCode: {}", + city, state, country, postalCode); Region region = Region.builder() - .city(city) - .state(state) - .country(country) - .postalCode(postalCode) - .build(); - return regionRepository.save(region); + .city(city) + .state(state) + .country(country) + .postalCode(postalCode) + .build(); + Region savedRegion = regionRepository.save(region); + log.info("Region saved successfully with id: {}", + savedRegion.getId()); + return savedRegion; } } diff --git a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java index 0de6380..990f051 100644 --- a/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java +++ b/src/main/java/com/Podzilla/analytics/services/RevenueReportService.java @@ -15,9 +15,11 @@ import com.Podzilla.analytics.repositories.OrderRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; @RequiredArgsConstructor @Service +@Slf4j public class RevenueReportService { private final OrderRepository orderRepository; @@ -29,6 +31,8 @@ public List getRevenueSummary( LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + log.info("Getting revenue summary between {} and {}" + + " for period '{}'", startDate, endDate, periodString); final List revenueData = orderRepository .findRevenueSummaryByPeriod( @@ -47,6 +51,7 @@ public List getRevenueSummary( summaryList.add(summaryItem); } + log.info("Revenue summary result size: {}", summaryList.size()); return summaryList; } @@ -63,6 +68,8 @@ public List getRevenueByCategory( LocalDateTime startDateTime = startDate.atStartOfDay(); LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + log.info("Getting revenue by category between" + + " {} and {}", startDate, endDate); final List queryResults = orderRepository .findRevenueByCategory( @@ -82,6 +89,8 @@ public List getRevenueByCategory( summaryList.add(summaryItem); } + log.info("Revenue by category result" + + " size: {}", summaryList.size()); return summaryList; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ae2f487..494844a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,7 +10,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.generate-ddl=true -spring.jpa.show-sql=true +# spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..e69de29 From fb7d2f3a23c9723a7d32ea5b82b6c7ffd4a8b20a Mon Sep 17 00:00:00 2001 From: Nour Eldien Ayman <49641430+NourAlPha@users.noreply.github.com> Date: Mon, 19 May 2025 18:00:42 +0300 Subject: [PATCH 23/28] chore: update podzilla-utils-lib version to v1.1.13 (#28) --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 299c965..36744ae 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ com.github.Podzilla podzilla-utils-lib - v1.1.12 + v1.1.13 From 9faf710efdcaf57ddf6f797e30db6fded915db0d Mon Sep 17 00:00:00 2001 From: Malek Mohamed Noureldean Elkssas <87043730+malekelkssas@users.noreply.github.com> Date: Mon, 19 May 2025 18:33:27 +0300 Subject: [PATCH 24/28] Inventory customers tests (#29) * test: create the controller tests for the inventory-analytics & customer-analytics endpoints * test: create the services tests for the inventory-analytics & customer-analytics endpoints * fix: lint * fix: query syntax --- .../repositories/CustomerRepository.java | 24 +- .../ProductSnapshotRepository.java | 43 ++-- .../CustomerReportControllerTest.java | 221 ++++++++++++++++++ .../InventoryReportControllerTest.java | 210 +++++++++++++++++ .../services/CustomerReportServiceTest.java | 165 +++++++++++++ .../services/InventoryReportServiceTest.java | 195 ++++++++++++++++ 6 files changed, 825 insertions(+), 33 deletions(-) create mode 100644 src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java create mode 100644 src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java create mode 100644 src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java diff --git a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java index 92af9b4..d50e880 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/CustomerRepository.java @@ -16,16 +16,16 @@ @Repository public interface CustomerRepository extends JpaRepository { - @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); + @Query("SELECT c.id AS customerId, c.name AS customerName, " + + "COALESCE(SUM(o.totalAmount), 0) AS totalSpending " + + "FROM Customer c " + + "INNER 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/ProductSnapshotRepository.java b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java index 6759b77..90795ce 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/ProductSnapshotRepository.java @@ -1,6 +1,7 @@ package com.Podzilla.analytics.repositories; import java.util.List; +import java.util.UUID; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -14,27 +15,27 @@ @Repository public interface ProductSnapshotRepository - extends JpaRepository { + 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.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); + @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/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java new file mode 100644 index 0000000..a39e7bf --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/CustomerReportControllerTest.java @@ -0,0 +1,221 @@ +package com.Podzilla.analytics.controllers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.hamcrest.Matchers.*; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.transaction.annotation.Transactional; + +import com.Podzilla.analytics.models.Customer; +import com.Podzilla.analytics.models.Order; +import com.Podzilla.analytics.models.Region; +import com.Podzilla.analytics.repositories.CustomerRepository; +import com.Podzilla.analytics.repositories.OrderRepository; +import com.Podzilla.analytics.repositories.RegionRepository; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +class CustomerReportControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private OrderRepository orderRepository; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private EntityManager entityManager; + + private static final DateTimeFormatter ISO_LOCAL_DATE = DateTimeFormatter.ISO_LOCAL_DATE; + + private Customer customer1; + private Customer customer2; + private Region region1; + private Order order1; + private Order order2; + private Order order3; + + @BeforeEach + void setUp() { + orderRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + // Create test region + region1 = regionRepository.save(Region.builder() + .city("Sample City") + .state("Sample State") + .country("Sample Country") + .postalCode("12345") + .build()); + + // Create test customers + customer1 = customerRepository.save(Customer.builder() + .id(UUID.randomUUID()) + .name("John Doe") + .build()); + + customer2 = customerRepository.save(Customer.builder() + .id(UUID.randomUUID()) + .name("Jane Smith") + .build()); + + // Create test orders + order1 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("1000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(2)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer1) + .region(region1) + .build()); + + order2 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("500.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer1) + .region(region1) + .build()); + + order3 = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("2000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(3)) + .status(Order.OrderStatus.DELIVERED) + .customer(customer2) + .region(region1) + .build()); + + entityManager.flush(); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + orderRepository.deleteAll(); + customerRepository.deleteAll(); + regionRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void contextLoads() { + } + + @Test + void getTopSpenders_ShouldReturnListOfTopSpenders() throws Exception { + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].customerName").value(customer1.getName())) + .andExpect(jsonPath("$[1].totalSpending") + .value(closeTo(order1.getTotalAmount().add(order2.getTotalAmount()).doubleValue(), 0.01))); + } + + @Test + void getTopSpenders_ShouldReturnEmptyListWhenNoOrdersInDateRange() throws Exception { + LocalDate startDate = LocalDate.now().plusDays(1); + LocalDate endDate = LocalDate.now().plusDays(2); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + void getTopSpenders_ShouldHandlePagination() throws Exception { + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))); + } + + @Test + void getTopSpenders_ShouldExcludeFailedOrders() throws Exception { + Order failedOrder = orderRepository.save(Order.builder() + .id(UUID.randomUUID()) + .totalAmount(new BigDecimal("3000.00")) + .finalStatusTimestamp(LocalDateTime.now().minusDays(1)) + .status(Order.OrderStatus.DELIVERY_FAILED) + .customer(customer1) + .region(region1) + .build()); + + entityManager.flush(); + entityManager.clear(); + + LocalDate startDate = LocalDate.now().minusDays(4); + LocalDate endDate = LocalDate.now(); + + mockMvc.perform(get("/customer-analytics/top-spenders") + .param("startDate", startDate.format(ISO_LOCAL_DATE)) + .param("endDate", endDate.format(ISO_LOCAL_DATE)) + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].customerName").value(customer2.getName())) + .andExpect(jsonPath("$[0].totalSpending").value(closeTo(order3.getTotalAmount().doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].customerName").value(customer1.getName())) + .andExpect(jsonPath("$[1].totalSpending") + .value(closeTo(order1.getTotalAmount().add(order2.getTotalAmount()).doubleValue(), 0.01))) + .andExpect(jsonPath("$[1].totalSpending").value(not(closeTo(order1.getTotalAmount() + .add(order2.getTotalAmount()).add(failedOrder.getTotalAmount()).doubleValue(), 0.01)))); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java b/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java new file mode 100644 index 0000000..a4af6b3 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/controllers/InventoryReportControllerTest.java @@ -0,0 +1,210 @@ +package com.Podzilla.analytics.controllers; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.hamcrest.Matchers.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.transaction.annotation.Transactional; + +import com.Podzilla.analytics.models.Product; +import com.Podzilla.analytics.models.ProductSnapshot; +import com.Podzilla.analytics.repositories.ProductRepository; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@ActiveProfiles("test") +class InventoryReportControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ProductRepository productRepository; + + @Autowired + private ProductSnapshotRepository productSnapshotRepository; + + @Autowired + private EntityManager entityManager; + + private Product electronicsProduct; + private Product clothingProduct; + + @BeforeEach + void setUp() { + productSnapshotRepository.deleteAll(); + productRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + + // Create test products + electronicsProduct = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("Laptop") + .category("Electronics") + .cost(new BigDecimal("1000.00")) + .lowStockThreshold(10) + .build()); + + clothingProduct = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("T-Shirt") + .category("Clothing") + .cost(new BigDecimal("20.00")) + .lowStockThreshold(5) + .build()); + + entityManager.flush(); + entityManager.clear(); + + // Create product snapshots in a separate transaction + ProductSnapshot electronicsSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(electronicsProduct) + .quantity(15) + .build(); + + ProductSnapshot clothingSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(clothingProduct) + .quantity(3) // Below threshold + .build(); + + productSnapshotRepository.save(electronicsSnapshot); + productSnapshotRepository.save(clothingSnapshot); + entityManager.flush(); + entityManager.clear(); + } + + @AfterEach + void tearDown() { + productSnapshotRepository.deleteAll(); + productRepository.deleteAll(); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void contextLoads() { + } + + @Test + void getInventoryValueByCategory_ShouldReturnListOfCategoryValues() throws Exception { + mockMvc.perform(get("/inventory-analytics/value/by-category")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[?(@.category == '" + electronicsProduct.getCategory() + + "')].totalStockValue") + .value(hasItem(closeTo(electronicsProduct.getCost() + .multiply(new BigDecimal("15")) + .doubleValue(), 0.01)))) + .andExpect(jsonPath("$[?(@.category == '" + clothingProduct.getCategory() + + "')].totalStockValue") + .value(hasItem(closeTo(clothingProduct.getCost() + .multiply(new BigDecimal("3")) + .doubleValue(), 0.01)))); + } + + @Test + void getLowStockProducts_ShouldReturnOnlyProductsBelowThreshold() throws Exception { + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(clothingProduct.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(3)) + .andExpect(jsonPath("$.content[0].threshold") + .value(clothingProduct.getLowStockThreshold())); + } + + @Test + void getLowStockProducts_ShouldReturnEmptyListWhenNoProductsBelowThreshold() throws Exception { + // Create a new snapshot with quantity above threshold + ProductSnapshot newSnapshot = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(clothingProduct) + .quantity(clothingProduct.getLowStockThreshold() + 1) + .build(); + + productSnapshotRepository.save(newSnapshot); + entityManager.flush(); + entityManager.clear(); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "10")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(0))); + } + + @Test + void getLowStockProducts_ShouldHandlePagination() throws Exception { + Product product2 = productRepository.save(Product.builder() + .id(UUID.randomUUID()) + .name("Jeans") + .category(clothingProduct.getCategory()) + .cost(new BigDecimal("50.00")) + .lowStockThreshold(8) + .build()); + + entityManager.flush(); + entityManager.clear(); + + ProductSnapshot snapshot2 = ProductSnapshot.builder() + .timestamp(LocalDateTime.now()) + .product(product2) + .quantity(5) + .build(); + + productSnapshotRepository.save(snapshot2); + entityManager.flush(); + entityManager.clear(); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "0") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(clothingProduct.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(3)) + .andExpect(jsonPath("$.content[0].threshold") + .value(clothingProduct.getLowStockThreshold())) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.totalPages").value(2)); + + mockMvc.perform(get("/inventory-analytics/low-stock") + .param("page", "1") + .param("size", "1")) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content", hasSize(1))) + .andExpect(jsonPath("$.content[0].productName").value(product2.getName())) + .andExpect(jsonPath("$.content[0].currentQuantity").value(5)) + .andExpect(jsonPath("$.content[0].threshold").value(product2.getLowStockThreshold())) + .andExpect(jsonPath("$.totalElements").value(2)) + .andExpect(jsonPath("$.totalPages").value(2)); + } +} \ No newline at end of file diff --git a/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java new file mode 100644 index 0000000..ccff387 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/CustomerReportServiceTest.java @@ -0,0 +1,165 @@ +package com.Podzilla.analytics.services; + +import com.Podzilla.analytics.api.dtos.customer.CustomersTopSpendersResponse; +import com.Podzilla.analytics.api.projections.customer.CustomersTopSpendersProjection; +import com.Podzilla.analytics.repositories.CustomerRepository; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +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; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CustomerReportServiceTest { + + @Mock + private CustomerRepository customerRepository; + + @InjectMocks + private CustomerAnalyticsService customerAnalyticsService; + + private LocalDate testStartDate; + private LocalDate testEndDate; + private LocalDateTime expectedStartDateTime; + private LocalDateTime expectedEndDateTime; + + @BeforeEach + void setUp() { + testStartDate = LocalDate.of(2024, 1, 1); + testEndDate = LocalDate.of(2024, 1, 31); + expectedStartDateTime = testStartDate.atStartOfDay(); + expectedEndDateTime = testEndDate.atTime(LocalTime.MAX); + } + + private CustomersTopSpendersProjection createMockProjection( + UUID customerId, String customerName, BigDecimal totalSpending) { + CustomersTopSpendersProjection mockProjection = Mockito.mock(CustomersTopSpendersProjection.class); + Mockito.lenient().when(mockProjection.getCustomerId()).thenReturn(customerId); + Mockito.lenient().when(mockProjection.getCustomerName()).thenReturn(customerName); + Mockito.lenient().when(mockProjection.getTotalSpending()).thenReturn(totalSpending); + return mockProjection; + } + + @Test + void getTopSpenders_shouldReturnCorrectSpendersForMultipleCustomers() { + // Arrange + UUID customerId1 = UUID.randomUUID(); + UUID customerId2 = UUID.randomUUID(); + CustomersTopSpendersProjection janeData = createMockProjection( + customerId1, "Jane", new BigDecimal("5000.00")); + CustomersTopSpendersProjection johnData = createMockProjection( + customerId2, "John", new BigDecimal("3000.00")); + + Page mockPage = new PageImpl<>( + Arrays.asList(janeData, johnData)); + + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 10); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + CustomersTopSpendersResponse janeResponse = result.stream() + .filter(r -> r.getCustomerName().equals("Jane")) + .findFirst().orElse(null); + assertNotNull(janeResponse); + assertEquals(customerId1, janeResponse.getCustomerId()); + assertEquals(new BigDecimal("5000.00"), janeResponse.getTotalSpending()); + + CustomersTopSpendersResponse johnResponse = result.stream() + .filter(r -> r.getCustomerName().equals("John")) + .findFirst().orElse(null); + assertNotNull(johnResponse); + assertEquals(customerId2, johnResponse.getCustomerId()); + assertEquals(new BigDecimal("3000.00"), johnResponse.getTotalSpending()); + + // Verify repository method was called with correct arguments + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 10)); + } + + @Test + void getTopSpenders_shouldReturnEmptyListWhenNoData() { + // Arrange + Page emptyPage = new PageImpl<>(Collections.emptyList()); + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(emptyPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 10); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 10)); + } + + @Test + void getTopSpenders_shouldHandlePagination() { + // Arrange + UUID customerId1 = UUID.randomUUID(); + CustomersTopSpendersProjection janeData = createMockProjection( + customerId1, "Jane", new BigDecimal("5000.00")); + + Page mockPage = new PageImpl<>( + Collections.singletonList(janeData)); + + when(customerRepository.findTopSpenders( + any(LocalDateTime.class), + any(LocalDateTime.class), + any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + List result = customerAnalyticsService + .getTopSpenders(testStartDate, testEndDate, 0, 1); + + // Assert + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Jane", result.get(0).getCustomerName()); + assertEquals(new BigDecimal("5000.00"), result.get(0).getTotalSpending()); + + Mockito.verify(customerRepository).findTopSpenders( + expectedStartDateTime, + expectedEndDateTime, + PageRequest.of(0, 1)); + } +} diff --git a/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java b/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java new file mode 100644 index 0000000..ce60a56 --- /dev/null +++ b/src/test/java/com/Podzilla/analytics/services/InventoryReportServiceTest.java @@ -0,0 +1,195 @@ +package com.Podzilla.analytics.services; + +import com.Podzilla.analytics.api.dtos.inventory.InventoryValueByCategoryResponse; +import com.Podzilla.analytics.api.dtos.inventory.LowStockProductResponse; +import com.Podzilla.analytics.api.projections.inventory.InventoryValueByCategoryProjection; +import com.Podzilla.analytics.api.projections.inventory.LowStockProductProjection; +import com.Podzilla.analytics.repositories.ProductSnapshotRepository; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.math.BigDecimal; +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; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class InventoryReportServiceTest { + + @Mock + private ProductSnapshotRepository inventoryRepo; + + @InjectMocks + private InventoryAnalyticsService inventoryAnalyticsService; + + private InventoryValueByCategoryProjection createMockInventoryProjection( + String category, BigDecimal totalStockValue) { + InventoryValueByCategoryProjection mockProjection = Mockito.mock(InventoryValueByCategoryProjection.class); + Mockito.lenient().when(mockProjection.getCategory()).thenReturn(category); + Mockito.lenient().when(mockProjection.getTotalStockValue()).thenReturn(totalStockValue); + return mockProjection; + } + + private LowStockProductProjection createMockLowStockProjection( + UUID productId, String productName, Long currentQuantity, Long threshold) { + LowStockProductProjection mockProjection = Mockito.mock(LowStockProductProjection.class); + Mockito.lenient().when(mockProjection.getProductId()).thenReturn(productId); + Mockito.lenient().when(mockProjection.getProductName()).thenReturn(productName); + Mockito.lenient().when(mockProjection.getCurrentQuantity()).thenReturn(currentQuantity); + Mockito.lenient().when(mockProjection.getThreshold()).thenReturn(threshold); + return mockProjection; + } + + @Test + void getInventoryValueByCategory_shouldReturnCorrectValuesForMultipleCategories() { + // Arrange + InventoryValueByCategoryProjection electronicsData = createMockInventoryProjection( + "Electronics", new BigDecimal("50000.00")); + InventoryValueByCategoryProjection clothingData = createMockInventoryProjection( + "Clothing", new BigDecimal("20000.00")); + + when(inventoryRepo.getInventoryValueByCategory()) + .thenReturn(Arrays.asList(electronicsData, clothingData)); + + // Act + List result = inventoryAnalyticsService + .getInventoryValueByCategory(); + + // Assert + assertNotNull(result); + assertEquals(2, result.size()); + + InventoryValueByCategoryResponse electronicsResponse = result.stream() + .filter(r -> r.getCategory().equals("Electronics")) + .findFirst().orElse(null); + assertNotNull(electronicsResponse); + assertEquals(new BigDecimal("50000.00"), electronicsResponse.getTotalStockValue()); + + InventoryValueByCategoryResponse clothingResponse = result.stream() + .filter(r -> r.getCategory().equals("Clothing")) + .findFirst().orElse(null); + assertNotNull(clothingResponse); + assertEquals(new BigDecimal("20000.00"), clothingResponse.getTotalStockValue()); + + Mockito.verify(inventoryRepo).getInventoryValueByCategory(); + } + + @Test + void getInventoryValueByCategory_shouldReturnEmptyListWhenNoData() { + // Arrange + when(inventoryRepo.getInventoryValueByCategory()) + .thenReturn(Collections.emptyList()); + + // Act + List result = inventoryAnalyticsService + .getInventoryValueByCategory(); + + // Assert + assertNotNull(result); + assertTrue(result.isEmpty()); + + Mockito.verify(inventoryRepo).getInventoryValueByCategory(); + } + + @Test + void getLowStockProducts_shouldReturnCorrectProducts() { + // Arrange + UUID productId1 = UUID.randomUUID(); + UUID productId2 = UUID.randomUUID(); + LowStockProductProjection product1Data = createMockLowStockProjection( + productId1, "Laptop", 5L, 10L); + LowStockProductProjection product2Data = createMockLowStockProjection( + productId2, "Mouse", 2L, 5L); + + Page mockPage = new PageImpl<>( + Arrays.asList(product1Data, product2Data)); + + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 10); + + // Assert + assertNotNull(result); + assertEquals(2, result.getContent().size()); + + LowStockProductResponse product1Response = result.getContent().stream() + .filter(p -> p.getProductName().equals("Laptop")) + .findFirst().orElse(null); + assertNotNull(product1Response); + assertEquals(productId1, product1Response.getProductId()); + assertEquals(5, product1Response.getCurrentQuantity()); + assertEquals(10, product1Response.getThreshold()); + + LowStockProductResponse product2Response = result.getContent().stream() + .filter(p -> p.getProductName().equals("Mouse")) + .findFirst().orElse(null); + assertNotNull(product2Response); + assertEquals(productId2, product2Response.getProductId()); + assertEquals(2, product2Response.getCurrentQuantity()); + assertEquals(5, product2Response.getThreshold()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 10)); + } + + @Test + void getLowStockProducts_shouldReturnEmptyPageWhenNoData() { + // Arrange + Page emptyPage = new PageImpl<>(Collections.emptyList()); + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(emptyPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 10); + + // Assert + assertNotNull(result); + assertTrue(result.getContent().isEmpty()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 10)); + } + + @Test + void getLowStockProducts_shouldHandlePagination() { + // Arrange + UUID productId = UUID.randomUUID(); + LowStockProductProjection productData = createMockLowStockProjection( + productId, "Laptop", 5L, 10L); + + Page mockPage = new PageImpl<>( + Collections.singletonList(productData)); + + when(inventoryRepo.getLowStockProducts(any(PageRequest.class))) + .thenReturn(mockPage); + + // Act + Page result = inventoryAnalyticsService + .getLowStockProducts(0, 1); + + // Assert + assertNotNull(result); + assertEquals(1, result.getContent().size()); + assertEquals("Laptop", result.getContent().get(0).getProductName()); + assertEquals(5, result.getContent().get(0).getCurrentQuantity()); + assertEquals(10, result.getContent().get(0).getThreshold()); + + Mockito.verify(inventoryRepo).getLowStockProducts(PageRequest.of(0, 1)); + } +} From 501be19c5da01a3fc7bd3cc9b445052c0ae5b42d Mon Sep 17 00:00:00 2001 From: Malek Mohamed Noureldean Elkssas <87043730+malekelkssas@users.noreply.github.com> Date: Mon, 19 May 2025 19:26:46 +0300 Subject: [PATCH 25/28] Feat cron jobs (#30) * feat: add the cron jobs to remove the orders monthly * feat: add the cron jobs to remove the orders monthly --- .../analytics/AnalyticsApplication.java | 2 ++ .../repositories/OrderRepository.java | 14 ++++++++ .../scheduling/CleanupScheduler.java | 32 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 src/main/java/com/Podzilla/analytics/scheduling/CleanupScheduler.java diff --git a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java index 683b0d5..cd1ea7d 100644 --- a/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java +++ b/src/main/java/com/Podzilla/analytics/AnalyticsApplication.java @@ -3,9 +3,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @ComponentScan(basePackages = { "com.podzilla", "com.Podzilla" }) +@EnableScheduling public class AnalyticsApplication { public static void main(final String[] args) { diff --git a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java index 6fabe47..7fcb5b5 100644 --- a/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java +++ b/src/main/java/com/Podzilla/analytics/repositories/OrderRepository.java @@ -6,6 +6,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.transaction.annotation.Transactional; import com.Podzilla.analytics.api.projections.fulfillment.FulfillmentTimeProjection; import com.Podzilla.analytics.api.projections.order.OrderFailureRateProjection; @@ -164,4 +166,16 @@ List findRevenueSummaryByPeriod( List findRevenueByCategory( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); + + @Modifying + @Transactional + @Query("DELETE FROM OrderItem oi " + + "WHERE oi.order.id IN (SELECT o.id FROM Order o " + + "WHERE o.orderPlacedTimestamp < :cutoff)") + void deleteOrderItemsOlderThan(@Param("cutoff") LocalDateTime cutoff); + + @Modifying + @Transactional + @Query("DELETE FROM Order o WHERE o.orderPlacedTimestamp < :cutoff") + void deleteOrdersOlderThan(@Param("cutoff") LocalDateTime cutoff); } diff --git a/src/main/java/com/Podzilla/analytics/scheduling/CleanupScheduler.java b/src/main/java/com/Podzilla/analytics/scheduling/CleanupScheduler.java new file mode 100644 index 0000000..d72f4b6 --- /dev/null +++ b/src/main/java/com/Podzilla/analytics/scheduling/CleanupScheduler.java @@ -0,0 +1,32 @@ +package com.Podzilla.analytics.scheduling; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import com.Podzilla.analytics.repositories.OrderRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; + +@Component +public class CleanupScheduler { + + private final OrderRepository orderRepository; + + @Autowired + public CleanupScheduler(final OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + // // Runs at 2:00 AM on the first day of every month + @Scheduled(cron = "0 0 2 1 * ?") + @Transactional + public void monthlyCleanup() { + System.out.println("Starting monthly cleanup"); + LocalDateTime cutoff = LocalDateTime.now().minusMonths(1); + // Delete order items first + orderRepository.deleteOrderItemsOlderThan(cutoff); + // Then delete orders + orderRepository.deleteOrdersOlderThan(cutoff); + System.out.println("Monthly cleanup completed"); + } +} From cc4b649494d4e133a9dd49a925d6971084ac8350 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Mon, 19 May 2025 23:37:21 +0300 Subject: [PATCH 26/28] fix: hot fix --- .../analytics/config/DatabaseSeeder.java | 44 +++++++++---------- .../order/OrderDeliveryFailedInvoker.java | 2 +- .../com/Podzilla/analytics/models/Order.java | 4 +- .../services/OrderAnalyticsService.java | 16 +++---- src/main/resources/application.properties | 1 + 5 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 57eb4a0..5cca8ef 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -96,37 +96,37 @@ public class DatabaseSeeder implements CommandLineRunner { @Override @Transactional public void run(final String... args) { - System.out.println("Checking if database needs seeding..."); + // System.out.println("Checking if database needs seeding..."); - if (courierRepository.count() > 0) { - System.out.println("Database already seeded. Skipping."); - return; - } + // if (courierRepository.count() > 0) { + // System.out.println("Database already seeded. Skipping."); + // return; + // } - System.out.println("Seeding database..."); + // System.out.println("Seeding database..."); - List regions = seedRegions(); - System.out.println("Seeded Regions: " + regions.size()); + // List regions = seedRegions(); + // System.out.println("Seeded Regions: " + regions.size()); - List products = seedProducts(); - System.out.println("Seeded Products: " + products.size()); + // List products = seedProducts(); + // System.out.println("Seeded Products: " + products.size()); - List couriers = seedCouriers(); - System.out.println("Seeded Couriers: " + couriers.size()); + // List couriers = seedCouriers(); + // System.out.println("Seeded Couriers: " + couriers.size()); - List customers = seedCustomers(); - System.out.println("Seeded Customers: " + customers.size()); + // List customers = seedCustomers(); + // System.out.println("Seeded Customers: " + customers.size()); - System.out.println("Seeding Orders and SalesLineItems..."); - seedOrders(customers, couriers, regions, products); - System.out.println("Seeded Orders: " + orderRepository.count()); + // System.out.println("Seeding Orders and SalesLineItems..."); + // seedOrders(customers, couriers, regions, products); + // System.out.println("Seeded Orders: " + orderRepository.count()); - System.out.println("Seeding Inventory Snapshots..."); - seedProductSnapshots(products); - System.out.println("Seeded Product Snapshots: " - + productSnapshotRepository.count()); + // System.out.println("Seeding Inventory Snapshots..."); + // seedProductSnapshots(products); + // System.out.println("Seeded Product Snapshots: " + // + productSnapshotRepository.count()); - System.out.println("Database seeding finished."); + // System.out.println("Database seeding finished."); } private List seedRegions() { 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 index c483511..ddbdaa5 100644 --- a/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java +++ b/src/main/java/com/Podzilla/analytics/messaging/invokers/order/OrderDeliveryFailedInvoker.java @@ -21,7 +21,7 @@ public void invoke(final OrderDeliveryFailedEvent event) { MarkOrderAsFailedToDeliverCommand command = commandFactory.createMarkOrderAsFailedToDeliverCommand( event.getOrderId(), - event.getCourierId(), + event.getReason(), event.getTimestamp() ); command.execute(); diff --git a/src/main/java/com/Podzilla/analytics/models/Order.java b/src/main/java/com/Podzilla/analytics/models/Order.java index 16c3b90..c109d57 100644 --- a/src/main/java/com/Podzilla/analytics/models/Order.java +++ b/src/main/java/com/Podzilla/analytics/models/Order.java @@ -48,7 +48,7 @@ public class Order { private BigDecimal courierRating; @ManyToOne - @JoinColumn(name = "customer_id", nullable = false) + @JoinColumn(name = "customer_id", nullable = true) private Customer customer; @ManyToOne @@ -56,7 +56,7 @@ public class Order { private Courier courier; @ManyToOne - @JoinColumn(name = "region_id", nullable = false) + @JoinColumn(name = "region_id", nullable = true) private Region region; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) diff --git a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java index 4c05379..3a6e491 100644 --- a/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java +++ b/src/main/java/com/Podzilla/analytics/services/OrderAnalyticsService.java @@ -257,14 +257,14 @@ public void markOrderAsFailedToFulfill( 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); + Order order = Order.builder() + .id(orderUUID) + .status(OrderStatus.FULFILLMENT_FAILED) + .failureReason(reason) + .orderFulfillmentFailedTimestamp( + orderFulfillmentFailedTimestamp) + .finalStatusTimestamp(orderFulfillmentFailedTimestamp) + .build(); orderRepository.save(order); } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 494844a..4de7429 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,5 @@ spring.application.name=analytics +server.servlet.context-path=/api # Database Configuration spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/analytics_db_dev} From cef4f3445e1d0b87dc9c45ea53e4b59ce92ec3b1 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 21 May 2025 11:46:25 +0300 Subject: [PATCH 27/28] dummmy commit --- .../analytics/config/DatabaseSeeder.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 5cca8ef..57eb4a0 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -96,37 +96,37 @@ public class DatabaseSeeder implements CommandLineRunner { @Override @Transactional public void run(final String... args) { - // System.out.println("Checking if database needs seeding..."); + System.out.println("Checking if database needs seeding..."); - // if (courierRepository.count() > 0) { - // System.out.println("Database already seeded. Skipping."); - // return; - // } + if (courierRepository.count() > 0) { + System.out.println("Database already seeded. Skipping."); + return; + } - // System.out.println("Seeding database..."); + System.out.println("Seeding database..."); - // List regions = seedRegions(); - // System.out.println("Seeded Regions: " + regions.size()); + List regions = seedRegions(); + System.out.println("Seeded Regions: " + regions.size()); - // List products = seedProducts(); - // System.out.println("Seeded Products: " + products.size()); + List products = seedProducts(); + System.out.println("Seeded Products: " + products.size()); - // List couriers = seedCouriers(); - // System.out.println("Seeded Couriers: " + couriers.size()); + List couriers = seedCouriers(); + System.out.println("Seeded Couriers: " + couriers.size()); - // List customers = seedCustomers(); - // System.out.println("Seeded Customers: " + customers.size()); + List customers = seedCustomers(); + System.out.println("Seeded Customers: " + customers.size()); - // System.out.println("Seeding Orders and SalesLineItems..."); - // seedOrders(customers, couriers, regions, products); - // System.out.println("Seeded Orders: " + orderRepository.count()); + System.out.println("Seeding Orders and SalesLineItems..."); + seedOrders(customers, couriers, regions, products); + System.out.println("Seeded Orders: " + orderRepository.count()); - // System.out.println("Seeding Inventory Snapshots..."); - // seedProductSnapshots(products); - // System.out.println("Seeded Product Snapshots: " - // + productSnapshotRepository.count()); + System.out.println("Seeding Inventory Snapshots..."); + seedProductSnapshots(products); + System.out.println("Seeded Product Snapshots: " + + productSnapshotRepository.count()); - // System.out.println("Database seeding finished."); + System.out.println("Database seeding finished."); } private List seedRegions() { From e838243bef4abe90264e3640c4e3aaea1c9c42a2 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Wed, 21 May 2025 12:01:28 +0300 Subject: [PATCH 28/28] dummmy commit 2 --- src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java index 57eb4a0..d47c979 100644 --- a/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java +++ b/src/main/java/com/Podzilla/analytics/config/DatabaseSeeder.java @@ -121,6 +121,7 @@ public void run(final String... args) { seedOrders(customers, couriers, regions, products); System.out.println("Seeded Orders: " + orderRepository.count()); + System.out.println("Seeding Inventory Snapshots..."); seedProductSnapshots(products); System.out.println("Seeded Product Snapshots: "