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>
231 lines
6.4 KiB
Go
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,
|
|
})
|
|
}
|