Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/whistleblower_workflow_handlers.go
Sharang Parnerkar 13f57c4519 refactor(go): split obligations, portfolio, rbac, whistleblower handlers and stores, roadmap parser
Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC.
No exported symbols renamed; zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:00:15 +02:00

255 lines
7.0 KiB
Go

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