package portfolio import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Store handles portfolio data persistence type Store struct { pool *pgxpool.Pool } // NewStore creates a new portfolio store func NewStore(pool *pgxpool.Pool) *Store { return &Store{pool: pool} } // ============================================================================ // Portfolio CRUD Operations // ============================================================================ // CreatePortfolio creates a new portfolio func (s *Store) CreatePortfolio(ctx context.Context, p *Portfolio) error { p.ID = uuid.New() p.CreatedAt = time.Now().UTC() p.UpdatedAt = p.CreatedAt if p.Status == "" { p.Status = PortfolioStatusDraft } settings, _ := json.Marshal(p.Settings) _, err := s.pool.Exec(ctx, ` INSERT INTO portfolios ( id, tenant_id, namespace_id, name, description, status, department, business_unit, owner, owner_email, total_assessments, total_roadmaps, total_workshops, avg_risk_score, high_risk_count, conditional_count, approved_count, compliance_score, settings, created_at, updated_at, created_by, approved_at, approved_by ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24 ) `, p.ID, p.TenantID, p.NamespaceID, p.Name, p.Description, string(p.Status), p.Department, p.BusinessUnit, p.Owner, p.OwnerEmail, p.TotalAssessments, p.TotalRoadmaps, p.TotalWorkshops, p.AvgRiskScore, p.HighRiskCount, p.ConditionalCount, p.ApprovedCount, p.ComplianceScore, settings, p.CreatedAt, p.UpdatedAt, p.CreatedBy, p.ApprovedAt, p.ApprovedBy, ) return err } // GetPortfolio retrieves a portfolio by ID func (s *Store) GetPortfolio(ctx context.Context, id uuid.UUID) (*Portfolio, error) { var p Portfolio var status string var settings []byte err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, namespace_id, name, description, status, department, business_unit, owner, owner_email, total_assessments, total_roadmaps, total_workshops, avg_risk_score, high_risk_count, conditional_count, approved_count, compliance_score, settings, created_at, updated_at, created_by, approved_at, approved_by FROM portfolios WHERE id = $1 `, id).Scan( &p.ID, &p.TenantID, &p.NamespaceID, &p.Name, &p.Description, &status, &p.Department, &p.BusinessUnit, &p.Owner, &p.OwnerEmail, &p.TotalAssessments, &p.TotalRoadmaps, &p.TotalWorkshops, &p.AvgRiskScore, &p.HighRiskCount, &p.ConditionalCount, &p.ApprovedCount, &p.ComplianceScore, &settings, &p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.ApprovedAt, &p.ApprovedBy, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } p.Status = PortfolioStatus(status) json.Unmarshal(settings, &p.Settings) return &p, nil } // ListPortfolios lists portfolios for a tenant with optional filters func (s *Store) ListPortfolios(ctx context.Context, tenantID uuid.UUID, filters *PortfolioFilters) ([]Portfolio, error) { query := ` SELECT id, tenant_id, namespace_id, name, description, status, department, business_unit, owner, owner_email, total_assessments, total_roadmaps, total_workshops, avg_risk_score, high_risk_count, conditional_count, approved_count, compliance_score, settings, created_at, updated_at, created_by, approved_at, approved_by FROM portfolios WHERE tenant_id = $1` args := []interface{}{tenantID} argIdx := 2 if filters != nil { if filters.Status != "" { query += fmt.Sprintf(" AND status = $%d", argIdx) args = append(args, string(filters.Status)) argIdx++ } if filters.Department != "" { query += fmt.Sprintf(" AND department = $%d", argIdx) args = append(args, filters.Department) argIdx++ } if filters.BusinessUnit != "" { query += fmt.Sprintf(" AND business_unit = $%d", argIdx) args = append(args, filters.BusinessUnit) argIdx++ } if filters.Owner != "" { query += fmt.Sprintf(" AND owner ILIKE $%d", argIdx) args = append(args, "%"+filters.Owner+"%") argIdx++ } if filters.MinRiskScore != nil { query += fmt.Sprintf(" AND avg_risk_score >= $%d", argIdx) args = append(args, *filters.MinRiskScore) argIdx++ } if filters.MaxRiskScore != nil { query += fmt.Sprintf(" AND avg_risk_score <= $%d", argIdx) args = append(args, *filters.MaxRiskScore) argIdx++ } } query += " ORDER BY updated_at DESC" if filters != nil && filters.Limit > 0 { query += fmt.Sprintf(" LIMIT $%d", argIdx) args = append(args, filters.Limit) argIdx++ if filters.Offset > 0 { query += fmt.Sprintf(" OFFSET $%d", argIdx) args = append(args, filters.Offset) } } rows, err := s.pool.Query(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() var portfolios []Portfolio for rows.Next() { var p Portfolio var status string var settings []byte err := rows.Scan( &p.ID, &p.TenantID, &p.NamespaceID, &p.Name, &p.Description, &status, &p.Department, &p.BusinessUnit, &p.Owner, &p.OwnerEmail, &p.TotalAssessments, &p.TotalRoadmaps, &p.TotalWorkshops, &p.AvgRiskScore, &p.HighRiskCount, &p.ConditionalCount, &p.ApprovedCount, &p.ComplianceScore, &settings, &p.CreatedAt, &p.UpdatedAt, &p.CreatedBy, &p.ApprovedAt, &p.ApprovedBy, ) if err != nil { return nil, err } p.Status = PortfolioStatus(status) json.Unmarshal(settings, &p.Settings) portfolios = append(portfolios, p) } return portfolios, nil } // UpdatePortfolio updates a portfolio func (s *Store) UpdatePortfolio(ctx context.Context, p *Portfolio) error { p.UpdatedAt = time.Now().UTC() settings, _ := json.Marshal(p.Settings) _, err := s.pool.Exec(ctx, ` UPDATE portfolios SET name = $2, description = $3, status = $4, department = $5, business_unit = $6, owner = $7, owner_email = $8, settings = $9, updated_at = $10, approved_at = $11, approved_by = $12 WHERE id = $1 `, p.ID, p.Name, p.Description, string(p.Status), p.Department, p.BusinessUnit, p.Owner, p.OwnerEmail, settings, p.UpdatedAt, p.ApprovedAt, p.ApprovedBy, ) return err } // DeletePortfolio deletes a portfolio and its items func (s *Store) DeletePortfolio(ctx context.Context, id uuid.UUID) error { // Delete items first _, err := s.pool.Exec(ctx, "DELETE FROM portfolio_items WHERE portfolio_id = $1", id) if err != nil { return err } // Delete portfolio _, err = s.pool.Exec(ctx, "DELETE FROM portfolios WHERE id = $1", id) return err }