package training import ( "context" "encoding/json" "fmt" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // CreateModule creates a new training module func (s *Store) CreateModule(ctx context.Context, module *TrainingModule) error { module.ID = uuid.New() module.CreatedAt = time.Now().UTC() module.UpdatedAt = module.CreatedAt if !module.IsActive { module.IsActive = true } isoControls, _ := json.Marshal(module.ISOControls) _, err := s.pool.Exec(ctx, ` INSERT INTO training_modules ( id, tenant_id, academy_course_id, module_code, title, description, regulation_area, nis2_relevant, iso_controls, frequency_type, validity_days, risk_weight, content_type, duration_minutes, pass_threshold, is_active, sort_order, created_at, updated_at ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19 ) `, module.ID, module.TenantID, module.AcademyCourseID, module.ModuleCode, module.Title, module.Description, string(module.RegulationArea), module.NIS2Relevant, isoControls, string(module.FrequencyType), module.ValidityDays, module.RiskWeight, module.ContentType, module.DurationMinutes, module.PassThreshold, module.IsActive, module.SortOrder, module.CreatedAt, module.UpdatedAt, ) return err } // GetModule retrieves a module by ID func (s *Store) GetModule(ctx context.Context, id uuid.UUID) (*TrainingModule, error) { var module TrainingModule var regulationArea, frequencyType string var isoControls []byte err := s.pool.QueryRow(ctx, ` SELECT id, tenant_id, academy_course_id, module_code, title, description, regulation_area, nis2_relevant, iso_controls, frequency_type, validity_days, risk_weight, content_type, duration_minutes, pass_threshold, is_active, sort_order, created_at, updated_at FROM training_modules WHERE id = $1 `, id).Scan( &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description, ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType, &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes, &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } module.RegulationArea = RegulationArea(regulationArea) module.FrequencyType = FrequencyType(frequencyType) json.Unmarshal(isoControls, &module.ISOControls) if module.ISOControls == nil { module.ISOControls = []string{} } return &module, nil } // ListModules lists training modules for a tenant with optional filters func (s *Store) ListModules(ctx context.Context, tenantID uuid.UUID, filters *ModuleFilters) ([]TrainingModule, int, error) { countQuery := "SELECT COUNT(*) FROM training_modules WHERE tenant_id = $1" countArgs := []interface{}{tenantID} countArgIdx := 2 query := ` SELECT id, tenant_id, academy_course_id, module_code, title, description, regulation_area, nis2_relevant, iso_controls, frequency_type, validity_days, risk_weight, content_type, duration_minutes, pass_threshold, is_active, sort_order, created_at, updated_at FROM training_modules WHERE tenant_id = $1` args := []interface{}{tenantID} argIdx := 2 if filters != nil { if filters.RegulationArea != "" { query += fmt.Sprintf(" AND regulation_area = $%d", argIdx) args = append(args, string(filters.RegulationArea)) argIdx++ countQuery += fmt.Sprintf(" AND regulation_area = $%d", countArgIdx) countArgs = append(countArgs, string(filters.RegulationArea)) countArgIdx++ } if filters.FrequencyType != "" { query += fmt.Sprintf(" AND frequency_type = $%d", argIdx) args = append(args, string(filters.FrequencyType)) argIdx++ countQuery += fmt.Sprintf(" AND frequency_type = $%d", countArgIdx) countArgs = append(countArgs, string(filters.FrequencyType)) countArgIdx++ } if filters.IsActive != nil { query += fmt.Sprintf(" AND is_active = $%d", argIdx) args = append(args, *filters.IsActive) argIdx++ countQuery += fmt.Sprintf(" AND is_active = $%d", countArgIdx) countArgs = append(countArgs, *filters.IsActive) countArgIdx++ } if filters.NIS2Relevant != nil { query += fmt.Sprintf(" AND nis2_relevant = $%d", argIdx) args = append(args, *filters.NIS2Relevant) argIdx++ countQuery += fmt.Sprintf(" AND nis2_relevant = $%d", countArgIdx) countArgs = append(countArgs, *filters.NIS2Relevant) countArgIdx++ } if filters.Search != "" { query += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", argIdx, argIdx, argIdx) args = append(args, "%"+filters.Search+"%") argIdx++ countQuery += fmt.Sprintf(" AND (title ILIKE $%d OR description ILIKE $%d OR module_code ILIKE $%d)", countArgIdx, countArgIdx, countArgIdx) countArgs = append(countArgs, "%"+filters.Search+"%") countArgIdx++ } } var total int err := s.pool.QueryRow(ctx, countQuery, countArgs...).Scan(&total) if err != nil { return nil, 0, err } query += " ORDER BY sort_order ASC, created_at DESC" 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 modules []TrainingModule for rows.Next() { var module TrainingModule var regulationArea, frequencyType string var isoControls []byte err := rows.Scan( &module.ID, &module.TenantID, &module.AcademyCourseID, &module.ModuleCode, &module.Title, &module.Description, ®ulationArea, &module.NIS2Relevant, &isoControls, &frequencyType, &module.ValidityDays, &module.RiskWeight, &module.ContentType, &module.DurationMinutes, &module.PassThreshold, &module.IsActive, &module.SortOrder, &module.CreatedAt, &module.UpdatedAt, ) if err != nil { return nil, 0, err } module.RegulationArea = RegulationArea(regulationArea) module.FrequencyType = FrequencyType(frequencyType) json.Unmarshal(isoControls, &module.ISOControls) if module.ISOControls == nil { module.ISOControls = []string{} } modules = append(modules, module) } if modules == nil { modules = []TrainingModule{} } return modules, total, nil } // UpdateModule updates a training module func (s *Store) UpdateModule(ctx context.Context, module *TrainingModule) error { module.UpdatedAt = time.Now().UTC() isoControls, _ := json.Marshal(module.ISOControls) _, err := s.pool.Exec(ctx, ` UPDATE training_modules SET title = $2, description = $3, nis2_relevant = $4, iso_controls = $5, validity_days = $6, risk_weight = $7, duration_minutes = $8, pass_threshold = $9, is_active = $10, sort_order = $11, updated_at = $12 WHERE id = $1 `, module.ID, module.Title, module.Description, module.NIS2Relevant, isoControls, module.ValidityDays, module.RiskWeight, module.DurationMinutes, module.PassThreshold, module.IsActive, module.SortOrder, module.UpdatedAt, ) return err } // DeleteModule deletes a training module by ID func (s *Store) DeleteModule(ctx context.Context, id uuid.UUID) error { _, err := s.pool.Exec(ctx, `DELETE FROM training_modules WHERE id = $1`, id) return err } // SetAcademyCourseID links a training module to an academy course func (s *Store) SetAcademyCourseID(ctx context.Context, moduleID, courseID uuid.UUID) error { _, err := s.pool.Exec(ctx, ` UPDATE training_modules SET academy_course_id = $2, updated_at = $3 WHERE id = $1 `, moduleID, courseID, time.Now().UTC()) return err }