package whistleblower import ( "context" "time" "github.com/google/uuid" ) // ============================================================================ // Message Operations // ============================================================================ // AddMessage adds an anonymous message to a report func (s *Store) AddMessage(ctx context.Context, msg *AnonymousMessage) error { msg.ID = uuid.New() msg.SentAt = time.Now().UTC() _, err := s.pool.Exec(ctx, ` INSERT INTO whistleblower_messages ( id, report_id, direction, content, sent_at, read_at ) VALUES ( $1, $2, $3, $4, $5, $6 ) `, msg.ID, msg.ReportID, string(msg.Direction), msg.Content, msg.SentAt, msg.ReadAt, ) return err } // ListMessages lists messages for a report func (s *Store) ListMessages(ctx context.Context, reportID uuid.UUID) ([]AnonymousMessage, error) { rows, err := s.pool.Query(ctx, ` SELECT id, report_id, direction, content, sent_at, read_at FROM whistleblower_messages WHERE report_id = $1 ORDER BY sent_at ASC `, reportID) if err != nil { return nil, err } defer rows.Close() var messages []AnonymousMessage for rows.Next() { var msg AnonymousMessage var direction string err := rows.Scan( &msg.ID, &msg.ReportID, &direction, &msg.Content, &msg.SentAt, &msg.ReadAt, ) if err != nil { return nil, err } msg.Direction = MessageDirection(direction) messages = append(messages, msg) } return messages, nil } // ============================================================================ // Measure Operations // ============================================================================ // AddMeasure adds a corrective measure to a report func (s *Store) AddMeasure(ctx context.Context, measure *Measure) error { measure.ID = uuid.New() measure.CreatedAt = time.Now().UTC() if measure.Status == "" { measure.Status = MeasureStatusPlanned } _, err := s.pool.Exec(ctx, ` INSERT INTO whistleblower_measures ( id, report_id, title, description, status, responsible, due_date, completed_at, created_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) `, measure.ID, measure.ReportID, measure.Title, measure.Description, string(measure.Status), measure.Responsible, measure.DueDate, measure.CompletedAt, measure.CreatedAt, ) return err } // ListMeasures lists measures for a report func (s *Store) ListMeasures(ctx context.Context, reportID uuid.UUID) ([]Measure, error) { rows, err := s.pool.Query(ctx, ` SELECT id, report_id, title, description, status, responsible, due_date, completed_at, created_at FROM whistleblower_measures WHERE report_id = $1 ORDER BY created_at ASC `, reportID) if err != nil { return nil, err } defer rows.Close() var measures []Measure for rows.Next() { var m Measure var status string err := rows.Scan( &m.ID, &m.ReportID, &m.Title, &m.Description, &status, &m.Responsible, &m.DueDate, &m.CompletedAt, &m.CreatedAt, ) if err != nil { return nil, err } m.Status = MeasureStatus(status) measures = append(measures, m) } return measures, nil } // UpdateMeasure updates a measure func (s *Store) UpdateMeasure(ctx context.Context, measure *Measure) error { _, err := s.pool.Exec(ctx, ` UPDATE whistleblower_measures SET title = $2, description = $3, status = $4, responsible = $5, due_date = $6, completed_at = $7 WHERE id = $1 `, measure.ID, measure.Title, measure.Description, string(measure.Status), measure.Responsible, measure.DueDate, measure.CompletedAt, ) return err } // ============================================================================ // Statistics // ============================================================================ // GetStatistics returns aggregated whistleblower statistics for a tenant func (s *Store) GetStatistics(ctx context.Context, tenantID uuid.UUID) (*WhistleblowerStatistics, error) { stats := &WhistleblowerStatistics{ ByStatus: make(map[string]int), ByCategory: make(map[string]int), } s.pool.QueryRow(ctx, "SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1", tenantID).Scan(&stats.TotalReports) rows, err := s.pool.Query(ctx, "SELECT status, COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 GROUP BY status", tenantID) if err == nil { defer rows.Close() for rows.Next() { var status string var count int rows.Scan(&status, &count) stats.ByStatus[status] = count } } rows, err = s.pool.Query(ctx, "SELECT category, COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 GROUP BY category", tenantID) if err == nil { defer rows.Close() for rows.Next() { var category string var count int rows.Scan(&category, &count) stats.ByCategory[category] = count } } s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 AND acknowledged_at IS NULL AND status = 'new' AND deadline_acknowledgment < NOW() `, tenantID).Scan(&stats.OverdueAcknowledgments) s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1 AND closed_at IS NULL AND status NOT IN ('closed', 'rejected') AND deadline_feedback < NOW() `, tenantID).Scan(&stats.OverdueFeedbacks) s.pool.QueryRow(ctx, ` SELECT COALESCE(AVG(EXTRACT(EPOCH FROM (closed_at - received_at)) / 86400), 0) FROM whistleblower_reports WHERE tenant_id = $1 AND closed_at IS NOT NULL `, tenantID).Scan(&stats.AvgResolutionDays) return stats, nil } // ============================================================================ // Sequence Number // ============================================================================ // GetNextSequenceNumber gets and increments the sequence number for reference number generation func (s *Store) GetNextSequenceNumber(ctx context.Context, tenantID uuid.UUID, year int) (int, error) { var seq int err := s.pool.QueryRow(ctx, ` INSERT INTO whistleblower_sequences (tenant_id, year, last_sequence) VALUES ($1, $2, 1) ON CONFLICT (tenant_id, year) DO UPDATE SET last_sequence = whistleblower_sequences.last_sequence + 1 RETURNING last_sequence `, tenantID, year).Scan(&seq) if err != nil { return 0, err } return seq, nil }