Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/portfolio_stats_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

231 lines
6.4 KiB
Go

package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/portfolio"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ============================================================================
// Statistics & Reports
// ============================================================================
// GetPortfolioStats returns statistics for a portfolio
// GET /sdk/v1/portfolios/:id/stats
func (h *PortfolioHandlers) GetPortfolioStats(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
stats, err := h.store.GetPortfolioStats(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// GetPortfolioActivity returns recent activity for a portfolio
// GET /sdk/v1/portfolios/:id/activity
func (h *PortfolioHandlers) GetPortfolioActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
limit := 20
if l := c.Query("limit"); l != "" {
if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 {
limit = parsed
}
}
activities, err := h.store.GetRecentActivity(c.Request.Context(), id, limit)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"activities": activities,
"total": len(activities),
})
}
// ComparePortfolios compares multiple portfolios
// POST /sdk/v1/portfolios/compare
func (h *PortfolioHandlers) ComparePortfolios(c *gin.Context) {
var req portfolio.ComparePortfoliosRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(req.PortfolioIDs) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "at least 2 portfolios required for comparison"})
return
}
if len(req.PortfolioIDs) > 5 {
c.JSON(http.StatusBadRequest, gin.H{"error": "maximum 5 portfolios can be compared"})
return
}
var portfolios []portfolio.Portfolio
comparison := portfolio.PortfolioComparison{
RiskScores: make(map[string]float64),
ComplianceScores: make(map[string]float64),
ItemCounts: make(map[string]int),
UniqueItems: make(map[string][]uuid.UUID),
}
allItems := make(map[uuid.UUID][]string)
for _, id := range req.PortfolioIDs {
p, err := h.store.GetPortfolio(c.Request.Context(), id)
if err != nil || p == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "portfolio not found: " + id.String()})
return
}
portfolios = append(portfolios, *p)
idStr := id.String()
comparison.RiskScores[idStr] = p.AvgRiskScore
comparison.ComplianceScores[idStr] = p.ComplianceScore
comparison.ItemCounts[idStr] = p.TotalAssessments + p.TotalRoadmaps + p.TotalWorkshops
items, _ := h.store.ListItems(c.Request.Context(), id, nil)
for _, item := range items {
allItems[item.ItemID] = append(allItems[item.ItemID], idStr)
}
}
for itemID, portfolioIDs := range allItems {
if len(portfolioIDs) > 1 {
comparison.CommonItems = append(comparison.CommonItems, itemID)
} else {
pid := portfolioIDs[0]
comparison.UniqueItems[pid] = append(comparison.UniqueItems[pid], itemID)
}
}
c.JSON(http.StatusOK, portfolio.ComparePortfoliosResponse{
Portfolios: portfolios,
Comparison: comparison,
})
}
// RecalculateMetrics manually recalculates portfolio metrics
// POST /sdk/v1/portfolios/:id/recalculate
func (h *PortfolioHandlers) RecalculateMetrics(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
if err := h.store.RecalculateMetrics(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
p, _ := h.store.GetPortfolio(c.Request.Context(), id)
c.JSON(http.StatusOK, gin.H{
"message": "metrics recalculated",
"portfolio": p,
})
}
// ============================================================================
// Approval Workflow
// ============================================================================
// ApprovePortfolio approves a portfolio
// POST /sdk/v1/portfolios/:id/approve
func (h *PortfolioHandlers) ApprovePortfolio(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
p, err := h.store.GetPortfolio(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if p == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "portfolio not found"})
return
}
if p.Status != portfolio.PortfolioStatusReview {
c.JSON(http.StatusBadRequest, gin.H{"error": "portfolio must be in REVIEW status to approve"})
return
}
userID := rbac.GetUserID(c)
now := c.Request.Context().Value("now")
if now == nil {
t := p.UpdatedAt
p.ApprovedAt = &t
}
p.ApprovedBy = &userID
p.Status = portfolio.PortfolioStatusApproved
if err := h.store.UpdatePortfolio(c.Request.Context(), p); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "portfolio approved",
"portfolio": p,
})
}
// SubmitForReview submits a portfolio for review
// POST /sdk/v1/portfolios/:id/submit-review
func (h *PortfolioHandlers) SubmitForReview(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
p, err := h.store.GetPortfolio(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if p == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "portfolio not found"})
return
}
if p.Status != portfolio.PortfolioStatusDraft && p.Status != portfolio.PortfolioStatusActive {
c.JSON(http.StatusBadRequest, gin.H{"error": "portfolio must be in DRAFT or ACTIVE status to submit for review"})
return
}
p.Status = portfolio.PortfolioStatusReview
if err := h.store.UpdatePortfolio(c.Request.Context(), p); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "portfolio submitted for review",
"portfolio": p,
})
}