Ajay Yadav commited on
Commit
b4ca43a
·
1 Parent(s): 377d3b3

Initial deployment of da-reporting-dev

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +27 -0
  2. README.md +34 -6
  3. build.gradle.kts +23 -0
  4. src/main/docker/Dockerfile +23 -0
  5. src/main/docker/Dockerfile.alpine-jlink +43 -0
  6. src/main/docker/Dockerfile.layered +34 -0
  7. src/main/docker/Dockerfile.native +20 -0
  8. src/main/java/com/dalab/reporting/DaReportingApplication.java +17 -0
  9. src/main/java/com/dalab/reporting/client/CatalogServiceClient.java +66 -0
  10. src/main/java/com/dalab/reporting/client/PolicyEngineServiceClient.java +59 -0
  11. src/main/java/com/dalab/reporting/common/exception/BadRequestException.java +15 -0
  12. src/main/java/com/dalab/reporting/common/exception/ConflictException.java +15 -0
  13. src/main/java/com/dalab/reporting/common/exception/ResourceNotFoundException.java +15 -0
  14. src/main/java/com/dalab/reporting/config/AsyncConfig.java +21 -0
  15. src/main/java/com/dalab/reporting/config/DashboardConfig.java +20 -0
  16. src/main/java/com/dalab/reporting/config/KafkaConsumerConfig.java +58 -0
  17. src/main/java/com/dalab/reporting/config/OpenAPIConfiguration.java +40 -0
  18. src/main/java/com/dalab/reporting/config/SecurityConfiguration.java +45 -0
  19. src/main/java/com/dalab/reporting/controller/DashboardController.java +159 -0
  20. src/main/java/com/dalab/reporting/controller/NotificationController.java +108 -0
  21. src/main/java/com/dalab/reporting/controller/ReportController.java +121 -0
  22. src/main/java/com/dalab/reporting/dto/ActionItemsDTO.java +173 -0
  23. src/main/java/com/dalab/reporting/dto/CostSavingsDTO.java +158 -0
  24. src/main/java/com/dalab/reporting/dto/GeneratedReportOutputDTO.java +26 -0
  25. src/main/java/com/dalab/reporting/dto/GovernanceScoreDTO.java +143 -0
  26. src/main/java/com/dalab/reporting/dto/NotificationPreferenceDTO.java +36 -0
  27. src/main/java/com/dalab/reporting/dto/NotificationRequestDTO.java +37 -0
  28. src/main/java/com/dalab/reporting/dto/ReportDefinitionDTO.java +36 -0
  29. src/main/java/com/dalab/reporting/dto/ReportGenerationRequestDTO.java +21 -0
  30. src/main/java/com/dalab/reporting/event/PolicyActionEventDTO.java +25 -0
  31. src/main/java/com/dalab/reporting/kafka/PolicyActionConsumer.java +144 -0
  32. src/main/java/com/dalab/reporting/mapper/NotificationPreferenceMapper.java +20 -0
  33. src/main/java/com/dalab/reporting/mapper/ReportMapper.java +31 -0
  34. src/main/java/com/dalab/reporting/model/GeneratedReport.java +146 -0
  35. src/main/java/com/dalab/reporting/model/NotificationChannel.java +8 -0
  36. src/main/java/com/dalab/reporting/model/ReportDefinition.java +135 -0
  37. src/main/java/com/dalab/reporting/model/ReportStatus.java +9 -0
  38. src/main/java/com/dalab/reporting/model/UserNotificationPreference.java +129 -0
  39. src/main/java/com/dalab/reporting/repository/GeneratedReportRepository.java +20 -0
  40. src/main/java/com/dalab/reporting/repository/ReportDefinitionRepository.java +15 -0
  41. src/main/java/com/dalab/reporting/repository/UserNotificationPreferenceRepository.java +20 -0
  42. src/main/java/com/dalab/reporting/service/DashboardService.java +410 -0
  43. src/main/java/com/dalab/reporting/service/IDashboardService.java +53 -0
  44. src/main/java/com/dalab/reporting/service/INotificationService.java +28 -0
  45. src/main/java/com/dalab/reporting/service/IReportService.java +33 -0
  46. src/main/java/com/dalab/reporting/service/impl/NotificationService.java +207 -0
  47. src/main/java/com/dalab/reporting/service/impl/ReportService.java +230 -0
  48. src/main/java/com/dalab/reporting/service/notification/IEmailNotificationProvider.java +5 -0
  49. src/main/java/com/dalab/reporting/service/notification/INotificationProvider.java +11 -0
  50. src/main/java/com/dalab/reporting/service/notification/ISlackNotificationProvider.java +6 -0
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM openjdk:21-jdk-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install required packages
6
+ RUN apt-get update && apt-get install -y \
7
+ curl \
8
+ wget \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy application files
12
+ COPY . .
13
+
14
+ # Build application (if build.gradle.kts exists)
15
+ RUN if [ -f "build.gradle.kts" ]; then \
16
+ ./gradlew build -x test; \
17
+ fi
18
+
19
+ # Expose port
20
+ EXPOSE 8080
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
24
+ CMD curl -f http://localhost:8080/actuator/health || exit 1
25
+
26
+ # Run application
27
+ CMD ["java", "-jar", "build/libs/da-reporting.jar"]
README.md CHANGED
@@ -1,10 +1,38 @@
1
  ---
2
- title: Da Reporting Dev
3
- emoji: 🐠
4
- colorFrom: pink
5
- colorTo: red
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: da-reporting (dev)
3
+ emoji: 🔧
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 8080
8
  ---
9
 
10
+ # da-reporting - dev Environment
11
+
12
+ This is the da-reporting microservice deployed in the dev environment.
13
+
14
+ ## Features
15
+
16
+ - RESTful API endpoints
17
+ - Health monitoring via Actuator
18
+ - JWT authentication integration
19
+ - PostgreSQL database connectivity
20
+
21
+ ## API Documentation
22
+
23
+ Once deployed, API documentation will be available at:
24
+ - Swagger UI: https://huggingface.co/spaces/dalabsai/da-reporting-dev/swagger-ui.html
25
+ - Health Check: https://huggingface.co/spaces/dalabsai/da-reporting-dev/actuator/health
26
+
27
+ ## Environment
28
+
29
+ - **Environment**: dev
30
+ - **Port**: 8080
31
+ - **Java Version**: 21
32
+ - **Framework**: Spring Boot
33
+
34
+ ## Deployment
35
+
36
+ This service is automatically deployed via the DALab CI/CD pipeline.
37
+
38
+ Last updated: 2025-06-16 23:39:54
build.gradle.kts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // da-reporting inherits common configuration from parent build.gradle.kts
2
+ // This build file adds reporting-specific dependencies
3
+
4
+ dependencies {
5
+ // DA-Protos common entities and utilities
6
+ implementation(project(":da-protos"))
7
+
8
+ // Email and notification dependencies
9
+ implementation("org.springframework.boot:spring-boot-starter-mail")
10
+ implementation("com.slack.api:slack-api-client:1.38.0")
11
+
12
+ // Cross-service communication for dashboard aggregation
13
+ implementation("org.springframework.cloud:spring-cloud-starter-openfeign:4.1.1")
14
+ implementation("org.springframework.boot:spring-boot-starter-cache")
15
+
16
+ // OpenAPI documentation for dashboard endpoints
17
+ implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0")
18
+ }
19
+
20
+ // Configure main application class
21
+ configure<org.springframework.boot.gradle.dsl.SpringBootExtension> {
22
+ mainClass.set("com.dalab.reporting.DaReportingApplication")
23
+ }
src/main/docker/Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-lean container using Google Distroless
2
+ # Expected final size: ~120-180MB (minimal base + JRE + JAR only)
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot
5
+
6
+ # Set working directory
7
+ WORKDIR /app
8
+
9
+ # Copy JAR file
10
+ COPY build/libs/da-reporting.jar app.jar
11
+
12
+ # Expose standard Spring Boot port
13
+ EXPOSE 8080
14
+
15
+ # Run application (distroless has no shell, so use exec form)
16
+ ENTRYPOINT ["java", \
17
+ "-XX:+UseContainerSupport", \
18
+ "-XX:MaxRAMPercentage=75.0", \
19
+ "-XX:+UseG1GC", \
20
+ "-XX:+UseStringDeduplication", \
21
+ "-Djava.security.egd=file:/dev/./urandom", \
22
+ "-Dspring.backgroundpreinitializer.ignore=true", \
23
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.alpine-jlink ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-minimal Alpine + Custom JRE
2
+ # Expected size: ~120-160MB
3
+
4
+ # Stage 1: Create custom JRE with only needed modules
5
+ FROM eclipse-temurin:21-jdk-alpine as jre-builder
6
+ WORKDIR /app
7
+
8
+ # Analyze JAR to find required modules
9
+ COPY build/libs/*.jar app.jar
10
+ RUN jdeps --ignore-missing-deps --print-module-deps app.jar > modules.txt
11
+
12
+ # Create minimal JRE with only required modules
13
+ RUN jlink \
14
+ --add-modules $(cat modules.txt),java.logging,java.xml,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \
15
+ --strip-debug \
16
+ --no-man-pages \
17
+ --no-header-files \
18
+ --compress=2 \
19
+ --output /custom-jre
20
+
21
+ # Stage 2: Production image
22
+ FROM alpine:3.19
23
+ RUN apk add --no-cache tzdata && \
24
+ addgroup -g 1001 -S appgroup && \
25
+ adduser -u 1001 -S appuser -G appgroup
26
+
27
+ # Copy custom JRE
28
+ COPY --from=jre-builder /custom-jre /opt/java
29
+ ENV JAVA_HOME=/opt/java
30
+ ENV PATH="$JAVA_HOME/bin:$PATH"
31
+
32
+ WORKDIR /app
33
+ COPY build/libs/*.jar app.jar
34
+ RUN chown appuser:appgroup app.jar
35
+
36
+ USER appuser
37
+ EXPOSE 8080
38
+
39
+ ENTRYPOINT ["java", \
40
+ "-XX:+UseContainerSupport", \
41
+ "-XX:MaxRAMPercentage=70.0", \
42
+ "-XX:+UseG1GC", \
43
+ "-jar", "app.jar"]
src/main/docker/Dockerfile.layered ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ultra-optimized layered build using Distroless
2
+ # Expected size: ~180-220MB with better caching
3
+
4
+ FROM gcr.io/distroless/java21-debian12:nonroot as base
5
+
6
+ # Stage 1: Extract JAR layers for optimal caching
7
+ FROM eclipse-temurin:21-jdk-alpine as extractor
8
+ WORKDIR /app
9
+ COPY build/libs/*.jar app.jar
10
+ RUN java -Djarmode=layertools -jar app.jar extract
11
+
12
+ # Stage 2: Production image with extracted layers
13
+ FROM base
14
+ WORKDIR /app
15
+
16
+ # Copy layers in dependency order (best caching)
17
+ COPY --from=extractor /app/dependencies/ ./
18
+ COPY --from=extractor /app/spring-boot-loader/ ./
19
+ COPY --from=extractor /app/snapshot-dependencies/ ./
20
+ COPY --from=extractor /app/application/ ./
21
+
22
+ EXPOSE 8080
23
+
24
+ # Optimized JVM settings for micro-containers
25
+ ENTRYPOINT ["java", \
26
+ "-XX:+UseContainerSupport", \
27
+ "-XX:MaxRAMPercentage=70.0", \
28
+ "-XX:+UseG1GC", \
29
+ "-XX:+UseStringDeduplication", \
30
+ "-XX:+CompactStrings", \
31
+ "-Xshare:on", \
32
+ "-Djava.security.egd=file:/dev/./urandom", \
33
+ "-Dspring.backgroundpreinitializer.ignore=true", \
34
+ "org.springframework.boot.loader.JarLauncher"]
src/main/docker/Dockerfile.native ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GraalVM Native Image - Ultra-fast startup, tiny size
2
+ # Expected size: ~50-80MB, startup <100ms
3
+ # Note: Requires native compilation support in Spring Boot
4
+
5
+ # Stage 1: Native compilation
6
+ FROM ghcr.io/graalvm/graalvm-ce:ol9-java21 as native-builder
7
+ WORKDIR /app
8
+
9
+ # Install native-image
10
+ RUN gu install native-image
11
+
12
+ # Copy source and build native executable
13
+ COPY . .
14
+ RUN ./gradlew nativeCompile
15
+
16
+ # Stage 2: Minimal runtime
17
+ FROM scratch
18
+ COPY --from=native-builder /app/build/native/nativeCompile/app /app
19
+ EXPOSE 8080
20
+ ENTRYPOINT ["/app"]
src/main/java/com/dalab/reporting/DaReportingApplication.java ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting;
2
+
3
+ import org.springframework.boot.SpringApplication;
4
+ import org.springframework.boot.autoconfigure.SpringBootApplication;
5
+ import org.springframework.scheduling.annotation.EnableAsync; // For async email/slack sending
6
+ import org.springframework.scheduling.annotation.EnableScheduling; // If scheduled report generation is needed
7
+
8
+ @SpringBootApplication
9
+ @EnableScheduling // Optional: if reports are generated on a schedule
10
+ @EnableAsync // Optional: for non-blocking notification sending
11
+ public class DaReportingApplication {
12
+
13
+ public static void main(String[] args) {
14
+ SpringApplication.run(DaReportingApplication.class, args);
15
+ }
16
+
17
+ }
src/main/java/com/dalab/reporting/client/CatalogServiceClient.java ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.client;
2
+
3
+ import java.util.Map;
4
+
5
+ import org.springframework.cloud.openfeign.FeignClient;
6
+ import org.springframework.web.bind.annotation.GetMapping;
7
+ import org.springframework.web.bind.annotation.RequestParam;
8
+
9
+ /**
10
+ * Feign client for da-catalog service
11
+ * Enables cross-service communication for dashboard aggregation
12
+ *
13
+ * @author DALab Development Team
14
+ * @since 2025-01-02
15
+ */
16
+ @FeignClient(name = "da-catalog", url = "${dalab.services.da-catalog.url:http://localhost:8081}")
17
+ public interface CatalogServiceClient {
18
+
19
+ /**
20
+ * Get total asset count for governance calculations
21
+ */
22
+ @GetMapping("/api/v1/catalog/assets/count")
23
+ Long getTotalAssetCount();
24
+
25
+ /**
26
+ * Get classified assets count for data classification metrics
27
+ */
28
+ @GetMapping("/api/v1/catalog/assets/count/classified")
29
+ Long getClassifiedAssetsCount();
30
+
31
+ /**
32
+ * Get assets with metadata count for metadata completeness metrics
33
+ */
34
+ @GetMapping("/api/v1/catalog/assets/count/with-metadata")
35
+ Long getAssetsWithMetadataCount();
36
+
37
+ /**
38
+ * Get assets with business context count for governance scoring
39
+ */
40
+ @GetMapping("/api/v1/catalog/assets/count/with-business-context")
41
+ Long getAssetsWithBusinessContextCount();
42
+
43
+ /**
44
+ * Get assets with lineage count for lineage coverage metrics
45
+ */
46
+ @GetMapping("/api/v1/catalog/assets/count/with-lineage")
47
+ Long getAssetsWithLineageCount();
48
+
49
+ /**
50
+ * Get total lineage connections count
51
+ */
52
+ @GetMapping("/api/v1/catalog/assets/lineage/connections/count")
53
+ Long getTotalLineageConnectionsCount();
54
+
55
+ /**
56
+ * Get governance issues from catalog service
57
+ */
58
+ @GetMapping("/api/v1/catalog/assets/governance-issues")
59
+ Map<String, Object> getGovernanceIssues(@RequestParam(required = false) String severity);
60
+
61
+ /**
62
+ * Get asset classification breakdown for scoring
63
+ */
64
+ @GetMapping("/api/v1/catalog/assets/classification-breakdown")
65
+ Map<String, Long> getClassificationBreakdown();
66
+ }
src/main/java/com/dalab/reporting/client/PolicyEngineServiceClient.java ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.client;
2
+
3
+ import org.springframework.cloud.openfeign.FeignClient;
4
+ import org.springframework.web.bind.annotation.GetMapping;
5
+ import org.springframework.web.bind.annotation.RequestParam;
6
+
7
+ import java.util.List;
8
+ import java.util.Map;
9
+
10
+ /**
11
+ * Feign client for da-policyengine service
12
+ * Enables cross-service communication for policy compliance data
13
+ *
14
+ * @author DALab Development Team
15
+ * @since 2025-01-02
16
+ */
17
+ @FeignClient(name = "da-policyengine", url = "${dalab.services.da-policyengine.url:http://localhost:8082}")
18
+ public interface PolicyEngineServiceClient {
19
+
20
+ /**
21
+ * Get active policies count for governance metrics
22
+ */
23
+ @GetMapping("/api/v1/policyengine/policies/count/active")
24
+ Long getActivePoliciesCount();
25
+
26
+ /**
27
+ * Get policy violations count for compliance scoring
28
+ */
29
+ @GetMapping("/api/v1/policyengine/policies/violations/count")
30
+ Long getPolicyViolationsCount(@RequestParam(required = false) String severity);
31
+
32
+ /**
33
+ * Get policy compliance rate
34
+ */
35
+ @GetMapping("/api/v1/policyengine/policies/compliance-rate")
36
+ Double getPolicyComplianceRate();
37
+
38
+ /**
39
+ * Get policy compliance breakdown by category
40
+ */
41
+ @GetMapping("/api/v1/policyengine/policies/compliance-breakdown")
42
+ Map<String, Object> getPolicyComplianceBreakdown();
43
+
44
+ /**
45
+ * Get policy action items for aggregation
46
+ */
47
+ @GetMapping("/api/v1/policyengine/policies/action-items")
48
+ List<Map<String, Object>> getPolicyActionItems(
49
+ @RequestParam(required = false) String severity,
50
+ @RequestParam(required = false) String status,
51
+ @RequestParam(defaultValue = "50") Integer limit);
52
+
53
+ /**
54
+ * Get historical policy compliance data
55
+ */
56
+ @GetMapping("/api/v1/policyengine/policies/compliance-history")
57
+ List<Map<String, Object>> getPolicyComplianceHistory(
58
+ @RequestParam(defaultValue = "30") Integer days);
59
+ }
src/main/java/com/dalab/reporting/common/exception/BadRequestException.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.common.exception;
2
+
3
+ import org.springframework.http.HttpStatus;
4
+ import org.springframework.web.bind.annotation.ResponseStatus;
5
+
6
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
7
+ public class BadRequestException extends RuntimeException {
8
+ public BadRequestException(String message) {
9
+ super(message);
10
+ }
11
+
12
+ public BadRequestException(String message, Throwable cause) {
13
+ super(message, cause);
14
+ }
15
+ }
src/main/java/com/dalab/reporting/common/exception/ConflictException.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.common.exception;
2
+
3
+ import org.springframework.http.HttpStatus;
4
+ import org.springframework.web.bind.annotation.ResponseStatus;
5
+
6
+ @ResponseStatus(HttpStatus.CONFLICT)
7
+ public class ConflictException extends RuntimeException {
8
+ public ConflictException(String message) {
9
+ super(message);
10
+ }
11
+
12
+ public ConflictException(String message, Throwable cause) {
13
+ super(message, cause);
14
+ }
15
+ }
src/main/java/com/dalab/reporting/common/exception/ResourceNotFoundException.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.common.exception;
2
+
3
+ import org.springframework.http.HttpStatus;
4
+ import org.springframework.web.bind.annotation.ResponseStatus;
5
+
6
+ @ResponseStatus(HttpStatus.NOT_FOUND)
7
+ public class ResourceNotFoundException extends RuntimeException {
8
+ public ResourceNotFoundException(String message) {
9
+ super(message);
10
+ }
11
+
12
+ public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
13
+ super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
14
+ }
15
+ }
src/main/java/com/dalab/reporting/config/AsyncConfig.java ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.config;
2
+
3
+ import org.springframework.context.annotation.Bean;
4
+ import org.springframework.context.annotation.Configuration;
5
+ import org.springframework.core.task.TaskExecutor;
6
+ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
7
+
8
+ @Configuration
9
+ public class AsyncConfig {
10
+
11
+ @Bean
12
+ public TaskExecutor taskExecutor() {
13
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
14
+ executor.setCorePoolSize(5); // Adjust as needed
15
+ executor.setMaxPoolSize(10); // Adjust as needed
16
+ executor.setQueueCapacity(25); // Adjust as needed
17
+ executor.setThreadNamePrefix("ReportAsync-");
18
+ executor.initialize();
19
+ return executor;
20
+ }
21
+ }
src/main/java/com/dalab/reporting/config/DashboardConfig.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.config;
2
+
3
+ import org.springframework.cache.annotation.EnableCaching;
4
+ import org.springframework.cloud.openfeign.EnableFeignClients;
5
+ import org.springframework.context.annotation.Configuration;
6
+
7
+ /**
8
+ * Configuration for dashboard aggregation infrastructure
9
+ * Includes basic caching and Feign clients for cross-service communication
10
+ *
11
+ * @author DALab Development Team
12
+ * @since 2025-01-02
13
+ */
14
+ @Configuration
15
+ @EnableCaching
16
+ @EnableFeignClients(basePackages = "com.dalab.reporting.client")
17
+ public class DashboardConfig {
18
+ // Basic configuration for Feign clients and caching
19
+ // Redis and circuit breaker configuration can be added later
20
+ }
src/main/java/com/dalab/reporting/config/KafkaConsumerConfig.java ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.config;
2
+
3
+ import java.util.HashMap;
4
+ import java.util.Map;
5
+
6
+ import org.apache.kafka.clients.consumer.ConsumerConfig;
7
+ import org.apache.kafka.common.serialization.StringDeserializer;
8
+ import org.springframework.beans.factory.annotation.Value;
9
+ import org.springframework.context.annotation.Bean;
10
+ import org.springframework.context.annotation.Configuration;
11
+ import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
12
+ import org.springframework.kafka.core.ConsumerFactory;
13
+ import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
14
+ import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;
15
+ import org.springframework.kafka.support.serializer.JsonDeserializer;
16
+
17
+ @Configuration
18
+ public class KafkaConsumerConfig {
19
+
20
+ @Value("${spring.kafka.bootstrap-servers}")
21
+ private String bootstrapServers;
22
+
23
+ @Value("${spring.kafka.consumer.group-id:reporting-group}") // Default group-id from properties
24
+ private String defaultGroupId;
25
+
26
+ @Value("${spring.kafka.consumer.properties.spring.json.trusted.packages:*}")
27
+ private String trustedPackages;
28
+
29
+ @Bean
30
+ public ConsumerFactory<String, Object> consumerFactory() {
31
+ Map<String, Object> props = new HashMap<>();
32
+ props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
33
+ // group-id will be set per listener, but a default can be here if needed
34
+ // props.put(ConsumerConfig.GROUP_ID_CONFIG, defaultGroupId);
35
+ props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
36
+
37
+ // Using ErrorHandlingDeserializer for both key and value
38
+ ErrorHandlingDeserializer<String> keyDe = new ErrorHandlingDeserializer<>(new StringDeserializer());
39
+ ErrorHandlingDeserializer<Object> valDe = new ErrorHandlingDeserializer<>(new JsonDeserializer<>(Object.class));
40
+ // Configure JsonDeserializer for trusted packages
41
+ props.put(JsonDeserializer.TRUSTED_PACKAGES, trustedPackages);
42
+ props.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, "false"); // If not using type headers
43
+ // If you expect specific types and don't rely on headers, you might need to configure value default type
44
+ // props.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.dalab.reporting.event.PolicyActionEventDTO");
45
+
46
+ return new DefaultKafkaConsumerFactory<>(props, keyDe, valDe);
47
+ }
48
+
49
+ @Bean
50
+ public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
51
+ ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
52
+ factory.setConsumerFactory(consumerFactory());
53
+ // Add other configurations like concurrency, error handlers, filtering, etc. if needed
54
+ // factory.setConcurrency(3);
55
+ // factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
56
+ return factory;
57
+ }
58
+ }
src/main/java/com/dalab/reporting/config/OpenAPIConfiguration.java ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.config;
2
+
3
+ import org.springframework.beans.factory.annotation.Value;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+
7
+ import io.swagger.v3.oas.models.Components;
8
+ import io.swagger.v3.oas.models.OpenAPI;
9
+ import io.swagger.v3.oas.models.info.Contact;
10
+ import io.swagger.v3.oas.models.info.Info;
11
+ import io.swagger.v3.oas.models.info.License;
12
+ import io.swagger.v3.oas.models.security.SecurityRequirement;
13
+ import io.swagger.v3.oas.models.security.SecurityScheme;
14
+
15
+ @Configuration
16
+ public class OpenAPIConfiguration {
17
+
18
+ @Value("${spring.application.name:DALab Reporting Service}")
19
+ private String applicationName;
20
+
21
+ @Bean
22
+ public OpenAPI customOpenAPI() {
23
+ final String securitySchemeName = "bearerAuth";
24
+
25
+ return new OpenAPI()
26
+ .info(new Info().title(applicationName)
27
+ .description("API for DALab Reporting and Notification Service. " +
28
+ "Handles report generation, management, and user notifications via various channels.")
29
+ .version("v1.2.0") // Corresponds to API Design Document version
30
+ .contact(new Contact().name("DALab Support").email("support@dalab.com"))
31
+ .license(new License().name("Apache 2.0").url("http://springdoc.org")))
32
+ .addSecurityItem(new SecurityRequirement().addList(securitySchemeName))
33
+ .components(new Components()
34
+ .addSecuritySchemes(securitySchemeName, new SecurityScheme()
35
+ .name(securitySchemeName)
36
+ .type(SecurityScheme.Type.HTTP)
37
+ .scheme("bearer")
38
+ .bearerFormat("JWT")));
39
+ }
40
+ }
src/main/java/com/dalab/reporting/config/SecurityConfiguration.java ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.config;
2
+
3
+ import org.springframework.beans.factory.annotation.Value;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+ import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
7
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8
+ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9
+ import org.springframework.security.config.http.SessionCreationPolicy;
10
+ import org.springframework.security.oauth2.jwt.JwtDecoder;
11
+ import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
12
+ import org.springframework.security.web.SecurityFilterChain;
13
+
14
+ @Configuration
15
+ @EnableWebSecurity
16
+ @EnableMethodSecurity(prePostEnabled = true) // Enables @PreAuthorize, @PostAuthorize, etc.
17
+ public class SecurityConfiguration {
18
+
19
+ @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
20
+ private String issuerUri;
21
+
22
+ @Bean
23
+ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
24
+ http
25
+ .csrf(csrf -> csrf.disable()) // Consider enabling CSRF with proper handling if web UI is served
26
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
27
+ .authorizeHttpRequests(authz -> authz
28
+ .requestMatchers("/api/v1/reporting/actuator/**").permitAll() // Standard Spring Boot actuators
29
+ .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() // OpenAPI docs
30
+ // .requestMatchers("/public/**").permitAll() // Example for public endpoints
31
+ .anyRequest().authenticated() // All other requests require authentication
32
+ )
33
+ .oauth2ResourceServer(oauth2 -> oauth2
34
+ .jwt(jwt -> jwt.decoder(jwtDecoder()))
35
+ );
36
+ return http.build();
37
+ }
38
+
39
+ @Bean
40
+ public JwtDecoder jwtDecoder() {
41
+ // Configure the decoder with the issuer URI from properties
42
+ // It will fetch the JWK set from the .well-known/jwks.json endpoint of the issuer
43
+ return NimbusJwtDecoder.withIssuerLocation(issuerUri).build();
44
+ }
45
+ }
src/main/java/com/dalab/reporting/controller/DashboardController.java ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.controller;
2
+
3
+ import org.slf4j.Logger;
4
+ import org.slf4j.LoggerFactory;
5
+ import org.springframework.cache.annotation.Cacheable;
6
+ import org.springframework.http.ResponseEntity;
7
+ import org.springframework.security.access.prepost.PreAuthorize;
8
+ import org.springframework.web.bind.annotation.GetMapping;
9
+ import org.springframework.web.bind.annotation.RequestMapping;
10
+ import org.springframework.web.bind.annotation.RequestParam;
11
+ import org.springframework.web.bind.annotation.RestController;
12
+
13
+ import com.dalab.common.security.SecurityUtils;
14
+ import com.dalab.reporting.dto.ActionItemsDTO;
15
+ import com.dalab.reporting.dto.CostSavingsDTO;
16
+ import com.dalab.reporting.dto.GovernanceScoreDTO;
17
+ import com.dalab.reporting.service.IDashboardService;
18
+
19
+ import io.swagger.v3.oas.annotations.Operation;
20
+ import io.swagger.v3.oas.annotations.Parameter;
21
+ import io.swagger.v3.oas.annotations.responses.ApiResponse;
22
+ import io.swagger.v3.oas.annotations.responses.ApiResponses;
23
+ import io.swagger.v3.oas.annotations.tags.Tag;
24
+ import lombok.RequiredArgsConstructor;
25
+
26
+ /**
27
+ * REST controller for DALab Dashboard Aggregation APIs
28
+ * Provides cross-service aggregation for role-based dashboards
29
+ *
30
+ * @author DALab Development Team
31
+ * @since 2025-01-02
32
+ */
33
+ @RestController
34
+ @RequestMapping("/api/v1/reporting/dashboards")
35
+ @RequiredArgsConstructor
36
+ @Tag(name = "Dashboard", description = "Dashboard aggregation and analytics APIs")
37
+ public class DashboardController {
38
+
39
+ private static final Logger log = LoggerFactory.getLogger(DashboardController.class);
40
+
41
+ private final IDashboardService dashboardService;
42
+
43
+ /**
44
+ * Get governance score dashboard for CDO/VP and Director roles
45
+ * Aggregates data from da-catalog, da-autocompliance, and da-policyengine
46
+ *
47
+ * @param includeHistory whether to include 30-day historical trend data
48
+ * @param includeBenchmark whether to include industry benchmark comparison
49
+ * @return comprehensive governance score with breakdown and trends
50
+ */
51
+ @GetMapping("/cdo/governance-score")
52
+ @PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'DIRECTOR_PRODUCT')")
53
+ @Cacheable(value = "governanceScore", key = "#includeHistory + '_' + #includeBenchmark")
54
+ @Operation(summary = "Get governance score dashboard",
55
+ description = "Provides comprehensive governance metrics with weighted scoring across policy compliance, data classification, metadata completeness, and lineage coverage")
56
+ @ApiResponses({
57
+ @ApiResponse(responseCode = "200", description = "Governance score retrieved successfully"),
58
+ @ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
59
+ @ApiResponse(responseCode = "500", description = "Internal server error during cross-service aggregation")
60
+ })
61
+ public ResponseEntity<GovernanceScoreDTO> getGovernanceScore(
62
+ @Parameter(description = "Include 30-day historical trend data")
63
+ @RequestParam(defaultValue = "true") Boolean includeHistory,
64
+ @Parameter(description = "Include industry benchmark comparison")
65
+ @RequestParam(defaultValue = "true") Boolean includeBenchmark) {
66
+
67
+ log.info("REST request to get governance score dashboard by user: {}", SecurityUtils.getAuthenticatedUserId());
68
+ GovernanceScoreDTO governanceScore = dashboardService.getGovernanceScore(includeHistory, includeBenchmark);
69
+ return ResponseEntity.ok(governanceScore);
70
+ }
71
+
72
+ /**
73
+ * Get cost savings dashboard for CDO/VP and executive roles
74
+ * Aggregates data from da-autoarchival, da-autodelete, and cloud provider APIs
75
+ *
76
+ * @param includeProjections whether to include future savings projections
77
+ * @param timeframe analysis timeframe: "30_DAYS", "90_DAYS", "12_MONTHS", "ALL_TIME"
78
+ * @return comprehensive cost savings analysis with ROI calculation
79
+ */
80
+ @GetMapping("/cdo/cost-savings")
81
+ @PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'ADMIN')")
82
+ @Cacheable(value = "costSavings", key = "#includeProjections + '_' + #timeframe")
83
+ @Operation(summary = "Get cost savings dashboard",
84
+ description = "Provides comprehensive cost analysis including archival savings, deletion reclamation, and ROI calculations across multiple cloud providers")
85
+ @ApiResponses({
86
+ @ApiResponse(responseCode = "200", description = "Cost savings data retrieved successfully"),
87
+ @ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
88
+ @ApiResponse(responseCode = "500", description = "Internal server error during cost calculation"),
89
+ @ApiResponse(responseCode = "503", description = "Cloud provider API temporarily unavailable")
90
+ })
91
+ public ResponseEntity<CostSavingsDTO> getCostSavings(
92
+ @Parameter(description = "Include future savings projections")
93
+ @RequestParam(defaultValue = "true") Boolean includeProjections,
94
+ @Parameter(description = "Analysis timeframe")
95
+ @RequestParam(defaultValue = "12_MONTHS") String timeframe) {
96
+
97
+ log.info("REST request to get cost savings dashboard for timeframe: {} by user: {}", timeframe, SecurityUtils.getAuthenticatedUserId());
98
+ CostSavingsDTO costSavings = dashboardService.getCostSavings(includeProjections, timeframe);
99
+ return ResponseEntity.ok(costSavings);
100
+ }
101
+
102
+ /**
103
+ * Get aggregated action items across all DALab services
104
+ * Aggregates data from da-autocompliance, da-policyengine, da-catalog, da-discovery
105
+ *
106
+ * @param assignedToUser filter to show only items assigned to current user
107
+ * @param severity filter by severity level: "CRITICAL", "HIGH", "MEDIUM", "LOW"
108
+ * @param status filter by status: "OPEN", "IN_PROGRESS", "PENDING_APPROVAL"
109
+ * @param limit maximum number of action items to return (default 50, max 200)
110
+ * @return comprehensive action items aggregation with SLA and business impact metrics
111
+ */
112
+ @GetMapping("/cdo/action-items")
113
+ @PreAuthorize("hasAnyRole('CDO', 'VP_DATA', 'DIRECTOR_ENGINEERING', 'DIRECTOR_PRODUCT', 'DATA_ENGINEER', 'DATA_SCIENTIST')")
114
+ @Cacheable(value = "actionItems", key = "#assignedToUser + '_' + #severity + '_' + #status + '_' + #limit")
115
+ @Operation(summary = "Get aggregated action items dashboard",
116
+ description = "Provides comprehensive action items aggregation across all DALab services with SLA compliance metrics and business impact assessment")
117
+ @ApiResponses({
118
+ @ApiResponse(responseCode = "200", description = "Action items retrieved successfully"),
119
+ @ApiResponse(responseCode = "400", description = "Invalid filter parameters"),
120
+ @ApiResponse(responseCode = "403", description = "Access denied - insufficient privileges"),
121
+ @ApiResponse(responseCode = "500", description = "Internal server error during aggregation")
122
+ })
123
+ public ResponseEntity<ActionItemsDTO> getActionItems(
124
+ @Parameter(description = "Filter to show only items assigned to current user")
125
+ @RequestParam(defaultValue = "false") Boolean assignedToUser,
126
+ @Parameter(description = "Filter by severity level")
127
+ @RequestParam(required = false) String severity,
128
+ @Parameter(description = "Filter by status")
129
+ @RequestParam(required = false) String status,
130
+ @Parameter(description = "Maximum number of items to return")
131
+ @RequestParam(defaultValue = "50") Integer limit) {
132
+
133
+ log.info("REST request to get action items dashboard with filters - assignedToUser: {}, severity: {}, status: {}, limit: {} by user: {}",
134
+ assignedToUser, severity, status, limit, SecurityUtils.getAuthenticatedUserId());
135
+
136
+ // Validate limit parameter
137
+ if (limit > 200) {
138
+ limit = 200;
139
+ }
140
+
141
+ ActionItemsDTO actionItems = dashboardService.getActionItems(assignedToUser, severity, status, limit);
142
+ return ResponseEntity.ok(actionItems);
143
+ }
144
+
145
+ /**
146
+ * Refresh dashboard cache for better performance
147
+ * Useful for scheduled cache warming or manual refresh
148
+ */
149
+ @GetMapping("/cache/refresh")
150
+ @PreAuthorize("hasAnyRole('ADMIN', 'CDO')")
151
+ @Operation(summary = "Refresh dashboard cache",
152
+ description = "Manually triggers cache refresh for all dashboard endpoints to ensure fresh data")
153
+ @ApiResponse(responseCode = "200", description = "Cache refresh initiated successfully")
154
+ public ResponseEntity<String> refreshDashboardCache() {
155
+ log.info("REST request to refresh dashboard cache by user: {}", SecurityUtils.getAuthenticatedUserId());
156
+ dashboardService.refreshAllCaches();
157
+ return ResponseEntity.ok("Dashboard cache refresh initiated successfully");
158
+ }
159
+ }
src/main/java/com/dalab/reporting/controller/NotificationController.java ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.controller;
2
+
3
+ import java.util.List;
4
+ import java.util.UUID;
5
+
6
+ import org.slf4j.Logger;
7
+ import org.slf4j.LoggerFactory;
8
+ import org.springframework.data.domain.Page;
9
+ import org.springframework.data.domain.Pageable;
10
+ import org.springframework.http.HttpStatus;
11
+ import org.springframework.http.ResponseEntity;
12
+ import org.springframework.security.access.prepost.PreAuthorize;
13
+ // import org.springframework.security.core.annotation.AuthenticationPrincipal;
14
+ // import org.springframework.security.oauth2.jwt.Jwt;
15
+ import org.springframework.web.bind.annotation.DeleteMapping;
16
+ import org.springframework.web.bind.annotation.GetMapping;
17
+ import org.springframework.web.bind.annotation.PathVariable;
18
+ import org.springframework.web.bind.annotation.PostMapping;
19
+ import org.springframework.web.bind.annotation.RequestBody;
20
+ import org.springframework.web.bind.annotation.RequestMapping;
21
+ import org.springframework.web.bind.annotation.ResponseStatus;
22
+ import org.springframework.web.bind.annotation.RestController;
23
+
24
+ import com.dalab.common.security.SecurityUtils;
25
+ import com.dalab.reporting.dto.NotificationPreferenceDTO;
26
+ import com.dalab.reporting.service.INotificationService;
27
+
28
+ import jakarta.validation.Valid;
29
+ import lombok.RequiredArgsConstructor;
30
+
31
+ @RestController
32
+ @RequestMapping("/api/v1/reporting/notifications")
33
+ @RequiredArgsConstructor
34
+ public class NotificationController {
35
+
36
+ private static final Logger log = LoggerFactory.getLogger(NotificationController.class);
37
+
38
+ private final INotificationService notificationService;
39
+
40
+ // TODO: Replace with actual authenticated user ID retrieval
41
+ private UUID getAuthenticatedUserId(/*@AuthenticationPrincipal Jwt jwt*/) {
42
+ // return UUID.fromString(jwt.getSubject());
43
+ return UUID.randomUUID(); // Placeholder
44
+ }
45
+
46
+ @PostMapping("/preferences")
47
+ @PreAuthorize("isAuthenticated()") // User manages their own preferences
48
+ public ResponseEntity<NotificationPreferenceDTO> saveNotificationPreference(@Valid @RequestBody NotificationPreferenceDTO preferenceDTO) {
49
+ // Ensure the userId in DTO matches the authenticated user, or is set by the backend
50
+ UUID authenticatedUserId = SecurityUtils.getAuthenticatedUserId();
51
+ if (preferenceDTO.getUserId() == null) {
52
+ preferenceDTO.setUserId(authenticatedUserId);
53
+ } else if (!preferenceDTO.getUserId().equals(authenticatedUserId)) {
54
+ // Non-admins should not be able to set preferences for other users.
55
+ // Admins might have a separate endpoint or logic.
56
+ // For now, let's assume this endpoint is for the authenticated user only.
57
+ throw new org.springframework.security.access.AccessDeniedException("Cannot set preferences for another user.");
58
+ }
59
+ return new ResponseEntity<>(notificationService.saveNotificationPreference(preferenceDTO), HttpStatus.OK);
60
+ }
61
+
62
+ @GetMapping("/preferences/my")
63
+ @PreAuthorize("isAuthenticated()")
64
+ public ResponseEntity<List<NotificationPreferenceDTO>> getMyNotificationPreferences() {
65
+ UUID userId = SecurityUtils.getAuthenticatedUserId();
66
+ return ResponseEntity.ok(notificationService.getNotificationPreferencesByUserId(userId));
67
+ }
68
+
69
+ @GetMapping("/preferences/user/{userId}")
70
+ @PreAuthorize("hasRole('ADMIN')") // Admin can view others' preferences
71
+ public ResponseEntity<List<NotificationPreferenceDTO>> getUserNotificationPreferences(@PathVariable UUID userId) {
72
+ return ResponseEntity.ok(notificationService.getNotificationPreferencesByUserId(userId));
73
+ }
74
+
75
+ @GetMapping("/preferences/{preferenceId}")
76
+ @PreAuthorize("isAuthenticated()") // User can get their own, admin can get any
77
+ public ResponseEntity<NotificationPreferenceDTO> getNotificationPreferenceById(@PathVariable UUID preferenceId) {
78
+ // TODO: Add ownership check if not admin
79
+ return ResponseEntity.ok(notificationService.getNotificationPreferenceById(preferenceId));
80
+ }
81
+
82
+ @GetMapping("/preferences")
83
+ @PreAuthorize("hasRole('ADMIN')") // Admin only: get all preferences
84
+ public ResponseEntity<Page<NotificationPreferenceDTO>> getAllNotificationPreferences(Pageable pageable) {
85
+ return ResponseEntity.ok(notificationService.getAllNotificationPreferences(pageable));
86
+ }
87
+
88
+ @DeleteMapping("/preferences/{preferenceId}")
89
+ @PreAuthorize("isAuthenticated()") // User can delete their own, admin can delete any
90
+ @ResponseStatus(HttpStatus.NO_CONTENT)
91
+ public void deleteNotificationPreference(@PathVariable UUID preferenceId) {
92
+ // TODO: Add ownership check if not admin
93
+ notificationService.deleteNotificationPreference(preferenceId);
94
+ }
95
+
96
+ @PostMapping("/test/{channelName}")
97
+ @PreAuthorize("isAuthenticated()")
98
+ public ResponseEntity<String> sendTestNotification(@PathVariable String channelName) {
99
+ UUID userId = SecurityUtils.getAuthenticatedUserId();
100
+ notificationService.sendTestNotification(userId, channelName);
101
+ return ResponseEntity.ok("Test notification sent to user " + userId + " via " + channelName);
102
+ }
103
+
104
+ // Note: A general purpose POST /notifications to send ad-hoc notifications is not exposed here.
105
+ // That would typically be an internal capability or a highly restricted admin API,
106
+ // as it could be misused for spam if `NotificationRequestDTO` is directly accepted.
107
+ // Notifications are primarily driven by events, report completions, or specific system actions.
108
+ }
src/main/java/com/dalab/reporting/controller/ReportController.java ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.controller;
2
+
3
+ import java.util.UUID;
4
+
5
+ import org.slf4j.Logger;
6
+ import org.slf4j.LoggerFactory;
7
+ import org.springframework.data.domain.Page;
8
+ import org.springframework.data.domain.Pageable;
9
+ import org.springframework.http.HttpStatus;
10
+ import org.springframework.http.ResponseEntity;
11
+ import org.springframework.security.access.prepost.PreAuthorize;
12
+ // import org.springframework.security.core.annotation.AuthenticationPrincipal;
13
+ // import org.springframework.security.oauth2.jwt.Jwt;
14
+ import org.springframework.web.bind.annotation.DeleteMapping;
15
+ import org.springframework.web.bind.annotation.GetMapping;
16
+ import org.springframework.web.bind.annotation.PathVariable;
17
+ import org.springframework.web.bind.annotation.PostMapping;
18
+ import org.springframework.web.bind.annotation.PutMapping;
19
+ import org.springframework.web.bind.annotation.RequestBody;
20
+ import org.springframework.web.bind.annotation.RequestMapping;
21
+ import org.springframework.web.bind.annotation.ResponseStatus;
22
+ import org.springframework.web.bind.annotation.RestController;
23
+
24
+ import com.dalab.common.security.SecurityUtils;
25
+ import com.dalab.reporting.dto.GeneratedReportOutputDTO;
26
+ import com.dalab.reporting.dto.ReportDefinitionDTO;
27
+ import com.dalab.reporting.dto.ReportGenerationRequestDTO;
28
+ import com.dalab.reporting.service.IReportService;
29
+
30
+ import jakarta.validation.Valid;
31
+ import lombok.RequiredArgsConstructor;
32
+
33
+ @RestController
34
+ @RequestMapping("/api/v1/reporting")
35
+ @RequiredArgsConstructor
36
+ public class ReportController {
37
+
38
+ private static final Logger log = LoggerFactory.getLogger(ReportController.class);
39
+
40
+ private final IReportService reportService;
41
+
42
+ // --- Report Definitions ---
43
+
44
+ @PostMapping("/definitions")
45
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
46
+ public ResponseEntity<ReportDefinitionDTO> createReportDefinition(@Valid @RequestBody ReportDefinitionDTO reportDefinitionDTO) {
47
+ return new ResponseEntity<>(reportService.createReportDefinition(reportDefinitionDTO), HttpStatus.CREATED);
48
+ }
49
+
50
+ @GetMapping("/definitions/{id}")
51
+ @PreAuthorize("isAuthenticated()")
52
+ public ResponseEntity<ReportDefinitionDTO> getReportDefinitionById(@PathVariable UUID id) {
53
+ return ResponseEntity.ok(reportService.getReportDefinitionById(id));
54
+ }
55
+
56
+ @GetMapping("/definitions/key/{reportKey}")
57
+ @PreAuthorize("isAuthenticated()")
58
+ public ResponseEntity<ReportDefinitionDTO> getReportDefinitionByKey(@PathVariable String reportKey) {
59
+ return ResponseEntity.ok(reportService.getReportDefinitionByKey(reportKey));
60
+ }
61
+
62
+ @GetMapping("/definitions")
63
+ @PreAuthorize("isAuthenticated()")
64
+ public ResponseEntity<Page<ReportDefinitionDTO>> getAllReportDefinitions(Pageable pageable) {
65
+ return ResponseEntity.ok(reportService.getAllReportDefinitions(pageable));
66
+ }
67
+
68
+ @PutMapping("/definitions/{id}")
69
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
70
+ public ResponseEntity<ReportDefinitionDTO> updateReportDefinition(@PathVariable UUID id, @Valid @RequestBody ReportDefinitionDTO reportDefinitionDTO) {
71
+ return ResponseEntity.ok(reportService.updateReportDefinition(id, reportDefinitionDTO));
72
+ }
73
+
74
+ @DeleteMapping("/definitions/{id}")
75
+ @PreAuthorize("hasRole('ADMIN') or hasAuthority('SCOPE_manage:reports')")
76
+ @ResponseStatus(HttpStatus.NO_CONTENT)
77
+ public void deleteReportDefinition(@PathVariable UUID id) {
78
+ reportService.deleteReportDefinition(id);
79
+ }
80
+
81
+ // --- Report Generation & Retrieval ---
82
+
83
+ @PostMapping("/key/{reportKey}/generate")
84
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_ANALYST')")
85
+ public ResponseEntity<GeneratedReportOutputDTO> generateReport(
86
+ @PathVariable String reportKey,
87
+ @RequestBody(required = false) ReportGenerationRequestDTO requestDTO) {
88
+ log.info("REST request to generate report: {}", reportKey);
89
+ UUID userId = SecurityUtils.getAuthenticatedUserId();
90
+ GeneratedReportOutputDTO generatedReport = reportService.generateReport(reportKey, requestDTO, userId);
91
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(generatedReport);
92
+ }
93
+
94
+ @PostMapping("/id/{id}/generate")
95
+ @PreAuthorize("hasAnyAuthority('ROLE_ADMIN', 'ROLE_ANALYST')")
96
+ public ResponseEntity<GeneratedReportOutputDTO> regenerateReport(@PathVariable UUID id) {
97
+ log.info("REST request to regenerate report: {}", id);
98
+ UUID userId = SecurityUtils.getAuthenticatedUserId();
99
+ GeneratedReportOutputDTO generatedReport = reportService.regenerateReport(id, userId);
100
+ return ResponseEntity.status(HttpStatus.ACCEPTED).body(generatedReport);
101
+ }
102
+
103
+ @GetMapping("/reports/jobs/{generatedReportId}")
104
+ @PreAuthorize("isAuthenticated()")
105
+ public ResponseEntity<GeneratedReportOutputDTO> getGeneratedReportStatus(@PathVariable UUID generatedReportId) {
106
+ // This DTO contains status, content (if ready), etc.
107
+ return ResponseEntity.ok(reportService.getGeneratedReportById(generatedReportId));
108
+ }
109
+
110
+ @GetMapping("/reports/key/{reportKey}")
111
+ @PreAuthorize("isAuthenticated()")
112
+ public ResponseEntity<Page<GeneratedReportOutputDTO>> getGeneratedReportsByReportKey(@PathVariable String reportKey, Pageable pageable) {
113
+ return ResponseEntity.ok(reportService.getGeneratedReportsByReportKey(reportKey, pageable));
114
+ }
115
+
116
+ @GetMapping("/reports")
117
+ @PreAuthorize("isAuthenticated()") // Might restrict further based on roles for seeing ALL reports
118
+ public ResponseEntity<Page<GeneratedReportOutputDTO>> getAllGeneratedReports(Pageable pageable) {
119
+ return ResponseEntity.ok(reportService.getAllGeneratedReports(pageable));
120
+ }
121
+ }
src/main/java/com/dalab/reporting/dto/ActionItemsDTO.java ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonFormat;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Builder;
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ import java.time.LocalDateTime;
10
+ import java.util.List;
11
+
12
+ /**
13
+ * DTO for Action Items Aggregation Dashboard response
14
+ * Provides comprehensive action items from all DALab services
15
+ *
16
+ * @author DALab Development Team
17
+ * @since 2025-01-02
18
+ */
19
+ @Data
20
+ @Builder
21
+ @NoArgsConstructor
22
+ @AllArgsConstructor
23
+ public class ActionItemsDTO {
24
+
25
+ /**
26
+ * Total number of active action items
27
+ */
28
+ private Integer totalActionItems;
29
+
30
+ /**
31
+ * Action items by priority/severity
32
+ */
33
+ private ActionItemsSummary summary;
34
+
35
+ /**
36
+ * Detailed list of action items
37
+ */
38
+ private List<ActionItem> actionItems;
39
+
40
+ /**
41
+ * SLA compliance metrics
42
+ */
43
+ private SlaMetrics slaMetrics;
44
+
45
+ /**
46
+ * Business impact assessment
47
+ */
48
+ private BusinessImpactSummary businessImpact;
49
+
50
+ /**
51
+ * Last aggregation timestamp
52
+ */
53
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
54
+ private LocalDateTime lastAggregated;
55
+
56
+ /**
57
+ * Data sources included in aggregation
58
+ */
59
+ private List<String> includedServices;
60
+
61
+ @Data
62
+ @Builder
63
+ @NoArgsConstructor
64
+ @AllArgsConstructor
65
+ public static class ActionItemsSummary {
66
+ private Integer criticalCount;
67
+ private Integer highCount;
68
+ private Integer mediumCount;
69
+ private Integer lowCount;
70
+
71
+ private Integer overdueCount;
72
+ private Integer dueTodayCount;
73
+ private Integer dueThisWeekCount;
74
+
75
+ private Integer assignedCount;
76
+ private Integer unassignedCount;
77
+ private Integer inProgressCount;
78
+ private Integer completedCount;
79
+ }
80
+
81
+ @Data
82
+ @Builder
83
+ @NoArgsConstructor
84
+ @AllArgsConstructor
85
+ public static class ActionItem {
86
+ private String id;
87
+ private String title;
88
+ private String description;
89
+ private String category; // "COMPLIANCE_VIOLATION", "POLICY_BREACH", "METADATA_MISSING", "DISCOVERY_FAILED", etc.
90
+ private String severity; // "CRITICAL", "HIGH", "MEDIUM", "LOW"
91
+ private String status; // "OPEN", "IN_PROGRESS", "PENDING_APPROVAL", "COMPLETED", "DISMISSED"
92
+
93
+ private String sourceService; // "da-autocompliance", "da-policyengine", etc.
94
+ private String sourceEntityType; // "ASSET", "POLICY", "USER", "CONNECTION"
95
+ private String sourceEntityId;
96
+ private String sourceEntityName;
97
+
98
+ private String assignedTo; // user ID or team
99
+ private String assignedTeam; // "DATA_ENGINEERING", "COMPLIANCE", "SECURITY"
100
+
101
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
102
+ private LocalDateTime createdAt;
103
+
104
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
105
+ private LocalDateTime dueDate;
106
+
107
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
108
+ private LocalDateTime lastUpdated;
109
+
110
+ private String actionRequired;
111
+ private String businessJustification;
112
+ private String estimatedEffort; // "MINUTES", "HOURS", "DAYS", "WEEKS"
113
+ private Integer businessImpactScore; // 1-10
114
+
115
+ private List<String> tags;
116
+ private String detailsUrl; // link to specific service UI
117
+ }
118
+
119
+ @Data
120
+ @Builder
121
+ @NoArgsConstructor
122
+ @AllArgsConstructor
123
+ public static class SlaMetrics {
124
+ private Double averageResolutionTimeHours;
125
+ private Double slaComplianceRate; // percentage
126
+ private Integer breachedSlaCount;
127
+ private Integer nearBreachCount; // within 24 hours of SLA breach
128
+
129
+ private SlaByCategory complianceViolations;
130
+ private SlaByCategory policyBreaches;
131
+ private SlaByCategory metadataIssues;
132
+ private SlaByCategory discoveryIssues;
133
+ }
134
+
135
+ @Data
136
+ @Builder
137
+ @NoArgsConstructor
138
+ @AllArgsConstructor
139
+ public static class SlaByCategory {
140
+ private Integer totalItems;
141
+ private Integer onTimeResolved;
142
+ private Integer breachedSla;
143
+ private Double averageResolutionHours;
144
+ private Double complianceRate;
145
+ }
146
+
147
+ @Data
148
+ @Builder
149
+ @NoArgsConstructor
150
+ @AllArgsConstructor
151
+ public static class BusinessImpactSummary {
152
+ private Integer highImpactItems; // score 8-10
153
+ private Integer mediumImpactItems; // score 5-7
154
+ private Integer lowImpactItems; // score 1-4
155
+
156
+ private Double averageImpactScore;
157
+ private String highestImpactCategory;
158
+
159
+ private List<ImpactArea> impactAreas;
160
+ }
161
+
162
+ @Data
163
+ @Builder
164
+ @NoArgsConstructor
165
+ @AllArgsConstructor
166
+ public static class ImpactArea {
167
+ private String areaName; // "DATA_PRIVACY", "REGULATORY_COMPLIANCE", "OPERATIONAL_EFFICIENCY", "COST_OPTIMIZATION"
168
+ private Integer affectedItems;
169
+ private Double averageImpactScore;
170
+ private String riskLevel; // "HIGH", "MEDIUM", "LOW"
171
+ private String description;
172
+ }
173
+ }
src/main/java/com/dalab/reporting/dto/CostSavingsDTO.java ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonFormat;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Builder;
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ import java.math.BigDecimal;
10
+ import java.time.LocalDateTime;
11
+ import java.util.List;
12
+ import java.util.Map;
13
+
14
+ /**
15
+ * DTO for Cost Savings Dashboard response
16
+ * Provides comprehensive cost analysis and savings opportunities
17
+ *
18
+ * @author DALab Development Team
19
+ * @since 2025-01-02
20
+ */
21
+ @Data
22
+ @Builder
23
+ @NoArgsConstructor
24
+ @AllArgsConstructor
25
+ public class CostSavingsDTO {
26
+
27
+ /**
28
+ * Total cost savings achieved (USD)
29
+ */
30
+ private BigDecimal totalSavings;
31
+
32
+ /**
33
+ * Monthly savings rate (USD per month)
34
+ */
35
+ private BigDecimal monthlySavingsRate;
36
+
37
+ /**
38
+ * ROI percentage including platform costs
39
+ */
40
+ private Double roiPercentage;
41
+
42
+ /**
43
+ * Cost savings breakdown by category
44
+ */
45
+ private CostSavingsBreakdown breakdown;
46
+
47
+ /**
48
+ * Savings by cloud provider
49
+ */
50
+ private List<ProviderSavings> providerSavings;
51
+
52
+ /**
53
+ * Projected savings pipeline
54
+ */
55
+ private List<SavingsOpportunity> savingsOpportunities;
56
+
57
+ /**
58
+ * Historical savings trend (last 12 months)
59
+ */
60
+ private List<SavingsTrendPoint> historicalTrend;
61
+
62
+ /**
63
+ * Cost optimization recommendations
64
+ */
65
+ private List<String> recommendations;
66
+
67
+ /**
68
+ * Last calculation timestamp
69
+ */
70
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
71
+ private LocalDateTime lastCalculated;
72
+
73
+ /**
74
+ * Data accuracy indicator
75
+ */
76
+ private String dataAccuracy; // "REAL_TIME", "DAILY", "ESTIMATED"
77
+
78
+ @Data
79
+ @Builder
80
+ @NoArgsConstructor
81
+ @AllArgsConstructor
82
+ public static class CostSavingsBreakdown {
83
+
84
+ /**
85
+ * Savings from archival operations
86
+ */
87
+ private BigDecimal archivalSavings;
88
+ private Integer archivedAssetsCount;
89
+ private String archivalStorageUsed; // e.g., "2.5 TB"
90
+
91
+ /**
92
+ * Savings from deletion operations
93
+ */
94
+ private BigDecimal deletionSavings;
95
+ private Integer deletedAssetsCount;
96
+ private String storageReclaimed; // e.g., "5.2 TB"
97
+
98
+ /**
99
+ * Savings from storage tier optimization
100
+ */
101
+ private BigDecimal tierOptimizationSavings;
102
+ private Integer optimizedAssetsCount;
103
+
104
+ /**
105
+ * Compute cost reductions
106
+ */
107
+ private BigDecimal computeSavings;
108
+ private Integer decommissionedResources;
109
+
110
+ /**
111
+ * Platform operational costs
112
+ */
113
+ private BigDecimal platformCosts;
114
+ private BigDecimal netSavings; // totalSavings - platformCosts
115
+ }
116
+
117
+ @Data
118
+ @Builder
119
+ @NoArgsConstructor
120
+ @AllArgsConstructor
121
+ public static class ProviderSavings {
122
+ private String providerName; // "AWS", "GCP", "Azure", "OCI"
123
+ private BigDecimal savings;
124
+ private BigDecimal monthlyRate;
125
+ private String primarySavingsSource; // "ARCHIVAL", "DELETION", "OPTIMIZATION"
126
+ private Double contributionPercentage;
127
+ }
128
+
129
+ @Data
130
+ @Builder
131
+ @NoArgsConstructor
132
+ @AllArgsConstructor
133
+ public static class SavingsOpportunity {
134
+ private String opportunityType; // "ARCHIVAL_CANDIDATE", "DELETION_CANDIDATE", "TIER_OPTIMIZATION"
135
+ private String description;
136
+ private BigDecimal potentialSavings;
137
+ private BigDecimal monthlyImpact;
138
+ private String timeframe; // "IMMEDIATE", "30_DAYS", "90_DAYS"
139
+ private String effortLevel; // "LOW", "MEDIUM", "HIGH"
140
+ private Integer affectedAssets;
141
+ private String providerName;
142
+ private String actionRequired;
143
+ }
144
+
145
+ @Data
146
+ @Builder
147
+ @NoArgsConstructor
148
+ @AllArgsConstructor
149
+ public static class SavingsTrendPoint {
150
+ @JsonFormat(pattern = "yyyy-MM")
151
+ private LocalDateTime month;
152
+ private BigDecimal cumulativeSavings;
153
+ private BigDecimal monthlySavings;
154
+ private BigDecimal platformCosts;
155
+ private BigDecimal netSavings;
156
+ private Integer assetsProcessed;
157
+ }
158
+ }
src/main/java/com/dalab/reporting/dto/GeneratedReportOutputDTO.java ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import com.dalab.reporting.model.ReportStatus;
8
+
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ @Data
13
+ @NoArgsConstructor
14
+ public class GeneratedReportOutputDTO {
15
+ private UUID id;
16
+ private String reportKey; // From associated ReportDefinition
17
+ private String reportDisplayName; // From associated ReportDefinition
18
+ private ReportStatus status;
19
+ private Map<String, Object> generationParameters;
20
+ private Object reportContent; // Can be String (URI, CSV) or Map/Object (JSON)
21
+ private String failureReason;
22
+ private Instant requestedAt;
23
+ private Instant startedAt;
24
+ private Instant completedAt;
25
+ private UUID requestedByUserId;
26
+ }
src/main/java/com/dalab/reporting/dto/GovernanceScoreDTO.java ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import com.fasterxml.jackson.annotation.JsonFormat;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Builder;
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ import java.time.LocalDateTime;
10
+ import java.util.List;
11
+ import java.util.Map;
12
+
13
+ /**
14
+ * DTO for Governance Score Dashboard response
15
+ * Provides comprehensive governance metrics and breakdown
16
+ *
17
+ * @author DALab Development Team
18
+ * @since 2025-01-02
19
+ */
20
+ @Data
21
+ @Builder
22
+ @NoArgsConstructor
23
+ @AllArgsConstructor
24
+ public class GovernanceScoreDTO {
25
+
26
+ /**
27
+ * Overall governance score (0-100)
28
+ * Weighted calculation: Policy Compliance 30%, Data Classification 25%,
29
+ * Metadata Completeness 25%, Lineage Coverage 20%
30
+ */
31
+ private Double overallScore;
32
+
33
+ /**
34
+ * Governance score breakdown by category
35
+ */
36
+ private GovernanceBreakdown breakdown;
37
+
38
+ /**
39
+ * Historical trend data (last 30 days)
40
+ */
41
+ private List<GovernanceTrendPoint> historicalTrend;
42
+
43
+ /**
44
+ * Top governance issues requiring attention
45
+ */
46
+ private List<GovernanceIssue> topIssues;
47
+
48
+ /**
49
+ * Benchmark comparison against industry standards
50
+ */
51
+ private BenchmarkComparison benchmark;
52
+
53
+ /**
54
+ * Score improvement recommendations
55
+ */
56
+ private List<String> recommendations;
57
+
58
+ /**
59
+ * Last calculation timestamp
60
+ */
61
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
62
+ private LocalDateTime lastCalculated;
63
+
64
+ /**
65
+ * Data freshness indicator (minutes since last update)
66
+ */
67
+ private Integer dataFreshnessMinutes;
68
+
69
+ @Data
70
+ @Builder
71
+ @NoArgsConstructor
72
+ @AllArgsConstructor
73
+ public static class GovernanceBreakdown {
74
+
75
+ /**
76
+ * Policy compliance score (30% weight)
77
+ */
78
+ private Double policyComplianceScore;
79
+ private Integer activePolicies;
80
+ private Integer violationsCount;
81
+ private Double complianceRate;
82
+
83
+ /**
84
+ * Data classification score (25% weight)
85
+ */
86
+ private Double dataClassificationScore;
87
+ private Integer classifiedAssets;
88
+ private Integer totalAssets;
89
+ private Double classificationRate;
90
+
91
+ /**
92
+ * Metadata completeness score (25% weight)
93
+ */
94
+ private Double metadataCompletenessScore;
95
+ private Integer assetsWithMetadata;
96
+ private Integer assetsWithBusinessContext;
97
+ private Double metadataCompletionRate;
98
+
99
+ /**
100
+ * Lineage coverage score (20% weight)
101
+ */
102
+ private Double lineageCoverageScore;
103
+ private Integer assetsWithLineage;
104
+ private Integer totalLineageConnections;
105
+ private Double lineageCoverageRate;
106
+ }
107
+
108
+ @Data
109
+ @Builder
110
+ @NoArgsConstructor
111
+ @AllArgsConstructor
112
+ public static class GovernanceTrendPoint {
113
+ @JsonFormat(pattern = "yyyy-MM-dd")
114
+ private LocalDateTime date;
115
+ private Double score;
116
+ private String period; // "daily", "weekly"
117
+ }
118
+
119
+ @Data
120
+ @Builder
121
+ @NoArgsConstructor
122
+ @AllArgsConstructor
123
+ public static class GovernanceIssue {
124
+ private String category; // "POLICY_VIOLATION", "MISSING_CLASSIFICATION", "INCOMPLETE_METADATA", "MISSING_LINEAGE"
125
+ private String description;
126
+ private String severity; // "HIGH", "MEDIUM", "LOW"
127
+ private Integer affectedAssets;
128
+ private String actionRequired;
129
+ private String assignedTeam;
130
+ }
131
+
132
+ @Data
133
+ @Builder
134
+ @NoArgsConstructor
135
+ @AllArgsConstructor
136
+ public static class BenchmarkComparison {
137
+ private Double industryAverage;
138
+ private Double peerAverage;
139
+ private String ranking; // "EXCELLENT", "GOOD", "AVERAGE", "BELOW_AVERAGE", "POOR"
140
+ private Integer percentile; // 0-100
141
+ private String comparisonNote;
142
+ }
143
+ }
src/main/java/com/dalab/reporting/dto/NotificationPreferenceDTO.java ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import com.dalab.reporting.model.NotificationChannel;
8
+
9
+ import jakarta.validation.constraints.NotBlank;
10
+ import jakarta.validation.constraints.NotNull;
11
+ import jakarta.validation.constraints.Size;
12
+ import lombok.Data;
13
+ import lombok.NoArgsConstructor;
14
+
15
+ @Data
16
+ @NoArgsConstructor
17
+ public class NotificationPreferenceDTO {
18
+ private UUID id;
19
+
20
+ @NotNull(message = "User ID cannot be null")
21
+ private UUID userId;
22
+
23
+ @NotBlank(message = "Notification type key cannot be blank")
24
+ @Size(max = 100, message = "Notification type key must be less than 100 characters")
25
+ private String notificationTypeKey;
26
+
27
+ @NotNull(message = "Channel cannot be null")
28
+ private NotificationChannel channel;
29
+
30
+ private boolean enabled;
31
+
32
+ private Map<String, String> channelConfiguration;
33
+
34
+ private Instant createdAt;
35
+ private Instant updatedAt;
36
+ }
src/main/java/com/dalab/reporting/dto/NotificationRequestDTO.java ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import com.dalab.reporting.model.NotificationChannel;
4
+ import jakarta.validation.constraints.NotBlank;
5
+ import jakarta.validation.constraints.NotEmpty;
6
+ import lombok.Builder;
7
+ import lombok.Data;
8
+
9
+ import java.util.List;
10
+ import java.util.Map;
11
+ import java.util.UUID;
12
+
13
+ @Data
14
+ @Builder
15
+ public class NotificationRequestDTO {
16
+ // Target specific user(s) by their IDs
17
+ private List<UUID> targetUserIds;
18
+
19
+ // Or target a specific notification type key (service will find subscribed users)
20
+ private String notificationTypeKey;
21
+
22
+ @NotEmpty(message = "At least one notification channel must be specified.")
23
+ private List<NotificationChannel> channels; // e.g., [EMAIL, SLACK]
24
+
25
+ @NotBlank(message = "Subject cannot be blank")
26
+ private String subject;
27
+
28
+ @NotBlank(message = "Body cannot be blank")
29
+ private String body;
30
+
31
+ // Optional: For rich content or structured data in notifications (e.g., Slack blocks, email template model)
32
+ private Map<String, Object> contentModel;
33
+
34
+ // Optional: Specific channel configurations to use for this notification, overriding user preferences or defaults.
35
+ // Key: "EMAIL" or "SLACK". Value: Map, e.g. for SLACK: {"targetChannelId": "#alerts"}
36
+ private Map<String, Map<String, String>> channelOverrides;
37
+ }
src/main/java/com/dalab/reporting/dto/ReportDefinitionDTO.java ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import jakarta.validation.constraints.NotBlank;
8
+ import jakarta.validation.constraints.Size;
9
+ import lombok.Data;
10
+ import lombok.NoArgsConstructor;
11
+
12
+ @Data
13
+ @NoArgsConstructor
14
+ public class ReportDefinitionDTO {
15
+ private UUID id;
16
+
17
+ @NotBlank(message = "Report key cannot be blank")
18
+ @Size(max = 100, message = "Report key must be less than 100 characters")
19
+ private String reportKey;
20
+
21
+ @NotBlank(message = "Display name cannot be blank")
22
+ @Size(max = 255, message = "Display name must be less than 255 characters")
23
+ private String displayName;
24
+
25
+ @Size(max = 1000, message = "Description must be less than 1000 characters")
26
+ private String description;
27
+
28
+ private Map<String, Object> defaultParameters;
29
+
30
+ @Size(max = 100, message = "Default schedule cron must be less than 100 characters")
31
+ private String defaultScheduleCron;
32
+
33
+ private boolean enabled;
34
+ private Instant createdAt;
35
+ private Instant updatedAt;
36
+ }
src/main/java/com/dalab/reporting/dto/ReportGenerationRequestDTO.java ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.dto;
2
+
3
+ import java.util.Map;
4
+ import java.util.UUID;
5
+
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ @Data
10
+ @NoArgsConstructor
11
+ public class ReportGenerationRequestDTO {
12
+ // This DTO is primarily for triggering. For output, see GeneratedReportOutputDTO.
13
+
14
+ // Can be used to specify parameters overriding the ReportDefinition's defaults
15
+ private Map<String, Object> parameters;
16
+
17
+ // Optional: if a specific user ID should be associated with this request
18
+ // Otherwise, it might be inferred from the authenticated context or be system-initiated.
19
+ private UUID requestedByUserId;
20
+
21
+ }
src/main/java/com/dalab/reporting/event/PolicyActionEventDTO.java ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.event;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import lombok.Data;
8
+ import lombok.NoArgsConstructor;
9
+
10
+ // Placeholder DTO - ideally this would come from da-protos or a shared library
11
+ @Data
12
+ @NoArgsConstructor
13
+ public class PolicyActionEventDTO {
14
+ private UUID eventId;
15
+ private Instant eventTimestamp;
16
+ private String policyId;
17
+ private String policyName;
18
+ private String ruleId; // Optional, if action is rule-specific
19
+ private String ruleName; // Optional
20
+ private UUID targetAssetId;
21
+ private String actionType; // e.g., "NOTIFY_ADMIN", "AUTO_TAG", "QUARANTINE_ASSET"
22
+ private Map<String, Object> actionParameters; // Specifics for the action
23
+ private String evaluationStatus; // e.g., "FAIL", "WARN"
24
+ private Map<String, Object> evaluationDetails; // e.g., what conditions triggered it
25
+ }
src/main/java/com/dalab/reporting/kafka/PolicyActionConsumer.java ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.kafka;
2
+
3
+ import java.util.HashMap;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+
7
+ import org.slf4j.Logger;
8
+ import org.slf4j.LoggerFactory;
9
+ import org.springframework.beans.factory.annotation.Autowired;
10
+ import org.springframework.kafka.annotation.KafkaListener;
11
+ import org.springframework.messaging.handler.annotation.Payload;
12
+ import org.springframework.stereotype.Service;
13
+
14
+ import com.dalab.reporting.dto.NotificationRequestDTO;
15
+ import com.dalab.reporting.model.NotificationChannel;
16
+ import com.dalab.reporting.service.INotificationService;
17
+
18
+ @Service
19
+ public class PolicyActionConsumer {
20
+
21
+ private static final Logger log = LoggerFactory.getLogger(PolicyActionConsumer.class);
22
+
23
+ private final INotificationService notificationService;
24
+
25
+ @Autowired
26
+ public PolicyActionConsumer(INotificationService notificationService) {
27
+ this.notificationService = notificationService;
28
+ }
29
+
30
+ // Updated to consume Map-based events instead of protobuf
31
+ @KafkaListener(topics = "${app.kafka.topic.policy-action-event:dalab.policies.actions}",
32
+ groupId = "${spring.kafka.consumer.group-id}")
33
+ public void handlePolicyActionEvent(@Payload Map<String, Object> event) {
34
+ log.info("Received PolicyActionEvent: AssetId={}, ActionType={}",
35
+ event.get("assetId"), event.get("actionType"));
36
+
37
+ String actionType = (String) event.get("actionType");
38
+ String policyId = (String) event.get("policyId");
39
+ String assetId = (String) event.get("assetId");
40
+
41
+ if (actionType != null && actionType.toUpperCase().startsWith("NOTIFY")) {
42
+ String subject = String.format("Policy Alert on Asset %s (Policy: %s)", assetId, policyId);
43
+
44
+ StringBuilder body = new StringBuilder();
45
+ body.append(String.format("Policy ID '%s' triggered action: %s\n", policyId, actionType));
46
+ body.append(String.format("Target Asset ID: %s\n", assetId));
47
+ if (event.containsKey("evaluationId")) {
48
+ body.append(String.format("Evaluation ID: %s\n", event.get("evaluationId")));
49
+ }
50
+
51
+ Map<String, Object> params = event;
52
+ if (params != null && !params.isEmpty()) {
53
+ body.append("Action Parameters:\n");
54
+ params.forEach((k, v) -> {
55
+ body.append(String.format(" %s: %s\n", k, valueToString(v)));
56
+ });
57
+ }
58
+ if (event.containsKey("eventTimestamp")) {
59
+ String eventTimestamp = (String) event.get("eventTimestamp");
60
+ body.append(String.format("Event Timestamp: %s\n", eventTimestamp));
61
+ }
62
+
63
+ NotificationRequestDTO.NotificationRequestDTOBuilder requestBuilder = NotificationRequestDTO.builder()
64
+ .subject(subject)
65
+ .body(body.toString());
66
+
67
+ Map<String, Object> contentModelForTemplate = new HashMap<>();
68
+ contentModelForTemplate.put("policyId", policyId);
69
+ contentModelForTemplate.put("assetId", assetId);
70
+ contentModelForTemplate.put("actionType", actionType);
71
+ if (event.containsKey("evaluationId")) contentModelForTemplate.put("evaluationId", event.get("evaluationId"));
72
+
73
+ Map<String, String> simpleParams = new HashMap<>();
74
+ params.forEach((k,v) -> simpleParams.put(k, valueToString(v)));
75
+ contentModelForTemplate.put("actionParameters", simpleParams);
76
+ if (event.containsKey("eventTimestamp")) {
77
+ String eventTimestamp = (String) event.get("eventTimestamp");
78
+ contentModelForTemplate.put("eventTimestamp", eventTimestamp);
79
+ }
80
+ requestBuilder.contentModel(contentModelForTemplate);
81
+
82
+ String notificationKeyForPolicyAlerts = "POLICY_ALERT";
83
+ Object notificationKeyValue = params.get("notificationTypeKey");
84
+ if (notificationKeyValue instanceof String) {
85
+ notificationKeyForPolicyAlerts = (String) notificationKeyValue;
86
+ } else {
87
+ log.warn("No specific notificationTypeKey in actionParameters for policy alert, using default key: {}", notificationKeyForPolicyAlerts);
88
+ }
89
+ requestBuilder.notificationTypeKey(notificationKeyForPolicyAlerts);
90
+
91
+ Object channelsValue = params.get("channels");
92
+ if (channelsValue instanceof List) {
93
+ try {
94
+ List<String> channelNames = (List<String>) channelsValue;
95
+ requestBuilder.channels(channelNames.stream().map(s -> NotificationChannel.valueOf(s.toUpperCase())).toList());
96
+ } catch (Exception e) {
97
+ log.warn("Could not parse 'channels' from actionParameters, defaulting to EMAIL and SLACK. Error: {}", e.getMessage());
98
+ requestBuilder.channels(List.of(NotificationChannel.EMAIL, NotificationChannel.SLACK));
99
+ }
100
+ } else {
101
+ requestBuilder.channels(List.of(NotificationChannel.EMAIL, NotificationChannel.SLACK));
102
+ }
103
+
104
+ try {
105
+ notificationService.sendNotification(requestBuilder.build());
106
+ log.info("Notification sent for PolicyActionEvent on assetId: {}", assetId);
107
+ } catch (Exception e) {
108
+ log.error("Failed to send notification for PolicyActionEvent on assetId {}: {}", assetId, e.getMessage(), e);
109
+ }
110
+ }
111
+
112
+ // TODO: Add logic here to feed data into internal reporting tables based on the event.
113
+ // e.g., increment compliance failure counts, log governance actions, etc.
114
+ // This would involve new JPA entities/repositories for aggregated report data.
115
+ log.info("Further processing for PolicyActionEvent on assetId {} (e.g. report data aggregation) can be added here.", assetId);
116
+ }
117
+
118
+ // Helper method to convert protobuf Value to String for logging/display
119
+ private String valueToString(Object value) {
120
+ if (value == null) return "null";
121
+ if (value instanceof String) return (String) value;
122
+ if (value instanceof Number) return value.toString();
123
+ if (value instanceof Boolean) return Boolean.toString((Boolean) value);
124
+ if (value instanceof Map) {
125
+ StringBuilder sb = new StringBuilder();
126
+ sb.append("{");
127
+ ((Map<?, ?>) value).forEach((k, v) -> {
128
+ sb.append("\"").append(k).append("\": ").append(valueToString(v)).append(", ");
129
+ });
130
+ sb.append("}");
131
+ return sb.toString();
132
+ }
133
+ if (value instanceof List) {
134
+ StringBuilder sb = new StringBuilder();
135
+ sb.append("[");
136
+ for (Object item : (List<?>) value) {
137
+ sb.append(valueToString(item)).append(", ");
138
+ }
139
+ sb.append("]");
140
+ return sb.toString();
141
+ }
142
+ return "<unknown_value_type>";
143
+ }
144
+ }
src/main/java/com/dalab/reporting/mapper/NotificationPreferenceMapper.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.mapper;
2
+
3
+ import java.util.List;
4
+
5
+ import org.mapstruct.Mapper;
6
+ import org.mapstruct.Mapping;
7
+ import org.mapstruct.factory.Mappers;
8
+
9
+ import com.dalab.reporting.dto.NotificationPreferenceDTO;
10
+ import com.dalab.reporting.model.UserNotificationPreference;
11
+
12
+ @Mapper(componentModel = "spring")
13
+ public interface NotificationPreferenceMapper {
14
+
15
+ NotificationPreferenceMapper INSTANCE = Mappers.getMapper(NotificationPreferenceMapper.class);
16
+
17
+ NotificationPreferenceDTO toDTO(UserNotificationPreference entity);
18
+ UserNotificationPreference toEntity(NotificationPreferenceDTO dto);
19
+ List<NotificationPreferenceDTO> toDTOList(List<UserNotificationPreference> entityList);
20
+ }
src/main/java/com/dalab/reporting/mapper/ReportMapper.java ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.mapper;
2
+
3
+ import com.dalab.reporting.dto.GeneratedReportOutputDTO;
4
+ import com.dalab.reporting.dto.ReportDefinitionDTO;
5
+ import com.dalab.reporting.model.GeneratedReport;
6
+ import com.dalab.reporting.model.ReportDefinition;
7
+ import org.mapstruct.Mapper;
8
+ import org.mapstruct.Mapping;
9
+ import org.mapstruct.factory.Mappers;
10
+
11
+ import java.util.List;
12
+
13
+ @Mapper(componentModel = "spring")
14
+ public interface ReportMapper {
15
+
16
+ ReportMapper INSTANCE = Mappers.getMapper(ReportMapper.class);
17
+
18
+ // ReportDefinition Mappings
19
+ ReportDefinitionDTO toDTO(ReportDefinition entity);
20
+ ReportDefinition toEntity(ReportDefinitionDTO dto);
21
+ List<ReportDefinitionDTO> toDTOList(List<ReportDefinition> entityList);
22
+
23
+ // GeneratedReport Mappings
24
+ @Mapping(source = "reportDefinition.reportKey", target = "reportKey")
25
+ @Mapping(source = "reportDefinition.displayName", target = "reportDisplayName")
26
+ GeneratedReportOutputDTO toDTO(GeneratedReport entity);
27
+ List<GeneratedReportOutputDTO> toGeneratedReportDTOList(List<GeneratedReport> entityList);
28
+
29
+ // We typically don't map from GeneratedReportOutputDTO back to GeneratedReport entity directly,
30
+ // as creation/update paths for GeneratedReport are usually more complex and handled by services.
31
+ }
src/main/java/com/dalab/reporting/model/GeneratedReport.java ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.model;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import org.hibernate.annotations.JdbcTypeCode;
8
+ import org.hibernate.type.SqlTypes;
9
+
10
+ import jakarta.persistence.*;
11
+ import jakarta.validation.constraints.NotNull;
12
+
13
+ @Entity
14
+ @Table(name = "generated_reports")
15
+ public class GeneratedReport {
16
+
17
+ @Id
18
+ @GeneratedValue(strategy = GenerationType.AUTO)
19
+ @Column(columnDefinition = "UUID")
20
+ private UUID id;
21
+
22
+ @NotNull
23
+ @ManyToOne(fetch = FetchType.LAZY)
24
+ @JoinColumn(name = "report_definition_id", nullable = false)
25
+ private ReportDefinition reportDefinition;
26
+
27
+ @Enumerated(EnumType.STRING)
28
+ @Column(nullable = false)
29
+ private ReportStatus status;
30
+
31
+ // Parameters used for this specific generation instance
32
+ @JdbcTypeCode(SqlTypes.JSON)
33
+ @Column(columnDefinition = "jsonb")
34
+ private Map<String, Object> generationParameters;
35
+
36
+ // Store the report content directly if it's small (e.g., JSON/CSV snippet)
37
+ // For larger reports, this might store a path/URI to the report file (e.g., S3 link)
38
+ @Lob
39
+ @JdbcTypeCode(SqlTypes.JSON) // Or SqlTypes.VARCHAR for path, or SqlTypes.BINARY for blob
40
+ @Column(columnDefinition = "jsonb") // Or TEXT / BYTEA depending on content type
41
+ private String reportContent; // Could be JSON, CSV string, or a URI
42
+
43
+ @Column(columnDefinition = "TEXT")
44
+ private String failureReason; // If status is FAILED
45
+
46
+ @Column(nullable = false, updatable = false)
47
+ private Instant requestedAt; // When the report generation was requested/scheduled
48
+
49
+ private Instant startedAt;
50
+ private Instant completedAt;
51
+
52
+ // Could be the user ID who requested it, or system if scheduled
53
+ private UUID requestedByUserId;
54
+
55
+ // Getters and Setters
56
+
57
+ public UUID getId() {
58
+ return id;
59
+ }
60
+
61
+ public void setId(UUID id) {
62
+ this.id = id;
63
+ }
64
+
65
+ public ReportDefinition getReportDefinition() {
66
+ return reportDefinition;
67
+ }
68
+
69
+ public void setReportDefinition(ReportDefinition reportDefinition) {
70
+ this.reportDefinition = reportDefinition;
71
+ }
72
+
73
+ public ReportStatus getStatus() {
74
+ return status;
75
+ }
76
+
77
+ public void setStatus(ReportStatus status) {
78
+ this.status = status;
79
+ }
80
+
81
+ public Map<String, Object> getGenerationParameters() {
82
+ return generationParameters;
83
+ }
84
+
85
+ public void setGenerationParameters(Map<String, Object> generationParameters) {
86
+ this.generationParameters = generationParameters;
87
+ }
88
+
89
+ public String getReportContent() {
90
+ return reportContent;
91
+ }
92
+
93
+ public void setReportContent(String reportContent) {
94
+ this.reportContent = reportContent;
95
+ }
96
+
97
+ public String getFailureReason() {
98
+ return failureReason;
99
+ }
100
+
101
+ public void setFailureReason(String failureReason) {
102
+ this.failureReason = failureReason;
103
+ }
104
+
105
+ public Instant getRequestedAt() {
106
+ return requestedAt;
107
+ }
108
+
109
+ public void setRequestedAt(Instant requestedAt) {
110
+ this.requestedAt = requestedAt;
111
+ }
112
+
113
+ public Instant getStartedAt() {
114
+ return startedAt;
115
+ }
116
+
117
+ public void setStartedAt(Instant startedAt) {
118
+ this.startedAt = startedAt;
119
+ }
120
+
121
+ public Instant getCompletedAt() {
122
+ return completedAt;
123
+ }
124
+
125
+ public void setCompletedAt(Instant completedAt) {
126
+ this.completedAt = completedAt;
127
+ }
128
+
129
+ public UUID getRequestedByUserId() {
130
+ return requestedByUserId;
131
+ }
132
+
133
+ public void setRequestedByUserId(UUID requestedByUserId) {
134
+ this.requestedByUserId = requestedByUserId;
135
+ }
136
+
137
+ @PrePersist
138
+ protected void onCreate() {
139
+ if (requestedAt == null) {
140
+ requestedAt = Instant.now();
141
+ }
142
+ if (status == null) {
143
+ status = ReportStatus.PENDING;
144
+ }
145
+ }
146
+ }
src/main/java/com/dalab/reporting/model/NotificationChannel.java ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.model;
2
+
3
+ public enum NotificationChannel {
4
+ EMAIL,
5
+ SLACK,
6
+ // IN_APP, // Future: for notifications within a UI
7
+ // TEAMS // Future: Microsoft Teams
8
+ }
src/main/java/com/dalab/reporting/model/ReportDefinition.java ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.model;
2
+
3
+ import java.time.Instant;
4
+ import java.util.Map;
5
+ import java.util.UUID;
6
+
7
+ import org.hibernate.annotations.JdbcTypeCode;
8
+ import org.hibernate.type.SqlTypes;
9
+
10
+ import jakarta.persistence.*;
11
+ import jakarta.validation.constraints.NotBlank;
12
+ import jakarta.validation.constraints.Size;
13
+
14
+ @Entity
15
+ @Table(name = "report_definitions")
16
+ public class ReportDefinition {
17
+
18
+ @Id
19
+ @GeneratedValue(strategy = GenerationType.AUTO)
20
+ @Column(columnDefinition = "UUID")
21
+ private UUID id;
22
+
23
+ @NotBlank
24
+ @Size(max = 100)
25
+ @Column(nullable = false, unique = true)
26
+ private String reportKey; // e.g., "COST_OPTIMIZATION", "COMPLIANCE_POSTURE"
27
+
28
+ @NotBlank
29
+ @Size(max = 255)
30
+ @Column(nullable = false)
31
+ private String displayName;
32
+
33
+ @Size(max = 1000)
34
+ private String description;
35
+
36
+ // Default parameters for generating this report, e.g., time range, specific filters
37
+ @JdbcTypeCode(SqlTypes.JSON)
38
+ @Column(columnDefinition = "jsonb")
39
+ private Map<String, Object> defaultParameters;
40
+
41
+ // Default cron schedule for automated generation, can be null if only manually triggered
42
+ @Size(max = 100)
43
+ private String defaultScheduleCron;
44
+
45
+ private boolean enabled = true; // Whether this report type is generally available
46
+
47
+ @Column(nullable = false, updatable = false)
48
+ private Instant createdAt;
49
+
50
+ private Instant updatedAt;
51
+
52
+ // Getters and Setters
53
+ public UUID getId() {
54
+ return id;
55
+ }
56
+
57
+ public void setId(UUID id) {
58
+ this.id = id;
59
+ }
60
+
61
+ public String getReportKey() {
62
+ return reportKey;
63
+ }
64
+
65
+ public void setReportKey(String reportKey) {
66
+ this.reportKey = reportKey;
67
+ }
68
+
69
+ public String getDisplayName() {
70
+ return displayName;
71
+ }
72
+
73
+ public void setDisplayName(String displayName) {
74
+ this.displayName = displayName;
75
+ }
76
+
77
+ public String getDescription() {
78
+ return description;
79
+ }
80
+
81
+ public void setDescription(String description) {
82
+ this.description = description;
83
+ }
84
+
85
+ public Map<String, Object> getDefaultParameters() {
86
+ return defaultParameters;
87
+ }
88
+
89
+ public void setDefaultParameters(Map<String, Object> defaultParameters) {
90
+ this.defaultParameters = defaultParameters;
91
+ }
92
+
93
+ public String getDefaultScheduleCron() {
94
+ return defaultScheduleCron;
95
+ }
96
+
97
+ public void setDefaultScheduleCron(String defaultScheduleCron) {
98
+ this.defaultScheduleCron = defaultScheduleCron;
99
+ }
100
+
101
+ public boolean isEnabled() {
102
+ return enabled;
103
+ }
104
+
105
+ public void setEnabled(boolean enabled) {
106
+ this.enabled = enabled;
107
+ }
108
+
109
+ public Instant getCreatedAt() {
110
+ return createdAt;
111
+ }
112
+
113
+ public void setCreatedAt(Instant createdAt) {
114
+ this.createdAt = createdAt;
115
+ }
116
+
117
+ public Instant getUpdatedAt() {
118
+ return updatedAt;
119
+ }
120
+
121
+ public void setUpdatedAt(Instant updatedAt) {
122
+ this.updatedAt = updatedAt;
123
+ }
124
+
125
+ @PrePersist
126
+ protected void onCreate() {
127
+ createdAt = Instant.now();
128
+ updatedAt = Instant.now();
129
+ }
130
+
131
+ @PreUpdate
132
+ protected void onUpdate() {
133
+ updatedAt = Instant.now();
134
+ }
135
+ }
src/main/java/com/dalab/reporting/model/ReportStatus.java ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.model;
2
+
3
+ public enum ReportStatus {
4
+ PENDING, // Report generation has been requested/scheduled but not yet started
5
+ GENERATING, // Report generation is in progress
6
+ COMPLETED, // Report generation finished successfully
7
+ FAILED, // Report generation failed
8
+ CANCELLED // Report generation was cancelled
9
+ }
src/main/java/com/dalab/reporting/model/UserNotificationPreference.java ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.model;
2
+
3
+ import jakarta.persistence.*;
4
+ import jakarta.validation.constraints.NotBlank;
5
+ import jakarta.validation.constraints.NotNull;
6
+ import jakarta.validation.constraints.Size;
7
+ import org.hibernate.annotations.JdbcTypeCode;
8
+ import org.hibernate.type.SqlTypes;
9
+
10
+ import java.time.Instant;
11
+ import java.util.Map;
12
+ import java.util.UUID;
13
+
14
+ @Entity
15
+ @Table(name = "user_notification_preferences",
16
+ uniqueConstraints = {
17
+ @UniqueConstraint(columnNames = {"user_id", "notification_type_key", "channel"})
18
+ })
19
+ public class UserNotificationPreference {
20
+
21
+ @Id
22
+ @GeneratedValue(strategy = GenerationType.AUTO)
23
+ @Column(columnDefinition = "UUID")
24
+ private UUID id;
25
+
26
+ @NotNull
27
+ @Column(name = "user_id", nullable = false)
28
+ private UUID userId; // The ID of the user (from Keycloak or central user management)
29
+
30
+ @NotBlank
31
+ @Size(max = 100)
32
+ @Column(name = "notification_type_key", nullable = false)
33
+ private String notificationTypeKey; // e.g., "REPORT_COMPLETED_COST_OPTIMIZATION", "POLICY_ALERT_HIGH_SEVERITY"
34
+
35
+ @NotNull
36
+ @Enumerated(EnumType.STRING)
37
+ @Column(nullable = false)
38
+ private NotificationChannel channel; // EMAIL, SLACK
39
+
40
+ private boolean enabled = true;
41
+
42
+ // Channel-specific details, e.g., for SLACK, it could be { "channelId": "C123", "userId": "U456" }
43
+ // For EMAIL, it might be overridden by a central user profile email but could allow specific addresses here.
44
+ @JdbcTypeCode(SqlTypes.JSON)
45
+ @Column(columnDefinition = "jsonb")
46
+ private Map<String, String> channelConfiguration; // e.g., Slack channel ID, specific email address
47
+
48
+ @Column(nullable = false, updatable = false)
49
+ private Instant createdAt;
50
+
51
+ private Instant updatedAt;
52
+
53
+ // Getters and Setters
54
+
55
+ public UUID getId() {
56
+ return id;
57
+ }
58
+
59
+ public void setId(UUID id) {
60
+ this.id = id;
61
+ }
62
+
63
+ public UUID getUserId() {
64
+ return userId;
65
+ }
66
+
67
+ public void setUserId(UUID userId) {
68
+ this.userId = userId;
69
+ }
70
+
71
+ public String getNotificationTypeKey() {
72
+ return notificationTypeKey;
73
+ }
74
+
75
+ public void setNotificationTypeKey(String notificationTypeKey) {
76
+ this.notificationTypeKey = notificationTypeKey;
77
+ }
78
+
79
+ public NotificationChannel getChannel() {
80
+ return channel;
81
+ }
82
+
83
+ public void setChannel(NotificationChannel channel) {
84
+ this.channel = channel;
85
+ }
86
+
87
+ public boolean isEnabled() {
88
+ return enabled;
89
+ }
90
+
91
+ public void setEnabled(boolean enabled) {
92
+ this.enabled = enabled;
93
+ }
94
+
95
+ public Map<String, String> getChannelConfiguration() {
96
+ return channelConfiguration;
97
+ }
98
+
99
+ public void setChannelConfiguration(Map<String, String> channelConfiguration) {
100
+ this.channelConfiguration = channelConfiguration;
101
+ }
102
+
103
+ public Instant getCreatedAt() {
104
+ return createdAt;
105
+ }
106
+
107
+ public void setCreatedAt(Instant createdAt) {
108
+ this.createdAt = createdAt;
109
+ }
110
+
111
+ public Instant getUpdatedAt() {
112
+ return updatedAt;
113
+ }
114
+
115
+ public void setUpdatedAt(Instant updatedAt) {
116
+ this.updatedAt = updatedAt;
117
+ }
118
+
119
+ @PrePersist
120
+ protected void onCreate() {
121
+ createdAt = Instant.now();
122
+ updatedAt = Instant.now();
123
+ }
124
+
125
+ @PreUpdate
126
+ protected void onUpdate() {
127
+ updatedAt = Instant.now();
128
+ }
129
+ }
src/main/java/com/dalab/reporting/repository/GeneratedReportRepository.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.repository;
2
+
3
+ import java.util.UUID;
4
+
5
+ import org.springframework.data.domain.Page;
6
+ import org.springframework.data.domain.Pageable;
7
+ import org.springframework.data.jpa.repository.JpaRepository;
8
+ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
9
+ import org.springframework.stereotype.Repository;
10
+
11
+ import com.dalab.reporting.model.GeneratedReport;
12
+ import com.dalab.reporting.model.ReportDefinition;
13
+ import com.dalab.reporting.model.ReportStatus;
14
+
15
+ @Repository
16
+ public interface GeneratedReportRepository extends JpaRepository<GeneratedReport, UUID>, JpaSpecificationExecutor<GeneratedReport> {
17
+ Page<GeneratedReport> findByReportDefinition(ReportDefinition reportDefinition, Pageable pageable);
18
+ Page<GeneratedReport> findByStatus(ReportStatus status, Pageable pageable);
19
+ Page<GeneratedReport> findByRequestedByUserId(UUID userId, Pageable pageable);
20
+ }
src/main/java/com/dalab/reporting/repository/ReportDefinitionRepository.java ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.repository;
2
+
3
+ import java.util.Optional;
4
+ import java.util.UUID;
5
+
6
+ import org.springframework.data.jpa.repository.JpaRepository;
7
+ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
8
+ import org.springframework.stereotype.Repository;
9
+
10
+ import com.dalab.reporting.model.ReportDefinition;
11
+
12
+ @Repository
13
+ public interface ReportDefinitionRepository extends JpaRepository<ReportDefinition, UUID>, JpaSpecificationExecutor<ReportDefinition> {
14
+ Optional<ReportDefinition> findByReportKey(String reportKey);
15
+ }
src/main/java/com/dalab/reporting/repository/UserNotificationPreferenceRepository.java ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.repository;
2
+
3
+ import java.util.List;
4
+ import java.util.Optional;
5
+ import java.util.UUID;
6
+
7
+ import org.springframework.data.jpa.repository.JpaRepository;
8
+ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
9
+ import org.springframework.stereotype.Repository;
10
+
11
+ import com.dalab.reporting.model.NotificationChannel;
12
+ import com.dalab.reporting.model.UserNotificationPreference;
13
+
14
+ @Repository
15
+ public interface UserNotificationPreferenceRepository extends JpaRepository<UserNotificationPreference, UUID>, JpaSpecificationExecutor<UserNotificationPreference> {
16
+ List<UserNotificationPreference> findByUserId(UUID userId);
17
+ Optional<UserNotificationPreference> findByUserIdAndNotificationTypeKeyAndChannel(UUID userId, String notificationTypeKey, NotificationChannel channel);
18
+ List<UserNotificationPreference> findByNotificationTypeKeyAndChannelAndEnabled(String notificationTypeKey, NotificationChannel channel, boolean enabled);
19
+ List<UserNotificationPreference> findByUserIdAndEnabled(UUID userId, boolean enabled);
20
+ }
src/main/java/com/dalab/reporting/service/DashboardService.java ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service;
2
+
3
+ import org.slf4j.Logger;
4
+ import org.slf4j.LoggerFactory;
5
+ import org.springframework.cache.annotation.CacheEvict;
6
+ import org.springframework.stereotype.Service;
7
+
8
+ import com.dalab.reporting.client.CatalogServiceClient;
9
+ import com.dalab.reporting.client.PolicyEngineServiceClient;
10
+ import com.dalab.reporting.dto.ActionItemsDTO;
11
+ import com.dalab.reporting.dto.CostSavingsDTO;
12
+ import com.dalab.reporting.dto.GovernanceScoreDTO;
13
+
14
+ import lombok.RequiredArgsConstructor;
15
+
16
+ import java.math.BigDecimal;
17
+ import java.time.LocalDateTime;
18
+ import java.util.Arrays;
19
+ import java.util.List;
20
+
21
+ /**
22
+ * Implementation of dashboard aggregation service
23
+ * Provides cross-service data aggregation for role-based dashboards
24
+ *
25
+ * @author DALab Development Team
26
+ * @since 2025-01-02
27
+ */
28
+ @Service
29
+ @RequiredArgsConstructor
30
+ public class DashboardService implements IDashboardService {
31
+
32
+ private static final Logger log = LoggerFactory.getLogger(DashboardService.class);
33
+
34
+ private final CatalogServiceClient catalogServiceClient;
35
+ private final PolicyEngineServiceClient policyEngineServiceClient;
36
+
37
+ @Override
38
+ public GovernanceScoreDTO getGovernanceScore(Boolean includeHistory, Boolean includeBenchmark) {
39
+ log.info("Calculating governance score with history: {}, benchmark: {}", includeHistory, includeBenchmark);
40
+
41
+ try {
42
+ // TODO: Replace with real cross-service aggregation
43
+ // For Week 1, return comprehensive mock data that matches the expected structure
44
+
45
+ // Mock governance breakdown data
46
+ GovernanceScoreDTO.GovernanceBreakdown breakdown = GovernanceScoreDTO.GovernanceBreakdown.builder()
47
+ .policyComplianceScore(85.5)
48
+ .activePolicies(12)
49
+ .violationsCount(3)
50
+ .complianceRate(92.3)
51
+ .dataClassificationScore(78.2)
52
+ .classifiedAssets(156)
53
+ .totalAssets(200)
54
+ .classificationRate(78.0)
55
+ .metadataCompletenessScore(82.1)
56
+ .assetsWithMetadata(164)
57
+ .assetsWithBusinessContext(145)
58
+ .metadataCompletionRate(82.0)
59
+ .lineageCoverageScore(71.4)
60
+ .assetsWithLineage(143)
61
+ .totalLineageConnections(287)
62
+ .lineageCoverageRate(71.5)
63
+ .build();
64
+
65
+ // Calculate weighted overall score: Policy 30%, Classification 25%, Metadata 25%, Lineage 20%
66
+ double overallScore = (breakdown.getPolicyComplianceScore() * 0.30) +
67
+ (breakdown.getDataClassificationScore() * 0.25) +
68
+ (breakdown.getMetadataCompletenessScore() * 0.25) +
69
+ (breakdown.getLineageCoverageScore() * 0.20);
70
+
71
+ // Mock historical trend data (if requested)
72
+ List<GovernanceScoreDTO.GovernanceTrendPoint> historicalTrend = null;
73
+ if (includeHistory) {
74
+ historicalTrend = Arrays.asList(
75
+ GovernanceScoreDTO.GovernanceTrendPoint.builder()
76
+ .date(LocalDateTime.now().minusDays(30))
77
+ .score(75.2)
78
+ .period("daily")
79
+ .build(),
80
+ GovernanceScoreDTO.GovernanceTrendPoint.builder()
81
+ .date(LocalDateTime.now().minusDays(15))
82
+ .score(78.8)
83
+ .period("daily")
84
+ .build(),
85
+ GovernanceScoreDTO.GovernanceTrendPoint.builder()
86
+ .date(LocalDateTime.now())
87
+ .score(overallScore)
88
+ .period("daily")
89
+ .build()
90
+ );
91
+ }
92
+
93
+ // Mock top governance issues
94
+ List<GovernanceScoreDTO.GovernanceIssue> topIssues = Arrays.asList(
95
+ GovernanceScoreDTO.GovernanceIssue.builder()
96
+ .category("MISSING_CLASSIFICATION")
97
+ .description("44 assets lack proper data classification labels")
98
+ .severity("HIGH")
99
+ .affectedAssets(44)
100
+ .actionRequired("Assign appropriate classification labels")
101
+ .assignedTeam("DATA_ENGINEERING")
102
+ .build(),
103
+ GovernanceScoreDTO.GovernanceIssue.builder()
104
+ .category("POLICY_VIOLATION")
105
+ .description("3 active policy violations require immediate attention")
106
+ .severity("CRITICAL")
107
+ .affectedAssets(3)
108
+ .actionRequired("Review and remediate policy violations")
109
+ .assignedTeam("COMPLIANCE")
110
+ .build()
111
+ );
112
+
113
+ // Mock benchmark comparison (if requested)
114
+ GovernanceScoreDTO.BenchmarkComparison benchmark = null;
115
+ if (includeBenchmark) {
116
+ benchmark = GovernanceScoreDTO.BenchmarkComparison.builder()
117
+ .industryAverage(72.5)
118
+ .peerAverage(76.8)
119
+ .ranking("GOOD")
120
+ .percentile(78)
121
+ .comparisonNote("Above industry average, approaching peer group excellence")
122
+ .build();
123
+ }
124
+
125
+ // Mock recommendations
126
+ List<String> recommendations = Arrays.asList(
127
+ "Focus on improving data lineage coverage to reach 80% target",
128
+ "Implement automated classification for new assets to maintain classification rate",
129
+ "Address critical policy violations within next 48 hours",
130
+ "Enhance metadata completeness for business context information"
131
+ );
132
+
133
+ return GovernanceScoreDTO.builder()
134
+ .overallScore(overallScore)
135
+ .breakdown(breakdown)
136
+ .historicalTrend(historicalTrend)
137
+ .topIssues(topIssues)
138
+ .benchmark(benchmark)
139
+ .recommendations(recommendations)
140
+ .lastCalculated(LocalDateTime.now())
141
+ .dataFreshnessMinutes(2)
142
+ .build();
143
+
144
+ } catch (Exception e) {
145
+ log.error("Error calculating governance score", e);
146
+ throw new RuntimeException("Failed to calculate governance score: " + e.getMessage(), e);
147
+ }
148
+ }
149
+
150
+ @Override
151
+ public CostSavingsDTO getCostSavings(Boolean includeProjections, String timeframe) {
152
+ log.info("Calculating cost savings for timeframe: {}, projections: {}", timeframe, includeProjections);
153
+
154
+ try {
155
+ // TODO: Replace with real cost calculation from archival/deletion services and cloud APIs
156
+ // For Week 1, return comprehensive mock data
157
+
158
+ CostSavingsDTO.CostSavingsBreakdown breakdown = CostSavingsDTO.CostSavingsBreakdown.builder()
159
+ .archivalSavings(new BigDecimal("45750.50"))
160
+ .archivedAssetsCount(234)
161
+ .archivalStorageUsed("12.8 TB")
162
+ .deletionSavings(new BigDecimal("23140.25"))
163
+ .deletedAssetsCount(89)
164
+ .storageReclaimed("8.4 TB")
165
+ .tierOptimizationSavings(new BigDecimal("8920.75"))
166
+ .optimizedAssetsCount(156)
167
+ .computeSavings(new BigDecimal("12580.30"))
168
+ .decommissionedResources(23)
169
+ .platformCosts(new BigDecimal("5200.00"))
170
+ .netSavings(new BigDecimal("85191.80"))
171
+ .build();
172
+
173
+ BigDecimal totalSavings = new BigDecimal("90391.80");
174
+ BigDecimal monthlySavingsRate = new BigDecimal("7532.65");
175
+
176
+ List<CostSavingsDTO.ProviderSavings> providerSavings = Arrays.asList(
177
+ CostSavingsDTO.ProviderSavings.builder()
178
+ .providerName("AWS")
179
+ .savings(new BigDecimal("38250.25"))
180
+ .monthlyRate(new BigDecimal("3187.52"))
181
+ .primarySavingsSource("ARCHIVAL")
182
+ .contributionPercentage(42.3)
183
+ .build(),
184
+ CostSavingsDTO.ProviderSavings.builder()
185
+ .providerName("GCP")
186
+ .savings(new BigDecimal("29140.35"))
187
+ .monthlyRate(new BigDecimal("2428.36"))
188
+ .primarySavingsSource("DELETION")
189
+ .contributionPercentage(32.2)
190
+ .build(),
191
+ CostSavingsDTO.ProviderSavings.builder()
192
+ .providerName("Azure")
193
+ .savings(new BigDecimal("23001.20"))
194
+ .monthlyRate(new BigDecimal("1916.77"))
195
+ .primarySavingsSource("OPTIMIZATION")
196
+ .contributionPercentage(25.5)
197
+ .build()
198
+ );
199
+
200
+ // Mock savings opportunities (if projections requested)
201
+ List<CostSavingsDTO.SavingsOpportunity> savingsOpportunities = null;
202
+ if (includeProjections) {
203
+ savingsOpportunities = Arrays.asList(
204
+ CostSavingsDTO.SavingsOpportunity.builder()
205
+ .opportunityType("ARCHIVAL_CANDIDATE")
206
+ .description("67 datasets eligible for archival to cold storage")
207
+ .potentialSavings(new BigDecimal("18750.00"))
208
+ .monthlyImpact(new BigDecimal("1562.50"))
209
+ .timeframe("30_DAYS")
210
+ .effortLevel("LOW")
211
+ .affectedAssets(67)
212
+ .providerName("AWS")
213
+ .actionRequired("Review and approve archival candidates")
214
+ .build()
215
+ );
216
+ }
217
+
218
+ // Mock historical trend
219
+ List<CostSavingsDTO.SavingsTrendPoint> historicalTrend = Arrays.asList(
220
+ CostSavingsDTO.SavingsTrendPoint.builder()
221
+ .month(LocalDateTime.now().minusMonths(2))
222
+ .cumulativeSavings(new BigDecimal("45230.50"))
223
+ .monthlySavings(new BigDecimal("6200.25"))
224
+ .platformCosts(new BigDecimal("4800.00"))
225
+ .netSavings(new BigDecimal("40430.50"))
226
+ .assetsProcessed(156)
227
+ .build(),
228
+ CostSavingsDTO.SavingsTrendPoint.builder()
229
+ .month(LocalDateTime.now().minusMonths(1))
230
+ .cumulativeSavings(new BigDecimal("67820.75"))
231
+ .monthlySavings(new BigDecimal("7120.40"))
232
+ .platformCosts(new BigDecimal("5000.00"))
233
+ .netSavings(new BigDecimal("62820.75"))
234
+ .assetsProcessed(201)
235
+ .build(),
236
+ CostSavingsDTO.SavingsTrendPoint.builder()
237
+ .month(LocalDateTime.now())
238
+ .cumulativeSavings(totalSavings)
239
+ .monthlySavings(monthlySavingsRate)
240
+ .platformCosts(new BigDecimal("5200.00"))
241
+ .netSavings(breakdown.getNetSavings())
242
+ .assetsProcessed(323)
243
+ .build()
244
+ );
245
+
246
+ List<String> recommendations = Arrays.asList(
247
+ "Prioritize archival of large datasets with low access frequency",
248
+ "Review deletion candidates for cost optimization potential",
249
+ "Consider automated tier optimization for frequently accessed data",
250
+ "Monitor platform costs vs savings ratio for ROI optimization"
251
+ );
252
+
253
+ double roiPercentage = (breakdown.getNetSavings().doubleValue() / breakdown.getPlatformCosts().doubleValue()) * 100;
254
+
255
+ return CostSavingsDTO.builder()
256
+ .totalSavings(totalSavings)
257
+ .monthlySavingsRate(monthlySavingsRate)
258
+ .roiPercentage(roiPercentage)
259
+ .breakdown(breakdown)
260
+ .providerSavings(providerSavings)
261
+ .savingsOpportunities(savingsOpportunities)
262
+ .historicalTrend(historicalTrend)
263
+ .recommendations(recommendations)
264
+ .lastCalculated(LocalDateTime.now())
265
+ .dataAccuracy("ESTIMATED")
266
+ .build();
267
+
268
+ } catch (Exception e) {
269
+ log.error("Error calculating cost savings", e);
270
+ throw new RuntimeException("Failed to calculate cost savings: " + e.getMessage(), e);
271
+ }
272
+ }
273
+
274
+ @Override
275
+ public ActionItemsDTO getActionItems(Boolean assignedToUser, String severity, String status, Integer limit) {
276
+ log.info("Aggregating action items with filters - assignedToUser: {}, severity: {}, status: {}, limit: {}",
277
+ assignedToUser, severity, status, limit);
278
+
279
+ try {
280
+ // TODO: Replace with real aggregation from all DALab services
281
+ // For Week 1, return comprehensive mock data
282
+
283
+ ActionItemsDTO.ActionItemsSummary summary = ActionItemsDTO.ActionItemsSummary.builder()
284
+ .criticalCount(4)
285
+ .highCount(12)
286
+ .mediumCount(23)
287
+ .lowCount(8)
288
+ .overdueCount(3)
289
+ .dueTodayCount(7)
290
+ .dueThisWeekCount(15)
291
+ .assignedCount(35)
292
+ .unassignedCount(12)
293
+ .inProgressCount(18)
294
+ .completedCount(156)
295
+ .build();
296
+
297
+ // Mock action items
298
+ List<ActionItemsDTO.ActionItem> actionItems = Arrays.asList(
299
+ ActionItemsDTO.ActionItem.builder()
300
+ .id("AI-001")
301
+ .title("Critical policy violation in customer database")
302
+ .description("PII policy violation detected in customer_data_prod table")
303
+ .category("POLICY_BREACH")
304
+ .severity("CRITICAL")
305
+ .status("OPEN")
306
+ .sourceService("da-policyengine")
307
+ .sourceEntityType("ASSET")
308
+ .sourceEntityId("asset-12345")
309
+ .sourceEntityName("customer_data_prod")
310
+ .assignedTo("john.doe@company.com")
311
+ .assignedTeam("COMPLIANCE")
312
+ .createdAt(LocalDateTime.now().minusHours(2))
313
+ .dueDate(LocalDateTime.now().plusHours(22))
314
+ .lastUpdated(LocalDateTime.now().minusMinutes(30))
315
+ .actionRequired("Review and apply data masking policy")
316
+ .businessJustification("Ensure GDPR compliance for customer data")
317
+ .estimatedEffort("HOURS")
318
+ .businessImpactScore(9)
319
+ .tags(Arrays.asList("GDPR", "PII", "URGENT"))
320
+ .detailsUrl("/policy-engine/violations/POL-001")
321
+ .build(),
322
+ ActionItemsDTO.ActionItem.builder()
323
+ .id("AI-002")
324
+ .title("Missing metadata for analytics datasets")
325
+ .description("44 datasets lack business metadata and data steward assignment")
326
+ .category("METADATA_MISSING")
327
+ .severity("HIGH")
328
+ .status("IN_PROGRESS")
329
+ .sourceService("da-catalog")
330
+ .sourceEntityType("ASSET")
331
+ .sourceEntityId("batch-meta-001")
332
+ .sourceEntityName("Analytics Dataset Batch")
333
+ .assignedTo("jane.smith@company.com")
334
+ .assignedTeam("DATA_ENGINEERING")
335
+ .createdAt(LocalDateTime.now().minusDays(3))
336
+ .dueDate(LocalDateTime.now().plusDays(4))
337
+ .lastUpdated(LocalDateTime.now().minusHours(6))
338
+ .actionRequired("Complete metadata forms for all analytics datasets")
339
+ .businessJustification("Improve data discoverability and governance")
340
+ .estimatedEffort("DAYS")
341
+ .businessImpactScore(6)
342
+ .tags(Arrays.asList("METADATA", "GOVERNANCE"))
343
+ .detailsUrl("/catalog/metadata-gaps/MDG-001")
344
+ .build()
345
+ );
346
+
347
+ // Mock SLA metrics
348
+ ActionItemsDTO.SlaMetrics slaMetrics = ActionItemsDTO.SlaMetrics.builder()
349
+ .averageResolutionTimeHours(24.5)
350
+ .slaComplianceRate(87.3)
351
+ .breachedSlaCount(6)
352
+ .nearBreachCount(3)
353
+ .complianceViolations(ActionItemsDTO.SlaByCategory.builder()
354
+ .totalItems(8)
355
+ .onTimeResolved(6)
356
+ .breachedSla(2)
357
+ .averageResolutionHours(18.5)
358
+ .complianceRate(75.0)
359
+ .build())
360
+ .build();
361
+
362
+ // Mock business impact summary
363
+ ActionItemsDTO.BusinessImpactSummary businessImpact = ActionItemsDTO.BusinessImpactSummary.builder()
364
+ .highImpactItems(4)
365
+ .mediumImpactItems(23)
366
+ .lowImpactItems(20)
367
+ .averageImpactScore(6.2)
368
+ .highestImpactCategory("DATA_PRIVACY")
369
+ .impactAreas(Arrays.asList(
370
+ ActionItemsDTO.ImpactArea.builder()
371
+ .areaName("DATA_PRIVACY")
372
+ .affectedItems(8)
373
+ .averageImpactScore(8.5)
374
+ .riskLevel("HIGH")
375
+ .description("Privacy compliance and PII protection issues")
376
+ .build(),
377
+ ActionItemsDTO.ImpactArea.builder()
378
+ .areaName("OPERATIONAL_EFFICIENCY")
379
+ .affectedItems(25)
380
+ .averageImpactScore(5.2)
381
+ .riskLevel("MEDIUM")
382
+ .description("Metadata and discovery efficiency improvements")
383
+ .build()
384
+ ))
385
+ .build();
386
+
387
+ return ActionItemsDTO.builder()
388
+ .totalActionItems(47)
389
+ .summary(summary)
390
+ .actionItems(actionItems)
391
+ .slaMetrics(slaMetrics)
392
+ .businessImpact(businessImpact)
393
+ .lastAggregated(LocalDateTime.now())
394
+ .includedServices(Arrays.asList("da-autocompliance", "da-policyengine", "da-catalog", "da-discovery"))
395
+ .build();
396
+
397
+ } catch (Exception e) {
398
+ log.error("Error aggregating action items", e);
399
+ throw new RuntimeException("Failed to aggregate action items: " + e.getMessage(), e);
400
+ }
401
+ }
402
+
403
+ @Override
404
+ @CacheEvict(value = {"governanceScore", "costSavings", "actionItems"}, allEntries = true)
405
+ public void refreshAllCaches() {
406
+ log.info("Refreshing all dashboard caches");
407
+ // Cache eviction handled by annotation
408
+ log.info("Dashboard caches refreshed successfully");
409
+ }
410
+ }
src/main/java/com/dalab/reporting/service/IDashboardService.java ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service;
2
+
3
+ import com.dalab.reporting.dto.ActionItemsDTO;
4
+ import com.dalab.reporting.dto.CostSavingsDTO;
5
+ import com.dalab.reporting.dto.GovernanceScoreDTO;
6
+
7
+ /**
8
+ * Service interface for DALab Dashboard Aggregation operations
9
+ * Provides cross-service data aggregation for role-based dashboards
10
+ *
11
+ * @author DALab Development Team
12
+ * @since 2025-01-02
13
+ */
14
+ public interface IDashboardService {
15
+
16
+ /**
17
+ * Calculate comprehensive governance score with weighted metrics
18
+ * Aggregates data from da-catalog, da-autocompliance, and da-policyengine
19
+ *
20
+ * @param includeHistory whether to include 30-day historical trend data
21
+ * @param includeBenchmark whether to include industry benchmark comparison
22
+ * @return governance score with breakdown, trends, and recommendations
23
+ */
24
+ GovernanceScoreDTO getGovernanceScore(Boolean includeHistory, Boolean includeBenchmark);
25
+
26
+ /**
27
+ * Calculate comprehensive cost savings analysis
28
+ * Aggregates data from da-autoarchival, da-autodelete, and cloud provider APIs
29
+ *
30
+ * @param includeProjections whether to include future savings projections
31
+ * @param timeframe analysis timeframe: "30_DAYS", "90_DAYS", "12_MONTHS", "ALL_TIME"
32
+ * @return cost savings analysis with ROI calculation and opportunities
33
+ */
34
+ CostSavingsDTO getCostSavings(Boolean includeProjections, String timeframe);
35
+
36
+ /**
37
+ * Aggregate action items from all DALab services
38
+ * Collects data from da-autocompliance, da-policyengine, da-catalog, da-discovery
39
+ *
40
+ * @param assignedToUser filter to show only items assigned to current user
41
+ * @param severity filter by severity level
42
+ * @param status filter by status
43
+ * @param limit maximum number of action items to return
44
+ * @return aggregated action items with SLA and business impact metrics
45
+ */
46
+ ActionItemsDTO getActionItems(Boolean assignedToUser, String severity, String status, Integer limit);
47
+
48
+ /**
49
+ * Refresh all dashboard caches for performance optimization
50
+ * Triggers cache warming for common dashboard queries
51
+ */
52
+ void refreshAllCaches();
53
+ }
src/main/java/com/dalab/reporting/service/INotificationService.java ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service;
2
+
3
+ import java.util.List;
4
+ import java.util.UUID;
5
+
6
+ import org.springframework.data.domain.Page;
7
+ import org.springframework.data.domain.Pageable;
8
+
9
+ import com.dalab.reporting.dto.NotificationPreferenceDTO;
10
+ import com.dalab.reporting.dto.NotificationRequestDTO;
11
+
12
+ public interface INotificationService {
13
+
14
+ // Notification Preference Management
15
+ NotificationPreferenceDTO saveNotificationPreference(NotificationPreferenceDTO preferenceDTO);
16
+ List<NotificationPreferenceDTO> getNotificationPreferencesByUserId(UUID userId);
17
+ NotificationPreferenceDTO getNotificationPreferenceById(UUID preferenceId);
18
+ Page<NotificationPreferenceDTO> getAllNotificationPreferences(Pageable pageable); // Admin endpoint
19
+ void deleteNotificationPreference(UUID preferenceId);
20
+
21
+ // Send Notifications
22
+ void sendNotification(NotificationRequestDTO notificationRequest);
23
+
24
+ // Test Notification (e.g., for a user to test their setup)
25
+ // This might take a simplified DTO or just user ID and channel
26
+ void sendTestNotification(UUID userId, String channelName); // channelName like "EMAIL" or "SLACK"
27
+
28
+ }
src/main/java/com/dalab/reporting/service/IReportService.java ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service;
2
+
3
+ import java.util.UUID;
4
+
5
+ import org.springframework.data.domain.Page;
6
+ import org.springframework.data.domain.Pageable;
7
+
8
+ import com.dalab.reporting.dto.GeneratedReportOutputDTO;
9
+ import com.dalab.reporting.dto.ReportDefinitionDTO;
10
+ import com.dalab.reporting.dto.ReportGenerationRequestDTO;
11
+
12
+ public interface IReportService {
13
+
14
+ // Report Definition Management
15
+ ReportDefinitionDTO createReportDefinition(ReportDefinitionDTO reportDefinitionDTO);
16
+ ReportDefinitionDTO getReportDefinitionById(UUID id);
17
+ ReportDefinitionDTO getReportDefinitionByKey(String reportKey);
18
+ Page<ReportDefinitionDTO> getAllReportDefinitions(Pageable pageable);
19
+ ReportDefinitionDTO updateReportDefinition(UUID id, ReportDefinitionDTO reportDefinitionDTO);
20
+ void deleteReportDefinition(UUID id);
21
+
22
+ // Report Generation & Retrieval
23
+ GeneratedReportOutputDTO generateReport(String reportKey, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId);
24
+ GeneratedReportOutputDTO generateReport(UUID reportDefinitionId, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId);
25
+ GeneratedReportOutputDTO regenerateReport(UUID reportId, UUID triggeredByUserId);
26
+ GeneratedReportOutputDTO getGeneratedReportById(UUID generatedReportId);
27
+ Page<GeneratedReportOutputDTO> getGeneratedReportsByReportKey(String reportKey, Pageable pageable);
28
+ Page<GeneratedReportOutputDTO> getAllGeneratedReports(Pageable pageable);
29
+ // TODO: Add methods for querying generated reports by status, user, date range, etc.
30
+
31
+ // Internal method for scheduled or event-triggered generation (implementation detail)
32
+ // void processReportGenerationJob(UUID generatedReportId);
33
+ }
src/main/java/com/dalab/reporting/service/impl/NotificationService.java ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service.impl;
2
+
3
+ import java.util.Collections;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+ import java.util.UUID;
7
+ import java.util.function.Function;
8
+ import java.util.stream.Collectors;
9
+
10
+ import org.slf4j.Logger;
11
+ import org.slf4j.LoggerFactory;
12
+ import org.springframework.data.domain.Page;
13
+ import org.springframework.data.domain.Pageable;
14
+ import org.springframework.stereotype.Service;
15
+ import org.springframework.transaction.annotation.Transactional;
16
+ import org.springframework.util.CollectionUtils;
17
+
18
+ import com.dalab.reporting.common.exception.BadRequestException;
19
+ import com.dalab.reporting.common.exception.ResourceNotFoundException;
20
+ import com.dalab.reporting.dto.NotificationPreferenceDTO;
21
+ import com.dalab.reporting.dto.NotificationRequestDTO;
22
+ import com.dalab.reporting.mapper.NotificationPreferenceMapper;
23
+ import com.dalab.reporting.model.NotificationChannel;
24
+ import com.dalab.reporting.model.UserNotificationPreference;
25
+ import com.dalab.reporting.repository.UserNotificationPreferenceRepository;
26
+ import com.dalab.reporting.service.INotificationService;
27
+ import com.dalab.reporting.service.notification.INotificationProvider;
28
+
29
+ @Service
30
+ public class NotificationService implements INotificationService {
31
+
32
+ private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
33
+
34
+ private final UserNotificationPreferenceRepository preferenceRepository;
35
+ private final NotificationPreferenceMapper preferenceMapper;
36
+ private final Map<NotificationChannel, INotificationProvider> notificationProviders;
37
+
38
+ public NotificationService(
39
+ UserNotificationPreferenceRepository preferenceRepository,
40
+ NotificationPreferenceMapper preferenceMapper,
41
+ List<INotificationProvider> providers
42
+ ) {
43
+ this.preferenceRepository = preferenceRepository;
44
+ this.preferenceMapper = preferenceMapper;
45
+ this.notificationProviders = providers.stream()
46
+ .collect(Collectors.toUnmodifiableMap(INotificationProvider::getChannelType, Function.identity()));
47
+ log.info("Initialized NotificationService with providers for channels: {}", this.notificationProviders.keySet());
48
+ }
49
+
50
+ @Override
51
+ @Transactional
52
+ public NotificationPreferenceDTO saveNotificationPreference(NotificationPreferenceDTO preferenceDTO) {
53
+ if (preferenceDTO.getUserId() == null || preferenceDTO.getNotificationTypeKey() == null || preferenceDTO.getChannel() == null) {
54
+ throw new BadRequestException("User ID, Notification Type Key, and Channel are required for preferences.");
55
+ }
56
+
57
+ UserNotificationPreference preference = preferenceRepository
58
+ .findByUserIdAndNotificationTypeKeyAndChannel(
59
+ preferenceDTO.getUserId(),
60
+ preferenceDTO.getNotificationTypeKey(),
61
+ preferenceDTO.getChannel())
62
+ .orElseGet(() -> preferenceMapper.toEntity(preferenceDTO));
63
+
64
+ // Update fields if it exists
65
+ if (preference.getId() != null) {
66
+ preference.setEnabled(preferenceDTO.isEnabled());
67
+ preference.setChannelConfiguration(preferenceDTO.getChannelConfiguration());
68
+ }
69
+ // For new preferences, mapper already set the values from DTO.
70
+
71
+ return preferenceMapper.toDTO(preferenceRepository.save(preference));
72
+ }
73
+
74
+ @Override
75
+ @Transactional(readOnly = true)
76
+ public List<NotificationPreferenceDTO> getNotificationPreferencesByUserId(UUID userId) {
77
+ return preferenceMapper.toDTOList(preferenceRepository.findByUserId(userId));
78
+ }
79
+
80
+ @Override
81
+ @Transactional(readOnly = true)
82
+ public NotificationPreferenceDTO getNotificationPreferenceById(UUID preferenceId) {
83
+ return preferenceRepository.findById(preferenceId)
84
+ .map(preferenceMapper::toDTO)
85
+ .orElseThrow(() -> new ResourceNotFoundException("NotificationPreference", "id", preferenceId));
86
+ }
87
+
88
+ @Override
89
+ @Transactional(readOnly = true)
90
+ public Page<NotificationPreferenceDTO> getAllNotificationPreferences(Pageable pageable) {
91
+ return preferenceRepository.findAll(pageable).map(preferenceMapper::toDTO);
92
+ }
93
+
94
+ @Override
95
+ @Transactional
96
+ public void deleteNotificationPreference(UUID preferenceId) {
97
+ if (!preferenceRepository.existsById(preferenceId)) {
98
+ throw new ResourceNotFoundException("NotificationPreference", "id", preferenceId);
99
+ }
100
+ preferenceRepository.deleteById(preferenceId);
101
+ }
102
+
103
+ @Override
104
+ public void sendNotification(NotificationRequestDTO request) {
105
+ if (CollectionUtils.isEmpty(request.getChannels())) {
106
+ log.warn("No channels specified in notification request. Subject: {}", request.getSubject());
107
+ return;
108
+ }
109
+
110
+ List<UserNotificationPreference> preferences = Collections.emptyList();
111
+
112
+ if (!CollectionUtils.isEmpty(request.getTargetUserIds())) {
113
+ // Fetch preferences for specific users and filter by requested channels
114
+ preferences = preferenceRepository.findAllById(request.getTargetUserIds()).stream()
115
+ .filter(UserNotificationPreference::isEnabled)
116
+ .filter(p -> request.getChannels().contains(p.getChannel()))
117
+ .collect(Collectors.toList());
118
+ // We might also need to handle cases where a user ID is given but they have no matching preference for the channel.
119
+ // This current logic assumes we only notify based on *existing* preferences.
120
+ // If direct notification to user without preference is needed, that's a different path.
121
+
122
+ } else if (request.getNotificationTypeKey() != null) {
123
+ // Fetch all enabled preferences for the given notification type key and requested channels
124
+ preferences = request.getChannels().stream()
125
+ .flatMap(channel ->
126
+ preferenceRepository.findByNotificationTypeKeyAndChannelAndEnabled(
127
+ request.getNotificationTypeKey(), channel, true).stream())
128
+ .distinct() // Avoid duplicate if user subscribed via multiple ways to same effective outcome (though our model prevents this row-wise)
129
+ .collect(Collectors.toList());
130
+ } else {
131
+ log.warn("Notification request must specify targetUserIds or a notificationTypeKey. Subject: {}", request.getSubject());
132
+ return;
133
+ }
134
+
135
+ if (CollectionUtils.isEmpty(preferences)) {
136
+ log.info("No matching user preferences found for notification. Subject: {}", request.getSubject());
137
+ return;
138
+ }
139
+
140
+ for (UserNotificationPreference preference : preferences) {
141
+ INotificationProvider provider = notificationProviders.get(preference.getChannel());
142
+ if (provider != null) {
143
+ try {
144
+ // Apply overrides if present
145
+ NotificationRequestDTO finalRequest = request;
146
+ if (request.getChannelOverrides() != null && request.getChannelOverrides().containsKey(preference.getChannel().name())) {
147
+ // This is a simplification. A real override might need deeper merging.
148
+ // For now, let's assume direct send is used if overrides are complex.
149
+ log.warn("Channel specific overrides are present but not fully implemented for preference-based send. Consider direct send. Channel: {}", preference.getChannel());
150
+ }
151
+ provider.send(preference, finalRequest);
152
+ log.info("Sent notification '{}' to user {} via {}", request.getSubject(), preference.getUserId(), preference.getChannel());
153
+ } catch (Exception e) {
154
+ log.error("Failed to send notification to user {} via {}. Subject: {}. Error: {}",
155
+ preference.getUserId(), preference.getChannel(), request.getSubject(), e.getMessage(), e);
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ @Override
162
+ public void sendTestNotification(UUID userId, String channelName) {
163
+ NotificationChannel channel;
164
+ try {
165
+ channel = NotificationChannel.valueOf(channelName.toUpperCase());
166
+ } catch (IllegalArgumentException e) {
167
+ throw new BadRequestException("Invalid notification channel: " + channelName);
168
+ }
169
+
170
+ INotificationProvider provider = notificationProviders.get(channel);
171
+ if (provider == null) {
172
+ throw new BadRequestException("No provider configured for channel: " + channelName);
173
+ }
174
+
175
+ // For a test, we might fetch a user's primary contact for that channel or use a default test address.
176
+ // Here, we'll try to find a preference or send to a default if not found.
177
+ UserNotificationPreference testPreference = preferenceRepository
178
+ .findByUserIdAndNotificationTypeKeyAndChannel(userId, "TEST_NOTIFICATION", channel)
179
+ .orElseGet(() -> {
180
+ UserNotificationPreference tempPref = new UserNotificationPreference();
181
+ tempPref.setUserId(userId);
182
+ tempPref.setChannel(channel);
183
+ tempPref.setEnabled(true);
184
+ // tempPref.setChannelConfiguration(...); // Potentially set a test email/slackId if not found in profile
185
+ return tempPref;
186
+ });
187
+
188
+ if (!testPreference.isEnabled()) {
189
+ throw new BadRequestException("Test notifications are disabled for this user and channel through preferences.");
190
+ }
191
+
192
+ NotificationRequestDTO testRequest = NotificationRequestDTO.builder()
193
+ .subject("Test Notification")
194
+ .body("This is a test notification from DALab Reporting Service.")
195
+ .channels(Collections.singletonList(channel))
196
+ .targetUserIds(Collections.singletonList(userId)) // For context, though provider.send uses preference
197
+ .build();
198
+
199
+ try {
200
+ provider.send(testPreference, testRequest);
201
+ log.info("Sent test notification to user {} via {}", userId, channel);
202
+ } catch (Exception e) {
203
+ log.error("Failed to send test notification to user {} via {}: {}", userId, channel, e.getMessage(), e);
204
+ throw new RuntimeException("Failed to send test notification: " + e.getMessage(), e);
205
+ }
206
+ }
207
+ }
src/main/java/com/dalab/reporting/service/impl/ReportService.java ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service.impl;
2
+
3
+ import java.time.Instant;
4
+ import java.util.HashMap;
5
+ import java.util.Map;
6
+ import java.util.UUID;
7
+
8
+ import org.slf4j.Logger;
9
+ import org.slf4j.LoggerFactory;
10
+ import org.springframework.data.domain.Page;
11
+ import org.springframework.data.domain.Pageable;
12
+ import org.springframework.scheduling.annotation.Async;
13
+ import org.springframework.stereotype.Service;
14
+ import org.springframework.transaction.annotation.Transactional;
15
+
16
+ import com.dalab.reporting.common.exception.BadRequestException;
17
+ import com.dalab.reporting.common.exception.ResourceNotFoundException;
18
+ import com.dalab.reporting.dto.GeneratedReportOutputDTO;
19
+ import com.dalab.reporting.dto.ReportDefinitionDTO;
20
+ import com.dalab.reporting.dto.ReportGenerationRequestDTO;
21
+ import com.dalab.reporting.mapper.ReportMapper;
22
+ import com.dalab.reporting.model.GeneratedReport;
23
+ import com.dalab.reporting.model.ReportDefinition;
24
+ import com.dalab.reporting.model.ReportStatus;
25
+ import com.dalab.reporting.repository.GeneratedReportRepository;
26
+ import com.dalab.reporting.repository.ReportDefinitionRepository;
27
+ import com.dalab.reporting.service.IReportService;
28
+
29
+ @Service
30
+ public class ReportService implements IReportService {
31
+
32
+ private static final Logger log = LoggerFactory.getLogger(ReportService.class);
33
+
34
+ private final ReportDefinitionRepository reportDefinitionRepository;
35
+ private final GeneratedReportRepository generatedReportRepository;
36
+ private final ReportMapper reportMapper;
37
+ // In a real app, this might be a ApplicationEventPublisher to trigger async job via Spring Events
38
+ // private final ApplicationEventPublisher eventPublisher;
39
+
40
+ public ReportService(ReportDefinitionRepository reportDefinitionRepository,
41
+ GeneratedReportRepository generatedReportRepository,
42
+ ReportMapper reportMapper) {
43
+ this.reportDefinitionRepository = reportDefinitionRepository;
44
+ this.generatedReportRepository = generatedReportRepository;
45
+ this.reportMapper = reportMapper;
46
+ }
47
+
48
+ @Override
49
+ @Transactional
50
+ public ReportDefinitionDTO createReportDefinition(ReportDefinitionDTO reportDefinitionDTO) {
51
+ if (reportDefinitionRepository.findByReportKey(reportDefinitionDTO.getReportKey()).isPresent()) {
52
+ throw new BadRequestException("Report definition with key '" + reportDefinitionDTO.getReportKey() + "' already exists.");
53
+ }
54
+ ReportDefinition entity = reportMapper.toEntity(reportDefinitionDTO);
55
+ entity.setEnabled(true); // Default to enabled
56
+ return reportMapper.toDTO(reportDefinitionRepository.save(entity));
57
+ }
58
+
59
+ @Override
60
+ @Transactional(readOnly = true)
61
+ public ReportDefinitionDTO getReportDefinitionById(UUID id) {
62
+ return reportDefinitionRepository.findById(id)
63
+ .map(reportMapper::toDTO)
64
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", id));
65
+ }
66
+
67
+ @Override
68
+ @Transactional(readOnly = true)
69
+ public ReportDefinitionDTO getReportDefinitionByKey(String reportKey) {
70
+ return reportDefinitionRepository.findByReportKey(reportKey)
71
+ .map(reportMapper::toDTO)
72
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
73
+ }
74
+
75
+ @Override
76
+ @Transactional(readOnly = true)
77
+ public Page<ReportDefinitionDTO> getAllReportDefinitions(Pageable pageable) {
78
+ return reportDefinitionRepository.findAll(pageable).map(reportMapper::toDTO);
79
+ }
80
+
81
+ @Override
82
+ @Transactional
83
+ public ReportDefinitionDTO updateReportDefinition(UUID id, ReportDefinitionDTO reportDefinitionDTO) {
84
+ ReportDefinition existingDefinition = reportDefinitionRepository.findById(id)
85
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", id));
86
+
87
+ // Check if reportKey is being changed and if it conflicts
88
+ if (!existingDefinition.getReportKey().equals(reportDefinitionDTO.getReportKey()) &&
89
+ reportDefinitionRepository.findByReportKey(reportDefinitionDTO.getReportKey()).isPresent()) {
90
+ throw new BadRequestException("Report definition with key '" + reportDefinitionDTO.getReportKey() + "' already exists.");
91
+ }
92
+
93
+ existingDefinition.setReportKey(reportDefinitionDTO.getReportKey());
94
+ existingDefinition.setDisplayName(reportDefinitionDTO.getDisplayName());
95
+ existingDefinition.setDescription(reportDefinitionDTO.getDescription());
96
+ existingDefinition.setDefaultParameters(reportDefinitionDTO.getDefaultParameters());
97
+ existingDefinition.setDefaultScheduleCron(reportDefinitionDTO.getDefaultScheduleCron());
98
+ existingDefinition.setEnabled(reportDefinitionDTO.isEnabled());
99
+
100
+ return reportMapper.toDTO(reportDefinitionRepository.save(existingDefinition));
101
+ }
102
+
103
+ @Override
104
+ @Transactional
105
+ public void deleteReportDefinition(UUID id) {
106
+ if (!reportDefinitionRepository.existsById(id)) {
107
+ throw new ResourceNotFoundException("ReportDefinition", "id", id);
108
+ }
109
+ // TODO: Add logic to handle existing GeneratedReport instances if a definition is deleted.
110
+ // E.g., mark them as orphaned, disallow new generations, or prevent deletion if reports exist.
111
+ reportDefinitionRepository.deleteById(id);
112
+ }
113
+
114
+ @Override
115
+ @Transactional
116
+ public GeneratedReportOutputDTO generateReport(String reportKey, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
117
+ ReportDefinition reportDefinition = reportDefinitionRepository.findByReportKey(reportKey)
118
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
119
+ return createAndQueueReportGeneration(reportDefinition, requestDTO, triggeredByUserId);
120
+ }
121
+
122
+ @Override
123
+ @Transactional
124
+ public GeneratedReportOutputDTO generateReport(UUID reportDefinitionId, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
125
+ ReportDefinition reportDefinition = reportDefinitionRepository.findById(reportDefinitionId)
126
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "id", reportDefinitionId));
127
+ return createAndQueueReportGeneration(reportDefinition, requestDTO, triggeredByUserId);
128
+ }
129
+
130
+ private GeneratedReportOutputDTO createAndQueueReportGeneration(ReportDefinition reportDefinition, ReportGenerationRequestDTO requestDTO, UUID triggeredByUserId) {
131
+ if (!reportDefinition.isEnabled()) {
132
+ throw new BadRequestException("Report definition '" + reportDefinition.getReportKey() + "' is disabled.");
133
+ }
134
+
135
+ GeneratedReport generatedReport = new GeneratedReport();
136
+ generatedReport.setReportDefinition(reportDefinition);
137
+ generatedReport.setStatus(ReportStatus.PENDING);
138
+ generatedReport.setRequestedByUserId(triggeredByUserId);
139
+ generatedReport.setRequestedAt(Instant.now());
140
+
141
+ Map<String, Object> effectiveParams = new HashMap<>();
142
+ if (reportDefinition.getDefaultParameters() != null) {
143
+ effectiveParams.putAll(reportDefinition.getDefaultParameters());
144
+ }
145
+ if (requestDTO != null && requestDTO.getParameters() != null) {
146
+ effectiveParams.putAll(requestDTO.getParameters());
147
+ }
148
+ generatedReport.setGenerationParameters(effectiveParams);
149
+
150
+ GeneratedReport savedReport = generatedReportRepository.save(generatedReport);
151
+ log.info("Queued report generation for reportKey: {}, generatedReportId: {}", reportDefinition.getReportKey(), savedReport.getId());
152
+
153
+ // Trigger asynchronous processing of the report
154
+ // This could be publishing an event, sending a message to a queue, or calling an @Async method directly.
155
+ processReportGenerationJobAsync(savedReport.getId(), reportDefinition.getReportKey(), effectiveParams);
156
+
157
+ return reportMapper.toDTO(savedReport);
158
+ }
159
+
160
+ @Async("taskExecutor") // Assuming a TaskExecutor bean is configured for async tasks
161
+ public void processReportGenerationJobAsync(UUID generatedReportId, String reportKey, Map<String, Object> parameters) {
162
+ log.info("Starting asynchronous processing for generatedReportId: {}, reportKey: {}", generatedReportId, reportKey);
163
+ GeneratedReport report = generatedReportRepository.findById(generatedReportId)
164
+ .orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", generatedReportId)); // Should not happen if just saved
165
+
166
+ report.setStatus(ReportStatus.GENERATING);
167
+ report.setStartedAt(Instant.now());
168
+ generatedReportRepository.save(report);
169
+
170
+ try {
171
+ // Simulate report generation logic based on reportKey
172
+ // In a real application, this would involve fetching data from Kafka, other services (via Feign), DB, etc.
173
+ // and then compiling it into the reportContent.
174
+ log.info("Simulating report generation for: {} with params: {}", reportKey, parameters);
175
+ Thread.sleep(5000); // Simulate work
176
+
177
+ String reportContent = "Report content for " + reportKey + " generated at " + Instant.now() + " with params: " + parameters.toString();
178
+ // For complex reports, content could be JSON, CSV, or a link to a file.
179
+ // Example for JSON:
180
+ // Map<String, Object> jsonData = new HashMap<>();
181
+ // jsonData.put("title", "Sample Report: " + reportKey);
182
+ // jsonData.put("dataPoints", List.of(1,2,3,4,5));
183
+ // reportContent = new ObjectMapper().writeValueAsString(jsonData);
184
+
185
+ report.setReportContent(reportContent);
186
+ report.setStatus(ReportStatus.COMPLETED);
187
+ } catch (Exception e) {
188
+ log.error("Error during report generation for ID {}: {}", generatedReportId, e.getMessage(), e);
189
+ report.setStatus(ReportStatus.FAILED);
190
+ report.setFailureReason(e.getMessage());
191
+ } finally {
192
+ report.setCompletedAt(Instant.now());
193
+ generatedReportRepository.save(report);
194
+ log.info("Finished processing for generatedReportId: {}. Status: {}", generatedReportId, report.getStatus());
195
+ // TODO: Trigger notifications if configured (e.g., on completion or failure)
196
+ }
197
+ }
198
+
199
+ @Override
200
+ @Transactional(readOnly = true)
201
+ public GeneratedReportOutputDTO getGeneratedReportById(UUID generatedReportId) {
202
+ return generatedReportRepository.findById(generatedReportId)
203
+ .map(reportMapper::toDTO)
204
+ .orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", generatedReportId));
205
+ }
206
+
207
+ @Override
208
+ @Transactional(readOnly = true)
209
+ public Page<GeneratedReportOutputDTO> getGeneratedReportsByReportKey(String reportKey, Pageable pageable) {
210
+ ReportDefinition reportDefinition = reportDefinitionRepository.findByReportKey(reportKey)
211
+ .orElseThrow(() -> new ResourceNotFoundException("ReportDefinition", "reportKey", reportKey));
212
+ return generatedReportRepository.findByReportDefinition(reportDefinition, pageable).map(reportMapper::toDTO);
213
+ }
214
+
215
+ @Override
216
+ @Transactional(readOnly = true)
217
+ public Page<GeneratedReportOutputDTO> getAllGeneratedReports(Pageable pageable) {
218
+ return generatedReportRepository.findAll(pageable).map(reportMapper::toDTO);
219
+ }
220
+
221
+ @Override
222
+ @Transactional
223
+ public GeneratedReportOutputDTO regenerateReport(UUID reportId, UUID triggeredByUserId) {
224
+ GeneratedReport existingReport = generatedReportRepository.findById(reportId)
225
+ .orElseThrow(() -> new ResourceNotFoundException("GeneratedReport", "id", reportId));
226
+
227
+ // Create a new report generation based on the existing report's definition
228
+ return createAndQueueReportGeneration(existingReport.getReportDefinition(), null, triggeredByUserId);
229
+ }
230
+ }
src/main/java/com/dalab/reporting/service/notification/IEmailNotificationProvider.java ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package com.dalab.reporting.service.notification;
2
+
3
+ public interface IEmailNotificationProvider extends INotificationProvider {
4
+ // Potentially add email-specific methods here if needed in the future
5
+ }
src/main/java/com/dalab/reporting/service/notification/INotificationProvider.java ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service.notification;
2
+
3
+ import com.dalab.reporting.dto.NotificationRequestDTO;
4
+ import com.dalab.reporting.model.NotificationChannel;
5
+ import com.dalab.reporting.model.UserNotificationPreference;
6
+
7
+ public interface INotificationProvider {
8
+ NotificationChannel getChannelType();
9
+ void send(UserNotificationPreference preference, NotificationRequestDTO request); // Send based on user preference
10
+ void sendDirect(NotificationRequestDTO request, String targetAddress); // Send directly, e.g. email address or Slack channel ID
11
+ }
src/main/java/com/dalab/reporting/service/notification/ISlackNotificationProvider.java ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ package com.dalab.reporting.service.notification;
2
+
3
+ public interface ISlackNotificationProvider extends INotificationProvider {
4
+ // Potentially add Slack-specific methods here if needed in the future
5
+ // e.g., methods to format messages using Slack's Block Kit
6
+ }