package handlers import ( "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/whistleblower" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // AcknowledgeReport acknowledges a report (within 7-day HinSchG deadline) // POST /sdk/v1/whistleblower/reports/:id/acknowledge func (h *WhistleblowerHandlers) AcknowledgeReport(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } report, err := h.store.GetReport(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if report == nil { c.JSON(http.StatusNotFound, gin.H{"error": "report not found"}) return } if report.AcknowledgedAt != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "report already acknowledged"}) return } userID := rbac.GetUserID(c) if err := h.store.AcknowledgeReport(c.Request.Context(), id, userID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } var req whistleblower.AcknowledgeRequest if err := c.ShouldBindJSON(&req); err == nil && req.Message != "" { msg := &whistleblower.AnonymousMessage{ ReportID: id, Direction: whistleblower.MessageDirectionAdminToReporter, Content: req.Message, } h.store.AddMessage(c.Request.Context(), msg) } isOverdue := time.Now().UTC().After(report.DeadlineAcknowledgment) c.JSON(http.StatusOK, gin.H{ "message": "report acknowledged", "is_overdue": isOverdue, }) } // StartInvestigation changes the report status to investigation // POST /sdk/v1/whistleblower/reports/:id/investigate func (h *WhistleblowerHandlers) StartInvestigation(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } report, err := h.store.GetReport(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if report == nil { c.JSON(http.StatusNotFound, gin.H{"error": "report not found"}) return } userID := rbac.GetUserID(c) report.Status = whistleblower.ReportStatusInvestigation report.AuditTrail = append(report.AuditTrail, whistleblower.AuditEntry{ Timestamp: time.Now().UTC(), Action: "investigation_started", UserID: userID.String(), Details: "Investigation started", }) if err := h.store.UpdateReport(c.Request.Context(), report); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "message": "investigation started", "report": report, }) } // AddMeasure adds a corrective measure to a report // POST /sdk/v1/whistleblower/reports/:id/measures func (h *WhistleblowerHandlers) AddMeasure(c *gin.Context) { reportID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } report, err := h.store.GetReport(c.Request.Context(), reportID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if report == nil { c.JSON(http.StatusNotFound, gin.H{"error": "report not found"}) return } var req whistleblower.AddMeasureRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) measure := &whistleblower.Measure{ ReportID: reportID, Title: req.Title, Description: req.Description, Responsible: req.Responsible, DueDate: req.DueDate, } if err := h.store.AddMeasure(c.Request.Context(), measure); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if report.Status != whistleblower.ReportStatusMeasuresTaken && report.Status != whistleblower.ReportStatusClosed { report.Status = whistleblower.ReportStatusMeasuresTaken report.AuditTrail = append(report.AuditTrail, whistleblower.AuditEntry{ Timestamp: time.Now().UTC(), Action: "measure_added", UserID: userID.String(), Details: "Corrective measure added: " + req.Title, }) h.store.UpdateReport(c.Request.Context(), report) } c.JSON(http.StatusCreated, gin.H{"measure": measure}) } // CloseReport closes a report with a resolution // POST /sdk/v1/whistleblower/reports/:id/close func (h *WhistleblowerHandlers) CloseReport(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } var req whistleblower.CloseReportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID := rbac.GetUserID(c) if err := h.store.CloseReport(c.Request.Context(), id, userID, req.Resolution); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "report closed"}) } // SendAdminMessage sends a message from admin to reporter // POST /sdk/v1/whistleblower/reports/:id/messages func (h *WhistleblowerHandlers) SendAdminMessage(c *gin.Context) { reportID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } report, err := h.store.GetReport(c.Request.Context(), reportID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if report == nil { c.JSON(http.StatusNotFound, gin.H{"error": "report not found"}) return } var req whistleblower.SendMessageRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } msg := &whistleblower.AnonymousMessage{ ReportID: reportID, Direction: whistleblower.MessageDirectionAdminToReporter, Content: req.Content, } if err := h.store.AddMessage(c.Request.Context(), msg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{"message": msg}) } // ListMessages lists messages for a report // GET /sdk/v1/whistleblower/reports/:id/messages func (h *WhistleblowerHandlers) ListMessages(c *gin.Context) { reportID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"}) return } messages, err := h.store.ListMessages(c.Request.Context(), reportID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "messages": messages, "total": len(messages), }) } // GetStatistics returns whistleblower statistics for the tenant // GET /sdk/v1/whistleblower/statistics func (h *WhistleblowerHandlers) GetStatistics(c *gin.Context) { tenantID := rbac.GetTenantID(c) stats, err := h.store.GetStatistics(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) }