This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/obligations_handlers.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

540 lines
17 KiB
Go

package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
)
// ObligationsHandlers handles API requests for the generic obligations framework
type ObligationsHandlers struct {
registry *ucca.ObligationsRegistry
store *ucca.ObligationsStore // Optional: for persisting assessments
}
// NewObligationsHandlers creates a new ObligationsHandlers instance
func NewObligationsHandlers() *ObligationsHandlers {
return &ObligationsHandlers{
registry: ucca.NewObligationsRegistry(),
}
}
// NewObligationsHandlersWithStore creates a new ObligationsHandlers with a store
func NewObligationsHandlersWithStore(store *ucca.ObligationsStore) *ObligationsHandlers {
return &ObligationsHandlers{
registry: ucca.NewObligationsRegistry(),
store: store,
}
}
// RegisterRoutes registers all obligations-related routes
func (h *ObligationsHandlers) RegisterRoutes(r *gin.RouterGroup) {
obligations := r.Group("/obligations")
{
// Assessment endpoints
obligations.POST("/assess", h.AssessObligations)
obligations.GET("/:assessmentId", h.GetAssessment)
// Grouping/filtering endpoints
obligations.GET("/:assessmentId/by-regulation", h.GetByRegulation)
obligations.GET("/:assessmentId/by-deadline", h.GetByDeadline)
obligations.GET("/:assessmentId/by-responsible", h.GetByResponsible)
// Export endpoints
obligations.POST("/export/memo", h.ExportMemo)
obligations.POST("/export/direct", h.ExportMemoFromOverview)
// Metadata endpoints
obligations.GET("/regulations", h.ListRegulations)
obligations.GET("/regulations/:regulationId/decision-tree", h.GetDecisionTree)
// Quick check endpoint (no persistence)
obligations.POST("/quick-check", h.QuickCheck)
}
}
// AssessObligations assesses which obligations apply based on provided facts
// POST /sdk/v1/ucca/obligations/assess
func (h *ObligationsHandlers) AssessObligations(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Tenant ID required"})
return
}
var req ucca.ObligationsAssessRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
if req.Facts == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Facts are required"})
return
}
// Evaluate all regulations against the facts
overview := h.registry.EvaluateAll(tenantID, req.Facts, req.OrganizationName)
// Generate warnings if any
var warnings []string
if len(overview.ApplicableRegulations) == 0 {
warnings = append(warnings, "Keine der konfigurierten Regulierungen scheint anwendbar zu sein. Bitte prüfen Sie die eingegebenen Daten.")
}
if overview.ExecutiveSummary.OverdueObligations > 0 {
warnings = append(warnings, "Es gibt überfällige Pflichten, die sofortige Aufmerksamkeit erfordern.")
}
// Optionally persist the assessment
if h.store != nil {
assessment := &ucca.ObligationsAssessment{
ID: overview.ID,
TenantID: tenantID,
OrganizationName: req.OrganizationName,
Facts: req.Facts,
Overview: overview,
Status: "completed",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
CreatedBy: rbac.GetUserID(c),
}
if err := h.store.CreateAssessment(c.Request.Context(), assessment); err != nil {
// Log but don't fail - assessment was still generated
c.Set("store_error", err.Error())
}
}
c.JSON(http.StatusOK, ucca.ObligationsAssessResponse{
Overview: overview,
Warnings: warnings,
})
}
// GetAssessment retrieves a stored assessment by ID
// GET /sdk/v1/ucca/obligations/:assessmentId
func (h *ObligationsHandlers) GetAssessment(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
assessmentID := c.Param("assessmentId")
if h.store == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
return
}
id, err := uuid.Parse(assessmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
c.JSON(http.StatusOK, assessment.Overview)
}
// GetByRegulation returns obligations grouped by regulation
// GET /sdk/v1/ucca/obligations/:assessmentId/by-regulation
func (h *ObligationsHandlers) GetByRegulation(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
assessmentID := c.Param("assessmentId")
if h.store == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
return
}
id, err := uuid.Parse(assessmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
grouped := h.registry.GroupByRegulation(assessment.Overview.Obligations)
c.JSON(http.StatusOK, ucca.ObligationsByRegulationResponse{
Regulations: grouped,
})
}
// GetByDeadline returns obligations grouped by deadline timeframe
// GET /sdk/v1/ucca/obligations/:assessmentId/by-deadline
func (h *ObligationsHandlers) GetByDeadline(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
assessmentID := c.Param("assessmentId")
if h.store == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
return
}
id, err := uuid.Parse(assessmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
grouped := h.registry.GroupByDeadline(assessment.Overview.Obligations)
c.JSON(http.StatusOK, grouped)
}
// GetByResponsible returns obligations grouped by responsible role
// GET /sdk/v1/ucca/obligations/:assessmentId/by-responsible
func (h *ObligationsHandlers) GetByResponsible(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
assessmentID := c.Param("assessmentId")
if h.store == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
return
}
id, err := uuid.Parse(assessmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
grouped := h.registry.GroupByResponsible(assessment.Overview.Obligations)
c.JSON(http.StatusOK, ucca.ObligationsByResponsibleResponse{
ByRole: grouped,
})
}
// ExportMemo exports the obligations overview as a C-Level memo
// POST /sdk/v1/ucca/obligations/export/memo
func (h *ObligationsHandlers) ExportMemo(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
var req ucca.ExportMemoRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if h.store == nil {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Persistence not configured"})
return
}
id, err := uuid.Parse(req.AssessmentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid assessment ID"})
return
}
assessment, err := h.store.GetAssessment(c.Request.Context(), tenantID, id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Assessment not found"})
return
}
// Create exporter
exporter := ucca.NewPDFExporter(req.Language)
// Generate export based on format
var response *ucca.ExportMemoResponse
switch req.Format {
case "pdf":
response, err = exporter.ExportManagementMemo(assessment.Overview)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF", "details": err.Error()})
return
}
case "markdown", "":
response, err = exporter.ExportMarkdown(assessment.Overview)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Markdown", "details": err.Error()})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid format. Use 'markdown' or 'pdf'"})
return
}
c.JSON(http.StatusOK, response)
}
// ExportMemoFromOverview exports an overview directly (without persistence)
// POST /sdk/v1/ucca/obligations/export/direct
func (h *ObligationsHandlers) ExportMemoFromOverview(c *gin.Context) {
var req struct {
Overview *ucca.ManagementObligationsOverview `json:"overview"`
Format string `json:"format"` // "markdown" or "pdf"
Language string `json:"language,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if req.Overview == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Overview is required"})
return
}
exporter := ucca.NewPDFExporter(req.Language)
var response *ucca.ExportMemoResponse
var err error
switch req.Format {
case "pdf":
response, err = exporter.ExportManagementMemo(req.Overview)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate PDF", "details": err.Error()})
return
}
case "markdown", "":
response, err = exporter.ExportMarkdown(req.Overview)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Markdown", "details": err.Error()})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid format. Use 'markdown' or 'pdf'"})
return
}
c.JSON(http.StatusOK, response)
}
// ListRegulations returns all available regulation modules
// GET /sdk/v1/ucca/obligations/regulations
func (h *ObligationsHandlers) ListRegulations(c *gin.Context) {
modules := h.registry.ListModules()
c.JSON(http.StatusOK, ucca.AvailableRegulationsResponse{
Regulations: modules,
})
}
// GetDecisionTree returns the decision tree for a specific regulation
// GET /sdk/v1/ucca/obligations/regulations/:regulationId/decision-tree
func (h *ObligationsHandlers) GetDecisionTree(c *gin.Context) {
regulationID := c.Param("regulationId")
tree, err := h.registry.GetDecisionTree(regulationID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, tree)
}
// QuickCheck performs a quick obligations check without persistence
// POST /sdk/v1/ucca/obligations/quick-check
func (h *ObligationsHandlers) QuickCheck(c *gin.Context) {
var req struct {
// Organization basics
EmployeeCount int `json:"employee_count"`
AnnualRevenue float64 `json:"annual_revenue"`
BalanceSheetTotal float64 `json:"balance_sheet_total,omitempty"`
Country string `json:"country"`
// Sector
PrimarySector string `json:"primary_sector"`
SpecialServices []string `json:"special_services,omitempty"`
IsKRITIS bool `json:"is_kritis,omitempty"`
// Quick flags
ProcessesPersonalData bool `json:"processes_personal_data,omitempty"`
UsesAI bool `json:"uses_ai,omitempty"`
IsFinancialInstitution bool `json:"is_financial_institution,omitempty"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()})
return
}
// Build UnifiedFacts from quick check request
facts := &ucca.UnifiedFacts{
Organization: ucca.OrganizationFacts{
EmployeeCount: req.EmployeeCount,
AnnualRevenue: req.AnnualRevenue,
BalanceSheetTotal: req.BalanceSheetTotal,
Country: req.Country,
EUMember: isEUCountry(req.Country),
},
Sector: ucca.SectorFacts{
PrimarySector: req.PrimarySector,
SpecialServices: req.SpecialServices,
IsKRITIS: req.IsKRITIS,
KRITISThresholdMet: req.IsKRITIS,
IsFinancialInstitution: req.IsFinancialInstitution,
},
DataProtection: ucca.DataProtectionFacts{
ProcessesPersonalData: req.ProcessesPersonalData,
},
AIUsage: ucca.AIUsageFacts{
UsesAI: req.UsesAI,
},
Financial: ucca.FinancialFacts{
IsRegulated: req.IsFinancialInstitution,
},
}
// Quick evaluation
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
tenantID = uuid.New() // Generate temporary ID for quick check
}
overview := h.registry.EvaluateAll(tenantID, facts, "")
// Return simplified result
c.JSON(http.StatusOK, gin.H{
"applicable_regulations": overview.ApplicableRegulations,
"total_obligations": len(overview.Obligations),
"critical_obligations": overview.ExecutiveSummary.CriticalObligations,
"sanctions_summary": overview.SanctionsSummary,
"executive_summary": overview.ExecutiveSummary,
})
}
// ============================================================================
// Helper Functions
// ============================================================================
func generateMemoMarkdown(overview *ucca.ManagementObligationsOverview) string {
content := "# Pflichten-Übersicht für die Geschäftsführung\n\n"
content += "**Datum:** " + overview.AssessmentDate.Format("02.01.2006") + "\n"
if overview.OrganizationName != "" {
content += "**Organisation:** " + overview.OrganizationName + "\n"
}
content += "\n---\n\n"
// Executive Summary
content += "## Executive Summary\n\n"
content += "| Kennzahl | Wert |\n"
content += "|----------|------|\n"
content += "| Anwendbare Regulierungen | " + itoa(overview.ExecutiveSummary.TotalRegulations) + " |\n"
content += "| Gesamtzahl Pflichten | " + itoa(overview.ExecutiveSummary.TotalObligations) + " |\n"
content += "| Kritische Pflichten | " + itoa(overview.ExecutiveSummary.CriticalObligations) + " |\n"
content += "| Überfällige Pflichten | " + itoa(overview.ExecutiveSummary.OverdueObligations) + " |\n"
content += "| Anstehende Fristen (30 Tage) | " + itoa(overview.ExecutiveSummary.UpcomingDeadlines) + " |\n"
content += "\n"
// Key Risks
if len(overview.ExecutiveSummary.KeyRisks) > 0 {
content += "### Hauptrisiken\n\n"
for _, risk := range overview.ExecutiveSummary.KeyRisks {
content += "- ⚠️ " + risk + "\n"
}
content += "\n"
}
// Recommended Actions
if len(overview.ExecutiveSummary.RecommendedActions) > 0 {
content += "### Empfohlene Maßnahmen\n\n"
for i, action := range overview.ExecutiveSummary.RecommendedActions {
content += itoa(i+1) + ". " + action + "\n"
}
content += "\n"
}
// Applicable Regulations
content += "## Anwendbare Regulierungen\n\n"
for _, reg := range overview.ApplicableRegulations {
content += "### " + reg.Name + "\n\n"
content += "- **Klassifizierung:** " + reg.Classification + "\n"
content += "- **Begründung:** " + reg.Reason + "\n"
content += "- **Anzahl Pflichten:** " + itoa(reg.ObligationCount) + "\n"
content += "\n"
}
// Sanctions Summary
content += "## Sanktionsrisiken\n\n"
content += overview.SanctionsSummary.Summary + "\n\n"
if overview.SanctionsSummary.MaxFinancialRisk != "" {
content += "- **Maximales Bußgeld:** " + overview.SanctionsSummary.MaxFinancialRisk + "\n"
}
if overview.SanctionsSummary.PersonalLiabilityRisk {
content += "- **Persönliche Haftung:** Ja ⚠️\n"
}
content += "\n"
// Critical Obligations
content += "## Kritische Pflichten\n\n"
for _, obl := range overview.Obligations {
if obl.Priority == ucca.PriorityCritical {
content += "### " + obl.ID + ": " + obl.Title + "\n\n"
content += obl.Description + "\n\n"
content += "- **Verantwortlich:** " + string(obl.Responsible) + "\n"
if obl.Deadline != nil {
if obl.Deadline.Date != nil {
content += "- **Frist:** " + obl.Deadline.Date.Format("02.01.2006") + "\n"
} else if obl.Deadline.Duration != "" {
content += "- **Frist:** " + obl.Deadline.Duration + "\n"
}
}
if obl.Sanctions != nil && obl.Sanctions.MaxFine != "" {
content += "- **Sanktion:** " + obl.Sanctions.MaxFine + "\n"
}
content += "\n"
}
}
// Incident Deadlines
if len(overview.IncidentDeadlines) > 0 {
content += "## Meldepflichten bei Sicherheitsvorfällen\n\n"
content += "| Phase | Frist | Empfänger |\n"
content += "|-------|-------|-----------|\n"
for _, deadline := range overview.IncidentDeadlines {
content += "| " + deadline.Phase + " | " + deadline.Deadline + " | " + deadline.Recipient + " |\n"
}
content += "\n"
}
content += "---\n\n"
content += "*Dieses Dokument wurde automatisch generiert und ersetzt keine Rechtsberatung.*\n"
return content
}
func isEUCountry(country string) bool {
euCountries := map[string]bool{
"DE": true, "AT": true, "BE": true, "BG": true, "HR": true, "CY": true,
"CZ": true, "DK": true, "EE": true, "FI": true, "FR": true, "GR": true,
"HU": true, "IE": true, "IT": true, "LV": true, "LT": true, "LU": true,
"MT": true, "NL": true, "PL": true, "PT": true, "RO": true, "SK": true,
"SI": true, "ES": true, "SE": true,
}
return euCountries[country]
}
func itoa(i int) string {
return strconv.Itoa(i)
}