Files
breakpilot-compliance/ai-compliance-sdk/internal/whistleblower/store_messages.go
Sharang Parnerkar 13f57c4519 refactor(go): split obligations, portfolio, rbac, whistleblower handlers and stores, roadmap parser
Split 7 files exceeding the 500 LOC hard cap into 16 files, all under 500 LOC.
No exported symbols renamed; zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:00:15 +02:00

230 lines
6.1 KiB
Go

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
}