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>
This commit is contained in:
@@ -32,17 +32,15 @@ func (s *Store) CreateReport(ctx context.Context, report *Report) error {
|
||||
report.CreatedAt = now
|
||||
report.UpdatedAt = now
|
||||
report.ReceivedAt = now
|
||||
report.DeadlineAcknowledgment = now.AddDate(0, 0, 7) // 7 days per HinSchG
|
||||
report.DeadlineFeedback = now.AddDate(0, 3, 0) // 3 months per HinSchG
|
||||
report.DeadlineAcknowledgment = now.AddDate(0, 0, 7) // 7 days per HinSchG
|
||||
report.DeadlineFeedback = now.AddDate(0, 3, 0) // 3 months per HinSchG
|
||||
|
||||
if report.Status == "" {
|
||||
report.Status = ReportStatusNew
|
||||
}
|
||||
|
||||
// Generate access key
|
||||
report.AccessKey = generateAccessKey()
|
||||
|
||||
// Generate reference number
|
||||
year := now.Year()
|
||||
seq, err := s.GetNextSequenceNumber(ctx, report.TenantID, year)
|
||||
if err != nil {
|
||||
@@ -50,7 +48,6 @@ func (s *Store) CreateReport(ctx context.Context, report *Report) error {
|
||||
}
|
||||
report.ReferenceNumber = generateReferenceNumber(year, seq)
|
||||
|
||||
// Initialize audit trail
|
||||
if report.AuditTrail == nil {
|
||||
report.AuditTrail = []AuditEntry{}
|
||||
}
|
||||
@@ -154,7 +151,6 @@ func (s *Store) GetReportByAccessKey(ctx context.Context, accessKey string) (*Re
|
||||
|
||||
// ListReports lists reports for a tenant with optional filters
|
||||
func (s *Store) ListReports(ctx context.Context, tenantID uuid.UUID, filters *ReportFilters) ([]Report, int, error) {
|
||||
// Count total
|
||||
countQuery := "SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1"
|
||||
countArgs := []interface{}{tenantID}
|
||||
countArgIdx := 2
|
||||
@@ -178,7 +174,6 @@ func (s *Store) ListReports(ctx context.Context, tenantID uuid.UUID, filters *Re
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Build data query
|
||||
query := `
|
||||
SELECT
|
||||
id, tenant_id, reference_number, access_key,
|
||||
@@ -249,9 +244,7 @@ func (s *Store) ListReports(ctx context.Context, tenantID uuid.UUID, filters *Re
|
||||
report.Status = ReportStatus(status)
|
||||
json.Unmarshal(auditTrailJSON, &report.AuditTrail)
|
||||
|
||||
// Do not expose access key in list responses
|
||||
report.AccessKey = ""
|
||||
|
||||
reports = append(reports, report)
|
||||
}
|
||||
|
||||
@@ -362,230 +355,3 @@ func (s *Store) DeleteReport(ctx context.Context, id uuid.UUID) error {
|
||||
_, err = s.pool.Exec(ctx, "DELETE FROM whistleblower_reports WHERE id = $1", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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),
|
||||
}
|
||||
|
||||
// Total reports
|
||||
s.pool.QueryRow(ctx,
|
||||
"SELECT COUNT(*) FROM whistleblower_reports WHERE tenant_id = $1",
|
||||
tenantID).Scan(&stats.TotalReports)
|
||||
|
||||
// By status
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// By category
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Overdue acknowledgments: reports past deadline_acknowledgment that haven't been acknowledged
|
||||
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)
|
||||
|
||||
// Overdue feedbacks: reports past deadline_feedback that are still open
|
||||
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)
|
||||
|
||||
// Average resolution days (for closed reports)
|
||||
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
|
||||
}
|
||||
|
||||
229
ai-compliance-sdk/internal/whistleblower/store_messages.go
Normal file
229
ai-compliance-sdk/internal/whistleblower/store_messages.go
Normal file
@@ -0,0 +1,229 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user