feat: Add Academy, Whistleblower, Incidents, Vendor, DSB, SSO, Reporting, Multi-Tenant and Industry backends

Go handlers, models, stores and migrations for all SDK modules.
Updates developer portal navigation and BYOEH page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-13 21:11:27 +01:00
parent 364d2c69ff
commit 504dd3591b
40 changed files with 13105 additions and 7 deletions

View File

@@ -0,0 +1,97 @@
package reporting
import "time"
type ExecutiveReport struct {
GeneratedAt time.Time `json:"generated_at"`
TenantID string `json:"tenant_id"`
ComplianceScore int `json:"compliance_score"` // 0-100 overall score
// Module summaries
DSGVO DSGVOSummary `json:"dsgvo"`
Vendors VendorSummary `json:"vendors"`
Incidents IncidentSummary `json:"incidents"`
Whistleblower WhistleblowerSummary `json:"whistleblower"`
Academy AcademySummary `json:"academy"`
// Cross-module metrics
RiskOverview RiskOverview `json:"risk_overview"`
UpcomingDeadlines []Deadline `json:"upcoming_deadlines"`
RecentActivity []ActivityEntry `json:"recent_activity"`
}
type DSGVOSummary struct {
ProcessingActivities int `json:"processing_activities"`
ActiveProcessings int `json:"active_processings"`
TOMsImplemented int `json:"toms_implemented"`
TOMsPlanned int `json:"toms_planned"`
TOMsTotal int `json:"toms_total"`
CompletionPercent int `json:"completion_percent"` // TOMsImplemented / total * 100
OpenDSRs int `json:"open_dsrs"`
OverdueDSRs int `json:"overdue_dsrs"`
DSFAsCompleted int `json:"dsfas_completed"`
RetentionPolicies int `json:"retention_policies"`
}
type VendorSummary struct {
TotalVendors int `json:"total_vendors"`
ActiveVendors int `json:"active_vendors"`
ByRiskLevel map[string]int `json:"by_risk_level"`
PendingReviews int `json:"pending_reviews"`
ExpiredContracts int `json:"expired_contracts"`
}
type IncidentSummary struct {
TotalIncidents int `json:"total_incidents"`
OpenIncidents int `json:"open_incidents"`
CriticalIncidents int `json:"critical_incidents"`
NotificationsPending int `json:"notifications_pending"`
AvgResolutionHours float64 `json:"avg_resolution_hours"`
}
type WhistleblowerSummary struct {
TotalReports int `json:"total_reports"`
OpenReports int `json:"open_reports"`
OverdueAcknowledgments int `json:"overdue_acknowledgments"`
OverdueFeedbacks int `json:"overdue_feedbacks"`
AvgResolutionDays float64 `json:"avg_resolution_days"`
}
type AcademySummary struct {
TotalCourses int `json:"total_courses"`
TotalEnrollments int `json:"total_enrollments"`
CompletionRate float64 `json:"completion_rate"` // 0-100
OverdueCount int `json:"overdue_count"`
AvgCompletionDays float64 `json:"avg_completion_days"`
}
type RiskOverview struct {
OverallLevel string `json:"overall_level"` // LOW, MEDIUM, HIGH, CRITICAL
ModuleRisks []ModuleRisk `json:"module_risks"`
OpenFindings int `json:"open_findings"`
CriticalFindings int `json:"critical_findings"`
}
type ModuleRisk struct {
Module string `json:"module"`
Level string `json:"level"` // LOW, MEDIUM, HIGH, CRITICAL
Score int `json:"score"` // 0-100
Issues int `json:"issues"`
}
type Deadline struct {
Module string `json:"module"`
Type string `json:"type"`
Description string `json:"description"`
DueDate time.Time `json:"due_date"`
DaysLeft int `json:"days_left"`
Severity string `json:"severity"` // INFO, WARNING, URGENT, OVERDUE
}
type ActivityEntry struct {
Timestamp time.Time `json:"timestamp"`
Module string `json:"module"`
Action string `json:"action"`
Description string `json:"description"`
UserID string `json:"user_id,omitempty"`
}

View File

@@ -0,0 +1,520 @@
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
}