Files
breakpilot-compliance/ai-compliance-sdk/internal/portfolio/store_metrics.go
Sharang Parnerkar 3fb5b94905 refactor(go): split portfolio, workshop, training/models, roadmap stores
portfolio/store.go (818 LOC) → store_portfolio.go, store_items.go, store_metrics.go
workshop/store.go (793 LOC) → store_sessions.go, store_participants.go, store_responses.go
training/models.go (757 LOC) → models_enums.go, models_core.go, models_api.go, models_blocks.go
roadmap/store.go (757 LOC) → store_roadmap.go, store_items.go, store_import.go

All files under 350 LOC. Zero behavior changes, same package declarations.
go vet passes on all five packages.

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

312 lines
8.9 KiB
Go

package portfolio
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
)
// ============================================================================
// Metrics and Statistics
// ============================================================================
// RecalculateMetrics recalculates aggregated metrics for a portfolio
func (s *Store) RecalculateMetrics(ctx context.Context, portfolioID uuid.UUID) error {
// Count by type
var totalAssessments, totalRoadmaps, totalWorkshops int
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT'",
portfolioID).Scan(&totalAssessments)
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'ROADMAP'",
portfolioID).Scan(&totalRoadmaps)
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1 AND item_type = 'WORKSHOP'",
portfolioID).Scan(&totalWorkshops)
// Calculate risk metrics from assessments
var avgRiskScore float64
var highRiskCount, conditionalCount, approvedCount int
s.pool.QueryRow(ctx, `
SELECT COALESCE(AVG(risk_score), 0)
FROM portfolio_items
WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT'
`, portfolioID).Scan(&avgRiskScore)
s.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM portfolio_items
WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND risk_level IN ('HIGH', 'UNACCEPTABLE')
`, portfolioID).Scan(&highRiskCount)
s.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM portfolio_items
WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND feasibility = 'CONDITIONAL'
`, portfolioID).Scan(&conditionalCount)
s.pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM portfolio_items
WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT' AND feasibility = 'YES'
`, portfolioID).Scan(&approvedCount)
// Calculate compliance score (simplified: % of approved items)
var complianceScore float64
if totalAssessments > 0 {
complianceScore = (float64(approvedCount) / float64(totalAssessments)) * 100
}
// Update portfolio
_, err := s.pool.Exec(ctx, `
UPDATE portfolios SET
total_assessments = $2,
total_roadmaps = $3,
total_workshops = $4,
avg_risk_score = $5,
high_risk_count = $6,
conditional_count = $7,
approved_count = $8,
compliance_score = $9,
updated_at = NOW()
WHERE id = $1
`,
portfolioID,
totalAssessments, totalRoadmaps, totalWorkshops,
avgRiskScore, highRiskCount, conditionalCount, approvedCount,
complianceScore,
)
return err
}
// GetPortfolioStats returns detailed statistics for a portfolio
func (s *Store) GetPortfolioStats(ctx context.Context, portfolioID uuid.UUID) (*PortfolioStats, error) {
stats := &PortfolioStats{
ItemsByType: make(map[ItemType]int),
}
// Total items
s.pool.QueryRow(ctx,
"SELECT COUNT(*) FROM portfolio_items WHERE portfolio_id = $1",
portfolioID).Scan(&stats.TotalItems)
// Items by type
rows, _ := s.pool.Query(ctx, `
SELECT item_type, COUNT(*)
FROM portfolio_items
WHERE portfolio_id = $1
GROUP BY item_type
`, portfolioID)
if rows != nil {
defer rows.Close()
for rows.Next() {
var itemType string
var count int
rows.Scan(&itemType, &count)
stats.ItemsByType[ItemType(itemType)] = count
}
}
// Risk distribution
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND risk_level = 'MINIMAL'
`, portfolioID).Scan(&stats.RiskDistribution.Minimal)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND risk_level = 'LOW'
`, portfolioID).Scan(&stats.RiskDistribution.Low)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND risk_level = 'MEDIUM'
`, portfolioID).Scan(&stats.RiskDistribution.Medium)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND risk_level = 'HIGH'
`, portfolioID).Scan(&stats.RiskDistribution.High)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND risk_level = 'UNACCEPTABLE'
`, portfolioID).Scan(&stats.RiskDistribution.Unacceptable)
// Feasibility distribution
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND feasibility = 'YES'
`, portfolioID).Scan(&stats.FeasibilityDist.Yes)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND feasibility = 'CONDITIONAL'
`, portfolioID).Scan(&stats.FeasibilityDist.Conditional)
s.pool.QueryRow(ctx, `
SELECT COUNT(*) FROM portfolio_items
WHERE portfolio_id = $1 AND feasibility = 'NO'
`, portfolioID).Scan(&stats.FeasibilityDist.No)
// Average risk score
s.pool.QueryRow(ctx, `
SELECT COALESCE(AVG(risk_score), 0)
FROM portfolio_items
WHERE portfolio_id = $1 AND item_type = 'ASSESSMENT'
`, portfolioID).Scan(&stats.AvgRiskScore)
// Compliance score
s.pool.QueryRow(ctx,
"SELECT compliance_score FROM portfolios WHERE id = $1",
portfolioID).Scan(&stats.ComplianceScore)
// DSFA required count — estimate from high risk items
stats.DSFARequired = stats.RiskDistribution.High + stats.RiskDistribution.Unacceptable
// Controls required (items with CONDITIONAL feasibility)
stats.ControlsRequired = stats.FeasibilityDist.Conditional
stats.LastUpdated = time.Now().UTC()
return stats, nil
}
// GetPortfolioSummary returns a complete portfolio summary
func (s *Store) GetPortfolioSummary(ctx context.Context, portfolioID uuid.UUID) (*PortfolioSummary, error) {
portfolio, err := s.GetPortfolio(ctx, portfolioID)
if err != nil || portfolio == nil {
return nil, err
}
items, err := s.ListItems(ctx, portfolioID, nil)
if err != nil {
return nil, err
}
stats, err := s.GetPortfolioStats(ctx, portfolioID)
if err != nil {
return nil, err
}
return &PortfolioSummary{
Portfolio: portfolio,
Items: items,
RiskDistribution: stats.RiskDistribution,
FeasibilityDist: stats.FeasibilityDist,
}, nil
}
// ============================================================================
// Bulk Operations
// ============================================================================
// BulkAddItems adds multiple items to a portfolio
func (s *Store) BulkAddItems(ctx context.Context, portfolioID uuid.UUID, items []PortfolioItem, userID uuid.UUID) (*BulkAddItemsResponse, error) {
result := &BulkAddItemsResponse{
Errors: []string{},
}
for _, item := range items {
item.PortfolioID = portfolioID
item.AddedBy = userID
// Fetch item info from source table if not provided
if item.Title == "" {
s.populateItemInfo(ctx, &item)
}
if err := s.AddItem(ctx, &item); err != nil {
result.Skipped++
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", item.ItemID, err))
} else {
result.Added++
}
}
return result, nil
}
// populateItemInfo fetches item metadata from the source table
func (s *Store) populateItemInfo(ctx context.Context, item *PortfolioItem) {
switch item.ItemType {
case ItemTypeAssessment:
s.pool.QueryRow(ctx, `
SELECT title, feasibility, risk_level, risk_score, status
FROM ucca_assessments WHERE id = $1
`, item.ItemID).Scan(&item.Title, &item.Feasibility, &item.RiskLevel, &item.RiskScore, &item.Status)
case ItemTypeRoadmap:
s.pool.QueryRow(ctx, `
SELECT name, status
FROM roadmaps WHERE id = $1
`, item.ItemID).Scan(&item.Title, &item.Status)
case ItemTypeWorkshop:
s.pool.QueryRow(ctx, `
SELECT title, status
FROM workshop_sessions WHERE id = $1
`, item.ItemID).Scan(&item.Title, &item.Status)
}
}
// ============================================================================
// Activity Tracking
// ============================================================================
// LogActivity logs an activity entry for a portfolio
func (s *Store) LogActivity(ctx context.Context, portfolioID uuid.UUID, entry *ActivityEntry) error {
_, err := s.pool.Exec(ctx, `
INSERT INTO portfolio_activity (
id, portfolio_id, timestamp, action,
item_type, item_id, item_title, user_id
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
`,
uuid.New(), portfolioID, entry.Timestamp, entry.Action,
string(entry.ItemType), entry.ItemID, entry.ItemTitle, entry.UserID,
)
return err
}
// GetRecentActivity retrieves recent activity for a portfolio
func (s *Store) GetRecentActivity(ctx context.Context, portfolioID uuid.UUID, limit int) ([]ActivityEntry, error) {
if limit <= 0 {
limit = 20
}
rows, err := s.pool.Query(ctx, `
SELECT timestamp, action, item_type, item_id, item_title, user_id
FROM portfolio_activity
WHERE portfolio_id = $1
ORDER BY timestamp DESC
LIMIT $2
`, portfolioID, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var activities []ActivityEntry
for rows.Next() {
var entry ActivityEntry
var itemType string
err := rows.Scan(
&entry.Timestamp, &entry.Action, &itemType,
&entry.ItemID, &entry.ItemTitle, &entry.UserID,
)
if err != nil {
return nil, err
}
entry.ItemType = ItemType(itemType)
activities = append(activities, entry)
}
return activities, nil
}