package handlers import ( "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // EscalationHandlers handles escalation-related API endpoints. type EscalationHandlers struct { store *ucca.EscalationStore assessmentStore *ucca.Store trigger *ucca.EscalationTrigger } // NewEscalationHandlers creates new escalation handlers. func NewEscalationHandlers(store *ucca.EscalationStore, assessmentStore *ucca.Store) *EscalationHandlers { return &EscalationHandlers{ store: store, assessmentStore: assessmentStore, trigger: ucca.DefaultEscalationTrigger(), } } // ============================================================================ // GET /sdk/v1/ucca/escalations - List escalations // ============================================================================ // ListEscalations returns escalations for a tenant with optional filters. func (h *EscalationHandlers) ListEscalations(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } status := c.Query("status") level := c.Query("level") var assignedTo *uuid.UUID if assignedToStr := c.Query("assigned_to"); assignedToStr != "" { if id, err := uuid.Parse(assignedToStr); err == nil { assignedTo = &id } } // If user is a reviewer, filter to their assignments by default userID := rbac.GetUserID(c) if c.Query("my_reviews") == "true" && userID != uuid.Nil { assignedTo = &userID } escalations, err := h.store.ListEscalations(c.Request.Context(), tenantID, status, level, assignedTo) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"escalations": escalations}) } // ============================================================================ // GET /sdk/v1/ucca/escalations/:id - Get single escalation // ============================================================================ // GetEscalation returns a single escalation by ID. func (h *EscalationHandlers) GetEscalation(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } escalation, err := h.store.GetEscalation(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if escalation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } // Get history history, _ := h.store.GetEscalationHistory(c.Request.Context(), id) c.JSON(http.StatusOK, gin.H{ "escalation": escalation, "history": history, }) } // ============================================================================ // POST /sdk/v1/ucca/escalations - Create escalation (manual) // ============================================================================ // CreateEscalation creates a manual escalation for an assessment. func (h *EscalationHandlers) CreateEscalation(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } var req ucca.CreateEscalationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get the assessment assessment, err := h.assessmentStore.GetAssessment(c.Request.Context(), req.AssessmentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assessment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "assessment not found"}) return } // Determine escalation level result := &ucca.AssessmentResult{ Feasibility: assessment.Feasibility, RiskLevel: assessment.RiskLevel, RiskScore: assessment.RiskScore, TriggeredRules: assessment.TriggeredRules, DSFARecommended: assessment.DSFARecommended, Art22Risk: assessment.Art22Risk, } level, reason := h.trigger.DetermineEscalationLevel(result) // Calculate due date based on SLA responseHours, _ := ucca.GetDefaultSLA(level) var dueDate *time.Time if responseHours > 0 { due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour) dueDate = &due } // Create escalation escalation := &ucca.Escalation{ TenantID: tenantID, AssessmentID: req.AssessmentID, EscalationLevel: level, EscalationReason: reason, Status: ucca.EscalationStatusPending, DueDate: dueDate, } // For E0, auto-approve if level == ucca.EscalationLevelE0 { escalation.Status = ucca.EscalationStatusApproved approveDecision := ucca.EscalationDecisionApprove escalation.Decision = &approveDecision now := time.Now().UTC() escalation.DecisionAt = &now autoNotes := "Automatische Freigabe (E0)" escalation.DecisionNotes = &autoNotes } if err := h.store.CreateEscalation(c.Request.Context(), escalation); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Add history entry h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "created", NewStatus: string(escalation.Status), NewLevel: string(escalation.EscalationLevel), ActorID: userID, Notes: reason, }) // For E1/E2/E3, try to auto-assign if level != ucca.EscalationLevelE0 { role := ucca.GetRoleForLevel(level) reviewer, err := h.store.GetNextAvailableReviewer(c.Request.Context(), tenantID, role) if err == nil && reviewer != nil { h.store.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role) h.store.IncrementReviewerCount(c.Request.Context(), reviewer.UserID) h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "auto_assigned", OldStatus: string(ucca.EscalationStatusPending), NewStatus: string(ucca.EscalationStatusAssigned), ActorID: userID, Notes: "Automatisch zugewiesen an: " + reviewer.UserName, }) } } c.JSON(http.StatusCreated, escalation) } // ============================================================================ // POST /sdk/v1/ucca/escalations/:id/assign - Assign escalation // ============================================================================ // AssignEscalation assigns an escalation to a reviewer. func (h *EscalationHandlers) AssignEscalation(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } userID := rbac.GetUserID(c) var req ucca.AssignEscalationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } escalation, err := h.store.GetEscalation(c.Request.Context(), id) if err != nil || escalation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"}) return } role := ucca.GetRoleForLevel(escalation.EscalationLevel) if err := h.store.AssignEscalation(c.Request.Context(), id, req.AssignedTo, role); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } h.store.IncrementReviewerCount(c.Request.Context(), req.AssignedTo) h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: id, Action: "assigned", OldStatus: string(escalation.Status), NewStatus: string(ucca.EscalationStatusAssigned), ActorID: userID, }) c.JSON(http.StatusOK, gin.H{"message": "assigned"}) } // ============================================================================ // POST /sdk/v1/ucca/escalations/:id/review - Start review // ============================================================================ // StartReview marks an escalation as being reviewed. func (h *EscalationHandlers) StartReview(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } userID := rbac.GetUserID(c) if userID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user ID required"}) return } escalation, err := h.store.GetEscalation(c.Request.Context(), id) if err != nil || escalation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"}) return } if err := h.store.StartReview(c.Request.Context(), id, userID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: id, Action: "review_started", OldStatus: string(escalation.Status), NewStatus: string(ucca.EscalationStatusInReview), ActorID: userID, }) c.JSON(http.StatusOK, gin.H{"message": "review started"}) } // ============================================================================ // POST /sdk/v1/ucca/escalations/:id/decide - Make decision // ============================================================================ // DecideEscalation makes a decision on an escalation. func (h *EscalationHandlers) DecideEscalation(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } userID := rbac.GetUserID(c) if userID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "user ID required"}) return } var req ucca.DecideEscalationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } escalation, err := h.store.GetEscalation(c.Request.Context(), id) if err != nil || escalation == nil { c.JSON(http.StatusNotFound, gin.H{"error": "escalation not found"}) return } if err := h.store.DecideEscalation(c.Request.Context(), id, req.Decision, req.DecisionNotes, req.Conditions); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Decrement reviewer count if escalation.AssignedTo != nil { h.store.DecrementReviewerCount(c.Request.Context(), *escalation.AssignedTo) } newStatus := "decided" switch req.Decision { case ucca.EscalationDecisionApprove: newStatus = string(ucca.EscalationStatusApproved) case ucca.EscalationDecisionReject: newStatus = string(ucca.EscalationStatusRejected) case ucca.EscalationDecisionModify: newStatus = string(ucca.EscalationStatusReturned) case ucca.EscalationDecisionEscalate: newStatus = "escalated" } h.store.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: id, Action: "decision_made", OldStatus: string(escalation.Status), NewStatus: newStatus, ActorID: userID, Notes: req.DecisionNotes, }) c.JSON(http.StatusOK, gin.H{"message": "decision recorded", "status": newStatus}) } // ============================================================================ // GET /sdk/v1/ucca/escalations/stats - Get statistics // ============================================================================ // GetEscalationStats returns escalation statistics. func (h *EscalationHandlers) GetEscalationStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } stats, err := h.store.GetEscalationStats(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // ============================================================================ // DSB Pool Management // ============================================================================ // ListDSBPool returns the DSB review pool for a tenant. func (h *EscalationHandlers) ListDSBPool(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } role := c.Query("role") members, err := h.store.GetDSBPoolMembers(c.Request.Context(), tenantID, role) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"members": members}) } // AddDSBPoolMember adds a member to the DSB pool. func (h *EscalationHandlers) AddDSBPoolMember(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } var member ucca.DSBPoolMember if err := c.ShouldBindJSON(&member); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } member.TenantID = tenantID member.IsActive = true if member.MaxConcurrentReviews == 0 { member.MaxConcurrentReviews = 10 } if err := h.store.AddDSBPoolMember(c.Request.Context(), &member); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, member) }