-- ============================================================================ -- Migration 007: Portfolio Schema -- AI Use Case Portfolio Management with Merge Support -- ============================================================================ -- Portfolios table CREATE TABLE IF NOT EXISTS portfolios ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE, namespace_id UUID REFERENCES compliance_namespaces(id) ON DELETE SET NULL, -- Info name VARCHAR(255) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'DRAFT', -- DRAFT, ACTIVE, REVIEW, APPROVED, ARCHIVED -- Organization department VARCHAR(255), business_unit VARCHAR(255), owner VARCHAR(255), owner_email VARCHAR(255), -- Aggregated metrics (computed) total_assessments INT DEFAULT 0, total_roadmaps INT DEFAULT 0, total_workshops INT DEFAULT 0, avg_risk_score DECIMAL(5,2) DEFAULT 0, high_risk_count INT DEFAULT 0, conditional_count INT DEFAULT 0, approved_count INT DEFAULT 0, compliance_score DECIMAL(5,2) DEFAULT 0, -- 0-100 -- Settings (JSONB) settings JSONB DEFAULT '{}', -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL, approved_at TIMESTAMPTZ, approved_by UUID ); -- Portfolio items table (links portfolios to assessments, roadmaps, workshops) CREATE TABLE IF NOT EXISTS portfolio_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE, item_type VARCHAR(50) NOT NULL, -- ASSESSMENT, ROADMAP, WORKSHOP, DOCUMENT item_id UUID NOT NULL, -- Cached info from the linked item title VARCHAR(500), status VARCHAR(50), risk_level VARCHAR(20), risk_score INT DEFAULT 0, feasibility VARCHAR(20), -- Ordering and categorization sort_order INT DEFAULT 0, tags JSONB DEFAULT '[]', notes TEXT, -- Audit added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), added_by UUID NOT NULL, -- Unique constraint: item can only be in portfolio once UNIQUE(portfolio_id, item_id) ); -- Portfolio activity log table CREATE TABLE IF NOT EXISTS portfolio_activity ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), portfolio_id UUID NOT NULL REFERENCES portfolios(id) ON DELETE CASCADE, -- Activity info timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), action VARCHAR(50) NOT NULL, -- added, removed, updated, merged, approved, submitted item_type VARCHAR(50), item_id UUID, item_title VARCHAR(500), user_id UUID NOT NULL, -- Additional details details JSONB ); -- ============================================================================ -- Indexes -- ============================================================================ -- Portfolio indexes CREATE INDEX IF NOT EXISTS idx_portfolios_tenant ON portfolios(tenant_id); CREATE INDEX IF NOT EXISTS idx_portfolios_tenant_status ON portfolios(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_portfolios_department ON portfolios(tenant_id, department) WHERE department IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_portfolios_business_unit ON portfolios(tenant_id, business_unit) WHERE business_unit IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_portfolios_owner ON portfolios(owner) WHERE owner IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_portfolios_created_by ON portfolios(created_by); CREATE INDEX IF NOT EXISTS idx_portfolios_risk_score ON portfolios(avg_risk_score); -- Portfolio item indexes CREATE INDEX IF NOT EXISTS idx_portfolio_items_portfolio ON portfolio_items(portfolio_id); CREATE INDEX IF NOT EXISTS idx_portfolio_items_type ON portfolio_items(portfolio_id, item_type); CREATE INDEX IF NOT EXISTS idx_portfolio_items_item ON portfolio_items(item_id); CREATE INDEX IF NOT EXISTS idx_portfolio_items_risk ON portfolio_items(portfolio_id, risk_level); CREATE INDEX IF NOT EXISTS idx_portfolio_items_feasibility ON portfolio_items(portfolio_id, feasibility); CREATE INDEX IF NOT EXISTS idx_portfolio_items_sort ON portfolio_items(portfolio_id, sort_order); -- Activity indexes CREATE INDEX IF NOT EXISTS idx_portfolio_activity_portfolio ON portfolio_activity(portfolio_id); CREATE INDEX IF NOT EXISTS idx_portfolio_activity_timestamp ON portfolio_activity(portfolio_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_portfolio_activity_user ON portfolio_activity(user_id); -- ============================================================================ -- Triggers -- ============================================================================ -- Reuse existing update_updated_at_column function -- Portfolios trigger DROP TRIGGER IF EXISTS update_portfolios_updated_at ON portfolios; CREATE TRIGGER update_portfolios_updated_at BEFORE UPDATE ON portfolios FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Functions for Metrics Calculation -- ============================================================================ -- Function to recalculate portfolio metrics CREATE OR REPLACE FUNCTION recalculate_portfolio_metrics(p_portfolio_id UUID) RETURNS VOID AS $$ DECLARE v_total_assessments INT; v_total_roadmaps INT; v_total_workshops INT; v_avg_risk DECIMAL(5,2); v_high_risk INT; v_conditional INT; v_approved INT; v_compliance DECIMAL(5,2); BEGIN -- Count by type SELECT COUNT(*) INTO v_total_assessments FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT'; SELECT COUNT(*) INTO v_total_roadmaps FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ROADMAP'; SELECT COUNT(*) INTO v_total_workshops FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'WORKSHOP'; -- Calculate risk metrics SELECT COALESCE(AVG(risk_score), 0) INTO v_avg_risk FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT'; SELECT COUNT(*) INTO v_high_risk FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT' AND risk_level IN ('HIGH', 'UNACCEPTABLE'); SELECT COUNT(*) INTO v_conditional FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT' AND feasibility = 'CONDITIONAL'; SELECT COUNT(*) INTO v_approved FROM portfolio_items WHERE portfolio_id = p_portfolio_id AND item_type = 'ASSESSMENT' AND feasibility = 'YES'; -- Calculate compliance score IF v_total_assessments > 0 THEN v_compliance := (v_approved::DECIMAL / v_total_assessments) * 100; ELSE v_compliance := 0; END IF; -- Update portfolio UPDATE portfolios SET total_assessments = v_total_assessments, total_roadmaps = v_total_roadmaps, total_workshops = v_total_workshops, avg_risk_score = v_avg_risk, high_risk_count = v_high_risk, conditional_count = v_conditional, approved_count = v_approved, compliance_score = v_compliance, updated_at = NOW() WHERE id = p_portfolio_id; END; $$ LANGUAGE plpgsql; -- Trigger function to auto-update metrics on item changes CREATE OR REPLACE FUNCTION portfolio_items_metrics_trigger() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'DELETE' THEN PERFORM recalculate_portfolio_metrics(OLD.portfolio_id); RETURN OLD; ELSE PERFORM recalculate_portfolio_metrics(NEW.portfolio_id); RETURN NEW; END IF; END; $$ LANGUAGE plpgsql; -- Trigger for auto metrics update DROP TRIGGER IF EXISTS trg_portfolio_items_metrics ON portfolio_items; CREATE TRIGGER trg_portfolio_items_metrics AFTER INSERT OR UPDATE OR DELETE ON portfolio_items FOR EACH ROW EXECUTE FUNCTION portfolio_items_metrics_trigger(); -- ============================================================================ -- Views -- ============================================================================ -- View for portfolio summary with counts CREATE OR REPLACE VIEW portfolio_summary_view AS SELECT p.id, p.tenant_id, p.name, p.description, p.status, p.department, p.business_unit, p.owner, p.total_assessments, p.total_roadmaps, p.total_workshops, p.avg_risk_score, p.high_risk_count, p.conditional_count, p.approved_count, p.compliance_score, p.created_at, p.updated_at, (p.total_assessments + p.total_roadmaps + p.total_workshops) as total_items, CASE WHEN p.high_risk_count > 0 THEN 'CRITICAL' WHEN p.conditional_count > p.approved_count THEN 'WARNING' ELSE 'GOOD' END as health_status FROM portfolios p; -- ============================================================================ -- Comments -- ============================================================================ COMMENT ON TABLE portfolios IS 'AI use case portfolios for grouping and managing multiple assessments'; COMMENT ON TABLE portfolio_items IS 'Items linked to portfolios (assessments, roadmaps, workshops)'; COMMENT ON TABLE portfolio_activity IS 'Activity log for portfolio changes'; COMMENT ON COLUMN portfolios.compliance_score IS 'Percentage of assessments with YES feasibility (0-100)'; COMMENT ON COLUMN portfolios.avg_risk_score IS 'Average risk score across all assessments in portfolio'; COMMENT ON COLUMN portfolio_items.item_type IS 'Type of linked item: ASSESSMENT, ROADMAP, WORKSHOP, DOCUMENT'; COMMENT ON COLUMN portfolio_items.sort_order IS 'Custom ordering within the portfolio'; COMMENT ON FUNCTION recalculate_portfolio_metrics(UUID) IS 'Recalculates aggregated metrics for a portfolio';