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) }