package reporting import ( "context" "math" "sort" "time" "github.com/breakpilot/ai-compliance-sdk/internal/academy" "github.com/breakpilot/ai-compliance-sdk/internal/dsgvo" "github.com/breakpilot/ai-compliance-sdk/internal/incidents" "github.com/breakpilot/ai-compliance-sdk/internal/vendor" "github.com/breakpilot/ai-compliance-sdk/internal/whistleblower" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type Store struct { pool *pgxpool.Pool dsgvoStore *dsgvo.Store vendorStore *vendor.Store incidentStore *incidents.Store whistleStore *whistleblower.Store academyStore *academy.Store } func NewStore(pool *pgxpool.Pool, ds *dsgvo.Store, vs *vendor.Store, is *incidents.Store, ws *whistleblower.Store, as *academy.Store) *Store { return &Store{ pool: pool, dsgvoStore: ds, vendorStore: vs, incidentStore: is, whistleStore: ws, academyStore: as, } } func (s *Store) GenerateReport(ctx context.Context, tenantID uuid.UUID) (*ExecutiveReport, error) { report := &ExecutiveReport{ GeneratedAt: time.Now().UTC(), TenantID: tenantID.String(), } tid := tenantID.String() // 1. Gather DSGVO stats dsgvoStats, err := s.dsgvoStore.GetStats(ctx, tenantID) if err == nil && dsgvoStats != nil { total := dsgvoStats.TOMsImplemented + dsgvoStats.TOMsPlanned pct := 0 if total > 0 { pct = int(math.Round(float64(dsgvoStats.TOMsImplemented) / float64(total) * 100)) } report.DSGVO = DSGVOSummary{ ProcessingActivities: dsgvoStats.ProcessingActivities, ActiveProcessings: dsgvoStats.ActiveProcessings, TOMsImplemented: dsgvoStats.TOMsImplemented, TOMsPlanned: dsgvoStats.TOMsPlanned, TOMsTotal: total, CompletionPercent: pct, OpenDSRs: dsgvoStats.OpenDSRs, OverdueDSRs: dsgvoStats.OverdueDSRs, DSFAsCompleted: dsgvoStats.DSFAsCompleted, RetentionPolicies: dsgvoStats.RetentionPolicies, } } // 2. Gather vendor stats vendorStats, err := s.vendorStore.GetVendorStats(ctx, tid) if err == nil && vendorStats != nil { active := 0 if v, ok := vendorStats.ByStatus["ACTIVE"]; ok { active = v } report.Vendors = VendorSummary{ TotalVendors: vendorStats.TotalVendors, ActiveVendors: active, ByRiskLevel: vendorStats.ByRiskLevel, PendingReviews: vendorStats.PendingReviews, ExpiredContracts: vendorStats.ExpiredContracts, } } // 3. Gather incident stats incidentStats, err := s.incidentStore.GetStatistics(ctx, tenantID) if err == nil && incidentStats != nil { critical := 0 if v, ok := incidentStats.BySeverity["CRITICAL"]; ok { critical = v } report.Incidents = IncidentSummary{ TotalIncidents: incidentStats.TotalIncidents, OpenIncidents: incidentStats.OpenIncidents, CriticalIncidents: critical, NotificationsPending: incidentStats.NotificationsPending, AvgResolutionHours: incidentStats.AvgResolutionHours, } } // 4. Gather whistleblower stats whistleStats, err := s.whistleStore.GetStatistics(ctx, tenantID) if err == nil && whistleStats != nil { openReports := 0 for status, count := range whistleStats.ByStatus { if status != "CLOSED" && status != "ARCHIVED" { openReports += count } } report.Whistleblower = WhistleblowerSummary{ TotalReports: whistleStats.TotalReports, OpenReports: openReports, OverdueAcknowledgments: whistleStats.OverdueAcknowledgments, OverdueFeedbacks: whistleStats.OverdueFeedbacks, AvgResolutionDays: whistleStats.AvgResolutionDays, } } // 5. Gather academy stats academyStats, err := s.academyStore.GetStatistics(ctx, tenantID) if err == nil && academyStats != nil { report.Academy = AcademySummary{ TotalCourses: academyStats.TotalCourses, TotalEnrollments: academyStats.TotalEnrollments, CompletionRate: academyStats.CompletionRate, OverdueCount: academyStats.OverdueCount, AvgCompletionDays: academyStats.AvgCompletionDays, } } // 6. Calculate risk overview report.RiskOverview = s.calculateRiskOverview(report) // 7. Calculate compliance score (0-100) report.ComplianceScore = s.calculateComplianceScore(report) // 8. Gather upcoming deadlines from DB report.UpcomingDeadlines = s.getUpcomingDeadlines(ctx, tenantID) // 9. Gather recent activity from DB report.RecentActivity = s.getRecentActivity(ctx, tenantID) return report, nil } func (s *Store) calculateRiskOverview(report *ExecutiveReport) RiskOverview { modules := []ModuleRisk{} // DSGVO risk based on overdue DSRs and missing TOMs dsgvoScore := 100 dsgvoIssues := report.DSGVO.OverdueDSRs + report.DSGVO.TOMsPlanned if report.DSGVO.OverdueDSRs > 0 { dsgvoScore -= report.DSGVO.OverdueDSRs * 15 } if report.DSGVO.TOMsTotal > 0 { dsgvoScore = int(math.Round(float64(report.DSGVO.CompletionPercent))) } if dsgvoScore < 0 { dsgvoScore = 0 } modules = append(modules, ModuleRisk{Module: "DSGVO", Level: riskLevel(dsgvoScore), Score: dsgvoScore, Issues: dsgvoIssues}) // Vendor risk based on high-risk vendors and pending reviews vendorScore := 100 vendorIssues := report.Vendors.PendingReviews + report.Vendors.ExpiredContracts highRisk := 0 if v, ok := report.Vendors.ByRiskLevel["HIGH"]; ok { highRisk += v } if v, ok := report.Vendors.ByRiskLevel["CRITICAL"]; ok { highRisk += v } if report.Vendors.TotalVendors > 0 { vendorScore = 100 - int(math.Round(float64(highRisk)/float64(report.Vendors.TotalVendors)*100)) } vendorScore -= report.Vendors.PendingReviews * 5 vendorScore -= report.Vendors.ExpiredContracts * 10 if vendorScore < 0 { vendorScore = 0 } modules = append(modules, ModuleRisk{Module: "Vendors", Level: riskLevel(vendorScore), Score: vendorScore, Issues: vendorIssues}) // Incident risk incidentScore := 100 incidentIssues := report.Incidents.OpenIncidents incidentScore -= report.Incidents.CriticalIncidents * 20 incidentScore -= report.Incidents.OpenIncidents * 5 incidentScore -= report.Incidents.NotificationsPending * 15 if incidentScore < 0 { incidentScore = 0 } modules = append(modules, ModuleRisk{Module: "Incidents", Level: riskLevel(incidentScore), Score: incidentScore, Issues: incidentIssues}) // Whistleblower compliance whistleScore := 100 whistleIssues := report.Whistleblower.OverdueAcknowledgments + report.Whistleblower.OverdueFeedbacks whistleScore -= report.Whistleblower.OverdueAcknowledgments * 20 whistleScore -= report.Whistleblower.OverdueFeedbacks * 10 if whistleScore < 0 { whistleScore = 0 } modules = append(modules, ModuleRisk{Module: "Whistleblower", Level: riskLevel(whistleScore), Score: whistleScore, Issues: whistleIssues}) // Academy compliance academyScore := int(math.Round(report.Academy.CompletionRate)) academyIssues := report.Academy.OverdueCount modules = append(modules, ModuleRisk{Module: "Academy", Level: riskLevel(academyScore), Score: academyScore, Issues: academyIssues}) // Overall score is the average across modules totalScore := 0 for _, m := range modules { totalScore += m.Score } if len(modules) > 0 { totalScore = totalScore / len(modules) } totalFindings := 0 criticalFindings := 0 for _, m := range modules { totalFindings += m.Issues if m.Level == "CRITICAL" { criticalFindings += m.Issues } } return RiskOverview{ OverallLevel: riskLevel(totalScore), ModuleRisks: modules, OpenFindings: totalFindings, CriticalFindings: criticalFindings, } } func riskLevel(score int) string { switch { case score >= 75: return "LOW" case score >= 50: return "MEDIUM" case score >= 25: return "HIGH" default: return "CRITICAL" } } func (s *Store) calculateComplianceScore(report *ExecutiveReport) int { scores := []int{} weights := []int{} // DSGVO: weight 30 (most important) if report.DSGVO.TOMsTotal > 0 { scores = append(scores, report.DSGVO.CompletionPercent) } else { scores = append(scores, 0) } weights = append(weights, 30) // Vendor compliance: weight 20 vendorScore := 100 if report.Vendors.TotalVendors > 0 { vendorScore -= report.Vendors.PendingReviews * 10 vendorScore -= report.Vendors.ExpiredContracts * 15 } if vendorScore < 0 { vendorScore = 0 } scores = append(scores, vendorScore) weights = append(weights, 20) // Incident handling: weight 20 incidentScore := 100 incidentScore -= report.Incidents.OpenIncidents * 10 incidentScore -= report.Incidents.NotificationsPending * 20 if incidentScore < 0 { incidentScore = 0 } scores = append(scores, incidentScore) weights = append(weights, 20) // Whistleblower: weight 15 whistleScore := 100 whistleScore -= report.Whistleblower.OverdueAcknowledgments * 25 whistleScore -= report.Whistleblower.OverdueFeedbacks * 15 if whistleScore < 0 { whistleScore = 0 } scores = append(scores, whistleScore) weights = append(weights, 15) // Academy: weight 15 academyScore := int(math.Round(report.Academy.CompletionRate)) scores = append(scores, academyScore) weights = append(weights, 15) totalWeight := 0 weightedSum := 0 for i, sc := range scores { weightedSum += sc * weights[i] totalWeight += weights[i] } if totalWeight == 0 { return 0 } return int(math.Round(float64(weightedSum) / float64(totalWeight))) } func (s *Store) getUpcomingDeadlines(ctx context.Context, tenantID uuid.UUID) []Deadline { deadlines := []Deadline{} now := time.Now().UTC() // Vendor reviews due rows, err := s.pool.Query(ctx, ` SELECT name, next_review_date FROM vendor_vendors WHERE tenant_id = $1 AND next_review_date IS NOT NULL ORDER BY next_review_date ASC LIMIT 10 `, tenantID) if err == nil { defer rows.Close() for rows.Next() { var name string var dueDate time.Time if err := rows.Scan(&name, &dueDate); err != nil { continue } daysLeft := int(dueDate.Sub(now).Hours() / 24) severity := "INFO" if daysLeft < 0 { severity = "OVERDUE" } else if daysLeft <= 7 { severity = "URGENT" } else if daysLeft <= 30 { severity = "WARNING" } deadlines = append(deadlines, Deadline{ Module: "Vendors", Type: "REVIEW", Description: "Vendor-Review: " + name, DueDate: dueDate, DaysLeft: daysLeft, Severity: severity, }) } } // Contract expirations rows2, err := s.pool.Query(ctx, ` SELECT vv.name, vc.expiration_date, vc.document_type FROM vendor_contracts vc JOIN vendor_vendors vv ON vc.vendor_id = vv.id WHERE vc.tenant_id = $1 AND vc.expiration_date IS NOT NULL ORDER BY vc.expiration_date ASC LIMIT 10 `, tenantID) if err == nil { defer rows2.Close() for rows2.Next() { var name, docType string var dueDate time.Time if err := rows2.Scan(&name, &dueDate, &docType); err != nil { continue } daysLeft := int(dueDate.Sub(now).Hours() / 24) severity := "INFO" if daysLeft < 0 { severity = "OVERDUE" } else if daysLeft <= 14 { severity = "URGENT" } else if daysLeft <= 60 { severity = "WARNING" } deadlines = append(deadlines, Deadline{ Module: "Contracts", Type: "EXPIRATION", Description: docType + " läuft ab: " + name, DueDate: dueDate, DaysLeft: daysLeft, Severity: severity, }) } } // DSR deadlines (overdue) rows3, err := s.pool.Query(ctx, ` SELECT request_type, deadline FROM dsgvo_dsr_requests WHERE tenant_id = $1 AND status NOT IN ('COMPLETED', 'REJECTED') AND deadline IS NOT NULL ORDER BY deadline ASC LIMIT 10 `, tenantID) if err == nil { defer rows3.Close() for rows3.Next() { var reqType string var dueDate time.Time if err := rows3.Scan(&reqType, &dueDate); err != nil { continue } daysLeft := int(dueDate.Sub(now).Hours() / 24) severity := "INFO" if daysLeft < 0 { severity = "OVERDUE" } else if daysLeft <= 3 { severity = "URGENT" } else if daysLeft <= 14 { severity = "WARNING" } deadlines = append(deadlines, Deadline{ Module: "DSR", Type: "RESPONSE", Description: "Betroffenenrecht: " + reqType, DueDate: dueDate, DaysLeft: daysLeft, Severity: severity, }) } } // Sort by due date ascending sort.Slice(deadlines, func(i, j int) bool { return deadlines[i].DueDate.Before(deadlines[j].DueDate) }) // Limit to top 15 if len(deadlines) > 15 { deadlines = deadlines[:15] } return deadlines } func (s *Store) getRecentActivity(ctx context.Context, tenantID uuid.UUID) []ActivityEntry { activities := []ActivityEntry{} // Recent vendors created/updated rows, _ := s.pool.Query(ctx, ` SELECT name, created_at, 'CREATED' as action FROM vendor_vendors WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days' UNION ALL SELECT name, updated_at, 'UPDATED' FROM vendor_vendors WHERE tenant_id = $1 AND updated_at > created_at AND updated_at > NOW() - INTERVAL '30 days' ORDER BY 2 DESC LIMIT 5 `, tenantID) if rows != nil { defer rows.Close() for rows.Next() { var name, action string var ts time.Time if err := rows.Scan(&name, &ts, &action); err != nil { continue } desc := "Vendor " if action == "CREATED" { desc += "angelegt: " } else { desc += "aktualisiert: " } activities = append(activities, ActivityEntry{ Timestamp: ts, Module: "Vendors", Action: action, Description: desc + name, }) } } // Recent incidents rows2, _ := s.pool.Query(ctx, ` SELECT title, created_at, severity FROM incidents WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 5 `, tenantID) if rows2 != nil { defer rows2.Close() for rows2.Next() { var title, severity string var ts time.Time if err := rows2.Scan(&title, &ts, &severity); err != nil { continue } activities = append(activities, ActivityEntry{ Timestamp: ts, Module: "Incidents", Action: "CREATED", Description: "Datenpanne (" + severity + "): " + title, }) } } // Recent whistleblower reports (admin view) rows3, _ := s.pool.Query(ctx, ` SELECT category, created_at FROM whistleblower_reports WHERE tenant_id = $1 AND created_at > NOW() - INTERVAL '30 days' ORDER BY created_at DESC LIMIT 5 `, tenantID) if rows3 != nil { defer rows3.Close() for rows3.Next() { var category string var ts time.Time if err := rows3.Scan(&category, &ts); err != nil { continue } activities = append(activities, ActivityEntry{ Timestamp: ts, Module: "Whistleblower", Action: "REPORT", Description: "Neue Meldung: " + category, }) } } // Sort by timestamp descending (most recent first) sort.Slice(activities, func(i, j int) bool { return activities[i].Timestamp.After(activities[j].Timestamp) }) if len(activities) > 20 { activities = activities[:20] } return activities }