refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC
Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC, ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group into same-package files, all under the 500-line hard cap. Zero behavior changes, no renamed exported symbols. Also fixed pre-existing hazard_library split (missing functions and duplicate UUID keys from a prior session). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
340
ai-compliance-sdk/internal/training/store_assignments.go
Normal file
340
ai-compliance-sdk/internal/training/store_assignments.go
Normal file
@@ -0,0 +1,340 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// CreateAssignment creates a new training assignment
|
||||
func (s *Store) CreateAssignment(ctx context.Context, assignment *TrainingAssignment) error {
|
||||
assignment.ID = uuid.New()
|
||||
assignment.CreatedAt = time.Now().UTC()
|
||||
assignment.UpdatedAt = assignment.CreatedAt
|
||||
if assignment.Status == "" {
|
||||
assignment.Status = AssignmentStatusPending
|
||||
}
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO training_assignments (
|
||||
id, tenant_id, module_id, user_id, user_name, user_email,
|
||||
role_code, trigger_type, trigger_event, status, progress_percent,
|
||||
quiz_score, quiz_passed, quiz_attempts,
|
||||
started_at, completed_at, deadline, certificate_id,
|
||||
escalation_level, last_escalation_at, enrollment_id,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20, $21,
|
||||
$22, $23
|
||||
)
|
||||
`,
|
||||
assignment.ID, assignment.TenantID, assignment.ModuleID, assignment.UserID, assignment.UserName, assignment.UserEmail,
|
||||
assignment.RoleCode, string(assignment.TriggerType), assignment.TriggerEvent, string(assignment.Status), assignment.ProgressPercent,
|
||||
assignment.QuizScore, assignment.QuizPassed, assignment.QuizAttempts,
|
||||
assignment.StartedAt, assignment.CompletedAt, assignment.Deadline, assignment.CertificateID,
|
||||
assignment.EscalationLevel, assignment.LastEscalationAt, assignment.EnrollmentID,
|
||||
assignment.CreatedAt, assignment.UpdatedAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAssignment retrieves an assignment by ID
|
||||
func (s *Store) GetAssignment(ctx context.Context, id uuid.UUID) (*TrainingAssignment, error) {
|
||||
var a TrainingAssignment
|
||||
var status, triggerType string
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT
|
||||
ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
|
||||
ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
|
||||
ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
|
||||
ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
|
||||
ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
|
||||
ta.created_at, ta.updated_at,
|
||||
m.module_code, m.title
|
||||
FROM training_assignments ta
|
||||
JOIN training_modules m ON m.id = ta.module_id
|
||||
WHERE ta.id = $1
|
||||
`, id).Scan(
|
||||
&a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
|
||||
&a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
|
||||
&a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
|
||||
&a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
|
||||
&a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
|
||||
&a.CreatedAt, &a.UpdatedAt,
|
||||
&a.ModuleCode, &a.ModuleTitle,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.Status = AssignmentStatus(status)
|
||||
a.TriggerType = TriggerType(triggerType)
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// ListAssignments lists assignments for a tenant with optional filters
|
||||
func (s *Store) ListAssignments(ctx context.Context, tenantID uuid.UUID, filters *AssignmentFilters) ([]TrainingAssignment, int, error) {
|
||||
countQuery := "SELECT COUNT(*) FROM training_assignments WHERE tenant_id = $1"
|
||||
countArgs := []interface{}{tenantID}
|
||||
countArgIdx := 2
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
|
||||
ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
|
||||
ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
|
||||
ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
|
||||
ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
|
||||
ta.created_at, ta.updated_at,
|
||||
m.module_code, m.title
|
||||
FROM training_assignments ta
|
||||
JOIN training_modules m ON m.id = ta.module_id
|
||||
WHERE ta.tenant_id = $1`
|
||||
|
||||
args := []interface{}{tenantID}
|
||||
argIdx := 2
|
||||
|
||||
if filters != nil {
|
||||
if filters.ModuleID != nil {
|
||||
query += fmt.Sprintf(" AND ta.module_id = $%d", argIdx)
|
||||
args = append(args, *filters.ModuleID)
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND module_id = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, *filters.ModuleID)
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.UserID != nil {
|
||||
query += fmt.Sprintf(" AND ta.user_id = $%d", argIdx)
|
||||
args = append(args, *filters.UserID)
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND user_id = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, *filters.UserID)
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.RoleCode != "" {
|
||||
query += fmt.Sprintf(" AND ta.role_code = $%d", argIdx)
|
||||
args = append(args, filters.RoleCode)
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND role_code = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, filters.RoleCode)
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.Status != "" {
|
||||
query += fmt.Sprintf(" AND ta.status = $%d", argIdx)
|
||||
args = append(args, string(filters.Status))
|
||||
argIdx++
|
||||
countQuery += fmt.Sprintf(" AND status = $%d", countArgIdx)
|
||||
countArgs = append(countArgs, string(filters.Status))
|
||||
countArgIdx++
|
||||
}
|
||||
if filters.Overdue != nil && *filters.Overdue {
|
||||
query += " AND ta.deadline < NOW() AND ta.status IN ('pending', 'in_progress')"
|
||||
countQuery += " AND deadline < NOW() AND status IN ('pending', 'in_progress')"
|
||||
}
|
||||
}
|
||||
|
||||
var total int
|
||||
err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
query += " ORDER BY ta.deadline ASC"
|
||||
|
||||
if filters != nil && filters.Limit > 0 {
|
||||
query += fmt.Sprintf(" LIMIT $%d", argIdx)
|
||||
args = append(args, filters.Limit)
|
||||
argIdx++
|
||||
if filters.Offset > 0 {
|
||||
query += fmt.Sprintf(" OFFSET $%d", argIdx)
|
||||
args = append(args, filters.Offset)
|
||||
argIdx++
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var assignments []TrainingAssignment
|
||||
for rows.Next() {
|
||||
var a TrainingAssignment
|
||||
var status, triggerType string
|
||||
|
||||
err := rows.Scan(
|
||||
&a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
|
||||
&a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
|
||||
&a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
|
||||
&a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
|
||||
&a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
|
||||
&a.CreatedAt, &a.UpdatedAt,
|
||||
&a.ModuleCode, &a.ModuleTitle,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
a.Status = AssignmentStatus(status)
|
||||
a.TriggerType = TriggerType(triggerType)
|
||||
assignments = append(assignments, a)
|
||||
}
|
||||
|
||||
if assignments == nil {
|
||||
assignments = []TrainingAssignment{}
|
||||
}
|
||||
|
||||
return assignments, total, nil
|
||||
}
|
||||
|
||||
// UpdateAssignmentStatus updates the status and related fields
|
||||
func (s *Store) UpdateAssignmentStatus(ctx context.Context, id uuid.UUID, status AssignmentStatus, progress int) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET
|
||||
status = $2,
|
||||
progress_percent = $3,
|
||||
started_at = CASE
|
||||
WHEN started_at IS NULL AND $2 IN ('in_progress', 'completed') THEN $4
|
||||
ELSE started_at
|
||||
END,
|
||||
completed_at = CASE
|
||||
WHEN $2 = 'completed' THEN $4
|
||||
ELSE completed_at
|
||||
END,
|
||||
updated_at = $4
|
||||
WHERE id = $1
|
||||
`, id, string(status), progress, now)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAssignmentDeadline updates the deadline of an assignment
|
||||
func (s *Store) UpdateAssignmentDeadline(ctx context.Context, id uuid.UUID, deadline time.Time) error {
|
||||
now := time.Now().UTC()
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET
|
||||
deadline = $2,
|
||||
updated_at = $3
|
||||
WHERE id = $1
|
||||
`, id, deadline, now)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAssignmentQuizResult updates quiz-related fields on an assignment
|
||||
func (s *Store) UpdateAssignmentQuizResult(ctx context.Context, id uuid.UUID, score float64, passed bool, attempts int) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET
|
||||
quiz_score = $2,
|
||||
quiz_passed = $3,
|
||||
quiz_attempts = $4,
|
||||
status = CASE WHEN $3 = true THEN 'completed' ELSE status END,
|
||||
completed_at = CASE WHEN $3 = true THEN $5 ELSE completed_at END,
|
||||
progress_percent = CASE WHEN $3 = true THEN 100 ELSE progress_percent END,
|
||||
updated_at = $5
|
||||
WHERE id = $1
|
||||
`, id, score, passed, attempts, now)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ListOverdueAssignments returns assignments past their deadline
|
||||
func (s *Store) ListOverdueAssignments(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
|
||||
overdue := true
|
||||
assignments, _, err := s.ListAssignments(ctx, tenantID, &AssignmentFilters{
|
||||
Overdue: &overdue,
|
||||
Limit: 1000,
|
||||
})
|
||||
return assignments, err
|
||||
}
|
||||
|
||||
// SetCertificateID sets the certificate ID on an assignment
|
||||
func (s *Store) SetCertificateID(ctx context.Context, assignmentID, certID uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET certificate_id = $2, updated_at = NOW() WHERE id = $1
|
||||
`, assignmentID, certID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAssignmentByCertificateID finds an assignment by its certificate ID
|
||||
func (s *Store) GetAssignmentByCertificateID(ctx context.Context, certID uuid.UUID) (*TrainingAssignment, error) {
|
||||
var assignmentID uuid.UUID
|
||||
err := s.pool.QueryRow(ctx,
|
||||
"SELECT id FROM training_assignments WHERE certificate_id = $1",
|
||||
certID).Scan(&assignmentID)
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.GetAssignment(ctx, assignmentID)
|
||||
}
|
||||
|
||||
// ListCertificates lists assignments that have certificates for a tenant
|
||||
func (s *Store) ListCertificates(ctx context.Context, tenantID uuid.UUID) ([]TrainingAssignment, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
ta.id, ta.tenant_id, ta.module_id, ta.user_id, ta.user_name, ta.user_email,
|
||||
ta.role_code, ta.trigger_type, ta.trigger_event, ta.status, ta.progress_percent,
|
||||
ta.quiz_score, ta.quiz_passed, ta.quiz_attempts,
|
||||
ta.started_at, ta.completed_at, ta.deadline, ta.certificate_id,
|
||||
ta.escalation_level, ta.last_escalation_at, ta.enrollment_id,
|
||||
ta.created_at, ta.updated_at,
|
||||
m.module_code, m.title
|
||||
FROM training_assignments ta
|
||||
JOIN training_modules m ON m.id = ta.module_id
|
||||
WHERE ta.tenant_id = $1 AND ta.certificate_id IS NOT NULL
|
||||
ORDER BY ta.completed_at DESC
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var assignments []TrainingAssignment
|
||||
for rows.Next() {
|
||||
var a TrainingAssignment
|
||||
var status, triggerType string
|
||||
|
||||
err := rows.Scan(
|
||||
&a.ID, &a.TenantID, &a.ModuleID, &a.UserID, &a.UserName, &a.UserEmail,
|
||||
&a.RoleCode, &triggerType, &a.TriggerEvent, &status, &a.ProgressPercent,
|
||||
&a.QuizScore, &a.QuizPassed, &a.QuizAttempts,
|
||||
&a.StartedAt, &a.CompletedAt, &a.Deadline, &a.CertificateID,
|
||||
&a.EscalationLevel, &a.LastEscalationAt, &a.EnrollmentID,
|
||||
&a.CreatedAt, &a.UpdatedAt,
|
||||
&a.ModuleCode, &a.ModuleTitle,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.Status = AssignmentStatus(status)
|
||||
a.TriggerType = TriggerType(triggerType)
|
||||
assignments = append(assignments, a)
|
||||
}
|
||||
|
||||
if assignments == nil {
|
||||
assignments = []TrainingAssignment{}
|
||||
}
|
||||
|
||||
return assignments, nil
|
||||
}
|
||||
Reference in New Issue
Block a user