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