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

212 lines
5.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"
)
// PortfolioHandlers handles portfolio HTTP requests
type PortfolioHandlers struct {
store *portfolio.Store
}
// NewPortfolioHandlers creates new portfolio handlers
func NewPortfolioHandlers(store *portfolio.Store) *PortfolioHandlers {
return &PortfolioHandlers{store: store}
}
// ============================================================================
// Portfolio CRUD
// ============================================================================
// CreatePortfolio creates a new portfolio
// POST /sdk/v1/portfolios
func (h *PortfolioHandlers) CreatePortfolio(c *gin.Context) {
var req portfolio.CreatePortfolioRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
p := &portfolio.Portfolio{
TenantID: tenantID,
Name: req.Name,
Description: req.Description,
Status: portfolio.PortfolioStatusDraft,
Department: req.Department,
BusinessUnit: req.BusinessUnit,
Owner: req.Owner,
OwnerEmail: req.OwnerEmail,
Settings: req.Settings,
CreatedBy: userID,
}
if !p.Settings.AutoUpdateMetrics {
p.Settings.AutoUpdateMetrics = true
}
if err := h.store.CreatePortfolio(c.Request.Context(), p); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"portfolio": p})
}
// ListPortfolios lists portfolios
// GET /sdk/v1/portfolios
func (h *PortfolioHandlers) ListPortfolios(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
filters := &portfolio.PortfolioFilters{
Limit: 50,
}
if status := c.Query("status"); status != "" {
filters.Status = portfolio.PortfolioStatus(status)
}
if department := c.Query("department"); department != "" {
filters.Department = department
}
if businessUnit := c.Query("business_unit"); businessUnit != "" {
filters.BusinessUnit = businessUnit
}
if owner := c.Query("owner"); owner != "" {
filters.Owner = owner
}
if limit := c.Query("limit"); limit != "" {
if l, err := strconv.Atoi(limit); err == nil {
filters.Limit = l
}
}
if offset := c.Query("offset"); offset != "" {
if o, err := strconv.Atoi(offset); err == nil {
filters.Offset = o
}
}
portfolios, err := h.store.ListPortfolios(c.Request.Context(), tenantID, filters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"portfolios": portfolios,
"total": len(portfolios),
})
}
// GetPortfolio retrieves a portfolio
// GET /sdk/v1/portfolios/:id
func (h *PortfolioHandlers) GetPortfolio(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid portfolio ID"})
return
}
summary, err := h.store.GetPortfolioSummary(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if summary == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "portfolio not found"})
return
}
stats, _ := h.store.GetPortfolioStats(c.Request.Context(), id)
c.JSON(http.StatusOK, gin.H{
"portfolio": summary.Portfolio,
"items": summary.Items,
"risk_distribution": summary.RiskDistribution,
"feasibility_dist": summary.FeasibilityDist,
"stats": stats,
})
}
// UpdatePortfolio updates a portfolio
// PUT /sdk/v1/portfolios/:id
func (h *PortfolioHandlers) UpdatePortfolio(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
}
var req portfolio.UpdatePortfolioRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if req.Name != "" {
p.Name = req.Name
}
if req.Description != "" {
p.Description = req.Description
}
if req.Status != "" {
p.Status = req.Status
}
if req.Department != "" {
p.Department = req.Department
}
if req.BusinessUnit != "" {
p.BusinessUnit = req.BusinessUnit
}
if req.Owner != "" {
p.Owner = req.Owner
}
if req.OwnerEmail != "" {
p.OwnerEmail = req.OwnerEmail
}
if req.Settings != nil {
p.Settings = *req.Settings
}
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{"portfolio": p})
}
// DeletePortfolio deletes a portfolio
// DELETE /sdk/v1/portfolios/:id
func (h *PortfolioHandlers) DeletePortfolio(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.DeletePortfolio(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "portfolio deleted"})
}