package iace import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // ============================================================================ // Tech File Section Operations // ============================================================================ // CreateTechFileSection creates a new section in the technical file func (s *Store) CreateTechFileSection(ctx context.Context, projectID uuid.UUID, sectionType, title, content string) (*TechFileSection, error) { tf := &TechFileSection{ ID: uuid.New(), ProjectID: projectID, SectionType: sectionType, Title: title, Content: content, Version: 1, Status: TechFileSectionStatusDraft, CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_tech_file_sections ( id, project_id, section_type, title, content, version, status, approved_by, approved_at, metadata, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) `, tf.ID, tf.ProjectID, tf.SectionType, tf.Title, tf.Content, tf.Version, string(tf.Status), uuid.Nil, nil, nil, tf.CreatedAt, tf.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create tech file section: %w", err) } return tf, nil } // UpdateTechFileSection updates the content of a tech file section and bumps version func (s *Store) UpdateTechFileSection(ctx context.Context, id uuid.UUID, content string) error { _, err := s.pool.Exec(ctx, ` UPDATE iace_tech_file_sections SET content = $2, version = version + 1, status = $3, updated_at = NOW() WHERE id = $1 `, id, content, string(TechFileSectionStatusDraft)) if err != nil { return fmt.Errorf("update tech file section: %w", err) } return nil } // ApproveTechFileSection marks a tech file section as approved func (s *Store) ApproveTechFileSection(ctx context.Context, id uuid.UUID, approvedBy string) error { now := time.Now().UTC() approvedByUUID, err := uuid.Parse(approvedBy) if err != nil { return fmt.Errorf("invalid approved_by UUID: %w", err) } _, err = s.pool.Exec(ctx, ` UPDATE iace_tech_file_sections SET status = $2, approved_by = $3, approved_at = $4, updated_at = $4 WHERE id = $1 `, id, string(TechFileSectionStatusApproved), approvedByUUID, now) if err != nil { return fmt.Errorf("approve tech file section: %w", err) } return nil } // ListTechFileSections lists all tech file sections for a project func (s *Store) ListTechFileSections(ctx context.Context, projectID uuid.UUID) ([]TechFileSection, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, section_type, title, content, version, status, approved_by, approved_at, metadata, created_at, updated_at FROM iace_tech_file_sections WHERE project_id = $1 ORDER BY section_type ASC, created_at ASC `, projectID) if err != nil { return nil, fmt.Errorf("list tech file sections: %w", err) } defer rows.Close() var sections []TechFileSection for rows.Next() { var tf TechFileSection var status string var metadata []byte err := rows.Scan( &tf.ID, &tf.ProjectID, &tf.SectionType, &tf.Title, &tf.Content, &tf.Version, &status, &tf.ApprovedBy, &tf.ApprovedAt, &metadata, &tf.CreatedAt, &tf.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list tech file sections scan: %w", err) } tf.Status = TechFileSectionStatus(status) json.Unmarshal(metadata, &tf.Metadata) sections = append(sections, tf) } return sections, nil } // ============================================================================ // Monitoring Event Operations // ============================================================================ // CreateMonitoringEvent creates a new post-market monitoring event func (s *Store) CreateMonitoringEvent(ctx context.Context, req CreateMonitoringEventRequest) (*MonitoringEvent, error) { me := &MonitoringEvent{ ID: uuid.New(), ProjectID: req.ProjectID, EventType: req.EventType, Title: req.Title, Description: req.Description, Severity: req.Severity, Status: "open", CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(), } _, err := s.pool.Exec(ctx, ` INSERT INTO iace_monitoring_events ( id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13 ) `, me.ID, me.ProjectID, string(me.EventType), me.Title, me.Description, me.Severity, "", me.Status, nil, uuid.Nil, nil, me.CreatedAt, me.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("create monitoring event: %w", err) } return me, nil } // ListMonitoringEvents lists all monitoring events for a project func (s *Store) ListMonitoringEvents(ctx context.Context, projectID uuid.UUID) ([]MonitoringEvent, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at FROM iace_monitoring_events WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list monitoring events: %w", err) } defer rows.Close() var events []MonitoringEvent for rows.Next() { var me MonitoringEvent var eventType string var metadata []byte err := rows.Scan( &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description, &me.Severity, &me.ImpactAssessment, &me.Status, &me.ResolvedAt, &me.ResolvedBy, &metadata, &me.CreatedAt, &me.UpdatedAt, ) if err != nil { return nil, fmt.Errorf("list monitoring events scan: %w", err) } me.EventType = MonitoringEventType(eventType) json.Unmarshal(metadata, &me.Metadata) events = append(events, me) } return events, nil } // UpdateMonitoringEvent updates a monitoring event with a dynamic set of fields func (s *Store) UpdateMonitoringEvent(ctx context.Context, id uuid.UUID, updates map[string]interface{}) (*MonitoringEvent, error) { if len(updates) == 0 { return s.getMonitoringEvent(ctx, id) } query := "UPDATE iace_monitoring_events SET updated_at = NOW()" args := []interface{}{id} argIdx := 2 for key, val := range updates { switch key { case "title", "description", "severity", "impact_assessment", "status": query += fmt.Sprintf(", %s = $%d", key, argIdx) args = append(args, val) argIdx++ case "event_type": query += fmt.Sprintf(", event_type = $%d", argIdx) args = append(args, val) argIdx++ case "resolved_at": query += fmt.Sprintf(", resolved_at = $%d", argIdx) args = append(args, val) argIdx++ case "resolved_by": query += fmt.Sprintf(", resolved_by = $%d", argIdx) args = append(args, val) argIdx++ case "metadata": metaJSON, _ := json.Marshal(val) query += fmt.Sprintf(", metadata = $%d", argIdx) args = append(args, metaJSON) argIdx++ } } query += " WHERE id = $1" _, err := s.pool.Exec(ctx, query, args...) if err != nil { return nil, fmt.Errorf("update monitoring event: %w", err) } return s.getMonitoringEvent(ctx, id) } // getMonitoringEvent is a helper to fetch a single monitoring event by ID func (s *Store) getMonitoringEvent(ctx context.Context, id uuid.UUID) (*MonitoringEvent, error) { var me MonitoringEvent var eventType string var metadata []byte err := s.pool.QueryRow(ctx, ` SELECT id, project_id, event_type, title, description, severity, impact_assessment, status, resolved_at, resolved_by, metadata, created_at, updated_at FROM iace_monitoring_events WHERE id = $1 `, id).Scan( &me.ID, &me.ProjectID, &eventType, &me.Title, &me.Description, &me.Severity, &me.ImpactAssessment, &me.Status, &me.ResolvedAt, &me.ResolvedBy, &metadata, &me.CreatedAt, &me.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, fmt.Errorf("get monitoring event: %w", err) } me.EventType = MonitoringEventType(eventType) json.Unmarshal(metadata, &me.Metadata) return &me, nil } // ============================================================================ // Audit Trail Operations // ============================================================================ // AddAuditEntry adds an immutable audit trail entry func (s *Store) AddAuditEntry(ctx context.Context, projectID uuid.UUID, entityType string, entityID uuid.UUID, action AuditAction, userID string, oldValues, newValues json.RawMessage) error { id := uuid.New() now := time.Now().UTC() userUUID, err := uuid.Parse(userID) if err != nil { return fmt.Errorf("invalid user_id UUID: %w", err) } // Compute a simple hash for integrity: sha256(entityType + entityID + action + timestamp) hashInput := fmt.Sprintf("%s:%s:%s:%s:%s", projectID, entityType, entityID, string(action), now.Format(time.RFC3339Nano)) // Use a simple deterministic hash representation hash := fmt.Sprintf("%x", hashInput) _, err = s.pool.Exec(ctx, ` INSERT INTO iace_audit_trail ( id, project_id, entity_type, entity_id, action, user_id, old_values, new_values, hash, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10 ) `, id, projectID, entityType, entityID, string(action), userUUID, oldValues, newValues, hash, now, ) if err != nil { return fmt.Errorf("add audit entry: %w", err) } return nil } // ListAuditTrail lists all audit trail entries for a project, newest first func (s *Store) ListAuditTrail(ctx context.Context, projectID uuid.UUID) ([]AuditTrailEntry, error) { rows, err := s.pool.Query(ctx, ` SELECT id, project_id, entity_type, entity_id, action, user_id, old_values, new_values, hash, created_at FROM iace_audit_trail WHERE project_id = $1 ORDER BY created_at DESC `, projectID) if err != nil { return nil, fmt.Errorf("list audit trail: %w", err) } defer rows.Close() var entries []AuditTrailEntry for rows.Next() { var e AuditTrailEntry var action string err := rows.Scan( &e.ID, &e.ProjectID, &e.EntityType, &e.EntityID, &action, &e.UserID, &e.OldValues, &e.NewValues, &e.Hash, &e.CreatedAt, ) if err != nil { return nil, fmt.Errorf("list audit trail scan: %w", err) } e.Action = AuditAction(action) entries = append(entries, e) } return entries, nil } // HasAuditEntryForType checks if an audit trail entry exists for the given entity type within a project. func (s *Store) HasAuditEntryForType(ctx context.Context, projectID uuid.UUID, entityType string) (bool, error) { var exists bool err := s.pool.QueryRow(ctx, ` SELECT EXISTS( SELECT 1 FROM iace_audit_trail WHERE project_id = $1 AND entity_type = $2 ) `, projectID, entityType).Scan(&exists) if err != nil { return false, fmt.Errorf("has audit entry: %w", err) } return exists, nil }