package iace import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // ============================================================================ // Evidence Operations // ============================================================================ // CreateEvidence creates a new evidence record func (s *Store) CreateEvidence(ctx context.Context, evidence *Evidence) error { if evidence.ID == uuid.Nil { evidence.ID = uuid.New() } if evidence.CreatedAt.IsZero() { evidence.CreatedAt = time.Now().UTC() } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_evidence ( id, project_id, mitigation_id, verification_plan_id, file_name, file_path, file_hash, file_size, mime_type, description, uploaded_by, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) `, evidence.ID, evidence.ProjectID, evidence.MitigationID, evidence.VerificationPlanID, evidence.FileName, evidence.FilePath, evidence.FileHash, evidence.FileSize, evidence.MimeType, evidence.Description, evidence.UploadedBy, evidence.CreatedAt, ) if err != nil { return fmt.Errorf("create evidence: %w", err) } return nil } // ListEvidence lists all evidence for a project func (s *Store) ListEvidence(ctx context.Context, projectID uuid.UUID) ([]Evidence, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, mitigation_id, verification_plan_id, file_name, file_path, file_hash, file_size, mime_type, description, uploaded_by, created_at FROM iace_evidence WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list evidence: %w", err) } defer rows.Close() var evidence []Evidence for rows.Next() { var e Evidence err := rows.Scan( &e.ID, &e.ProjectID, &e.MitigationID, &e.VerificationPlanID, &e.FileName, &e.FilePath, &e.FileHash, &e.FileSize, &e.MimeType, &e.Description, &e.UploadedBy, &e.CreatedAt, ) if err != nil { return nil, fmt.Errorf("list evidence scan: %w", err) } evidence = append(evidence, e) } return evidence, nil } // ============================================================================ // Verification Plan Operations // ============================================================================ // CreateVerificationPlan creates a new verification plan func (s *Store) CreateVerificationPlan(ctx context.Context, req CreateVerificationPlanRequest) (*VerificationPlan, error) { vp := &VerificationPlan{ ID: uuid.New(), ProjectID: req.ProjectID, HazardID: req.HazardID, MitigationID: req.MitigationID, Title: req.Title, Description: req.Description, AcceptanceCriteria: req.AcceptanceCriteria, Method: req.Method, Status: "planned", CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_verification_plans ( id, project_id, hazard_id, mitigation_id, title, description, acceptance_criteria, method, status, result, completed_at, completed_by, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 ) `, vp.ID, vp.ProjectID, vp.HazardID, vp.MitigationID, vp.Title, vp.Description, vp.AcceptanceCriteria, string(vp.Method), vp.Status, "", nil, uuid.Nil, vp.CreatedAt, vp.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create verification plan: %w", err) } return vp, nil } // UpdateVerificationPlan updates a verification plan with a dynamic set of fields func (s *Store) UpdateVerificationPlan(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*VerificationPlan, error) { if len(updates) == 0 { return s.getVerificationPlan(ctx, id) } query := "UPDATE iace_verification_plans SET updated_at = NOW()" args := []interface{}{id} argIdx := 2 for key, val := range updates { switch key { case "title", "description", "acceptance_criteria", "result", "status": query += fmt.Sprintf(", %s = $%d", key, argIdx) args = append(args, val) argIdx++ case "method": query += fmt.Sprintf(", method = $%d", argIdx) args = append(args, val) argIdx++ } } query += " WHERE id = $1" _, err := s.pool.Exec(ctx, query, args...) if err != nil { return nil, fmt.Errorf("update verification plan: %w", err) } return s.getVerificationPlan(ctx, id) } // CompleteVerification marks a verification plan as completed func (s *Store) CompleteVerification(ctx context.Context, id uuid.UUID, result string, completedBy string) error { now := time.Now().UTC() completedByUUID, err := uuid.Parse(completedBy) if err != nil { return fmt.Errorf("invalid completed_by UUID: %w", err) } _, err = s.pool.Exec(ctx, ` UPDATE iace_verification_plans SET status = 'completed', result = $2, completed_at = $3, completed_by = $4, updated_at = $3 WHERE id = $1 `, id, result, now, completedByUUID) if err != nil { return fmt.Errorf("complete verification: %w", err) } return nil } // ListVerificationPlans lists all verification plans for a project func (s *Store) ListVerificationPlans(ctx context.Context, projectID uuid.UUID) ([]VerificationPlan, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, hazard_id, mitigation_id, title, description, acceptance_criteria, method, status, result, completed_at, completed_by, created_at, updated_at FROM iace_verification_plans WHERE project_id = $1 ORDER BY created_at ASC `, projectID) if err != nil { return nil, fmt.Errorf("list verification plans: %w", err) } defer rows.Close() var plans []VerificationPlan for rows.Next() { var vp VerificationPlan var method string err := rows.Scan( &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID, &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method, &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy, &vp.CreatedAt, &vp.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list verification plans scan: %w", err) } vp.Method = VerificationMethod(method) plans = append(plans, vp) } return plans, nil } // getVerificationPlan is a helper to fetch a single verification plan by ID func (s *Store) getVerificationPlan(ctx context.Context, id uuid.UUID) (*VerificationPlan, error) { var vp VerificationPlan var method string err := s.pool.QueryRow(ctx, ` SELECT id, project_id, hazard_id, mitigation_id, title, description, acceptance_criteria, method, status, result, completed_at, completed_by, created_at, updated_at FROM iace_verification_plans WHERE id = $1 `, id).Scan( &vp.ID, &vp.ProjectID, &vp.HazardID, &vp.MitigationID, &vp.Title, &vp.Description, &vp.AcceptanceCriteria, &method, &vp.Status, &vp.Result, &vp.CompletedAt, &vp.CompletedBy, &vp.CreatedAt, &vp.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get verification plan: %w", err) } vp.Method = VerificationMethod(method) return &vp, nil } // ============================================================================ // Reference Data Operations // ============================================================================ // ListLifecyclePhases returns all 12 lifecycle phases with DE/EN labels func (s *Store) ListLifecyclePhases(ctx context.Context) ([]LifecyclePhaseInfo, error) { rows, err := s.pool.Query(ctx, ` SELECT id, label_de, label_en, sort_order FROM iace_lifecycle_phases ORDER BY sort_order ASC `) if err != nil { return nil, fmt.Errorf("list lifecycle phases: %w", err) } defer rows.Close() var phases []LifecyclePhaseInfo for rows.Next() { var p LifecyclePhaseInfo if err := rows.Scan(&p.ID, &p.LabelDE, &p.LabelEN, &p.Sort); err != nil { return nil, fmt.Errorf("list lifecycle phases scan: %w", err) } phases = append(phases, p) } return phases, nil } // ListRoles returns all affected person roles from the reference table func (s *Store) ListRoles(ctx context.Context) ([]RoleInfo, error) { rows, err := s.pool.Query(ctx, ` SELECT id, label_de, label_en, sort_order FROM iace_roles ORDER BY sort_order ASC `) if err != nil { return nil, fmt.Errorf("list roles: %w", err) } defer rows.Close() var roles []RoleInfo for rows.Next() { var r RoleInfo if err := rows.Scan(&r.ID, &r.LabelDE, &r.LabelEN, &r.Sort); err != nil { return nil, fmt.Errorf("list roles scan: %w", err) } roles = append(roles, r) } return roles, nil } // ListEvidenceTypes returns all evidence types from the reference table func (s *Store) ListEvidenceTypes(ctx context.Context) ([]EvidenceTypeInfo, error) { rows, err := s.pool.Query(ctx, ` SELECT id, category, label_de, label_en, sort_order FROM iace_evidence_types ORDER BY sort_order ASC `) if err != nil { return nil, fmt.Errorf("list evidence types: %w", err) } defer rows.Close() var types []EvidenceTypeInfo for rows.Next() { var e EvidenceTypeInfo if err := rows.Scan(&e.ID, &e.Category, &e.LabelDE, &e.LabelEN, &e.Sort); err != nil { return nil, fmt.Errorf("list evidence types scan: %w", err) } types = append(types, e) } return types, nil }