package training import ( "context" "encoding/json" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // CreateCheckpoint inserts a new checkpoint func (s *Store) CreateCheckpoint(ctx context.Context, cp *Checkpoint) error { cp.ID = uuid.New() cp.CreatedAt = time.Now().UTC() _, err := s.pool.Exec(ctx, ` INSERT INTO training_checkpoints (id, module_id, checkpoint_index, title, timestamp_seconds, created_at) VALUES ($1, $2, $3, $4, $5, $6) `, cp.ID, cp.ModuleID, cp.CheckpointIndex, cp.Title, cp.TimestampSeconds, cp.CreatedAt) return err } // ListCheckpoints returns all checkpoints for a module ordered by index func (s *Store) ListCheckpoints(ctx context.Context, moduleID uuid.UUID) ([]Checkpoint, error) { rows, err := s.pool.Query(ctx, ` SELECT id, module_id, checkpoint_index, title, timestamp_seconds, created_at FROM training_checkpoints WHERE module_id = $1 ORDER BY checkpoint_index `, moduleID) if err != nil { return nil, err } defer rows.Close() var checkpoints []Checkpoint for rows.Next() { var cp Checkpoint if err := rows.Scan(&cp.ID, &cp.ModuleID, &cp.CheckpointIndex, &cp.Title, &cp.TimestampSeconds, &cp.CreatedAt); err != nil { return nil, err } checkpoints = append(checkpoints, cp) } if checkpoints == nil { checkpoints = []Checkpoint{} } return checkpoints, nil } // DeleteCheckpointsForModule removes all checkpoints for a module (used before regenerating) func (s *Store) DeleteCheckpointsForModule(ctx context.Context, moduleID uuid.UUID) error { _, err := s.pool.Exec(ctx, `DELETE FROM training_checkpoints WHERE module_id = $1`, moduleID) return err } // GetCheckpointProgress retrieves progress for a specific checkpoint+assignment func (s *Store) GetCheckpointProgress(ctx context.Context, assignmentID, checkpointID uuid.UUID) (*CheckpointProgress, error) { var cp CheckpointProgress err := s.pool.QueryRow(ctx, ` SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at FROM training_checkpoint_progress WHERE assignment_id = $1 AND checkpoint_id = $2 `, assignmentID, checkpointID).Scan( &cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt, ) if err == pgx.ErrNoRows { return nil, nil } if err != nil { return nil, err } return &cp, nil } // UpsertCheckpointProgress creates or updates checkpoint progress func (s *Store) UpsertCheckpointProgress(ctx context.Context, progress *CheckpointProgress) error { progress.ID = uuid.New() now := time.Now().UTC() progress.LastAttemptAt = &now progress.CreatedAt = now _, err := s.pool.Exec(ctx, ` INSERT INTO training_checkpoint_progress (id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (assignment_id, checkpoint_id) DO UPDATE SET passed = EXCLUDED.passed, attempts = training_checkpoint_progress.attempts + 1, last_attempt_at = EXCLUDED.last_attempt_at `, progress.ID, progress.AssignmentID, progress.CheckpointID, progress.Passed, progress.Attempts, progress.LastAttemptAt, progress.CreatedAt) return err } // GetCheckpointQuestions retrieves quiz questions for a specific checkpoint func (s *Store) GetCheckpointQuestions(ctx context.Context, checkpointID uuid.UUID) ([]QuizQuestion, error) { rows, err := s.pool.Query(ctx, ` SELECT id, module_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at FROM training_quiz_questions WHERE checkpoint_id = $1 AND is_active = true ORDER BY sort_order `, checkpointID) if err != nil { return nil, err } defer rows.Close() var questions []QuizQuestion for rows.Next() { var q QuizQuestion var options []byte var difficulty string if err := rows.Scan(&q.ID, &q.ModuleID, &q.Question, &options, &q.CorrectIndex, &q.Explanation, &difficulty, &q.IsActive, &q.SortOrder, &q.CreatedAt); err != nil { return nil, err } json.Unmarshal(options, &q.Options) q.Difficulty = Difficulty(difficulty) questions = append(questions, q) } if questions == nil { questions = []QuizQuestion{} } return questions, nil } // CreateCheckpointQuizQuestion creates a quiz question linked to a checkpoint func (s *Store) CreateCheckpointQuizQuestion(ctx context.Context, q *QuizQuestion, checkpointID uuid.UUID) error { q.ID = uuid.New() q.CreatedAt = time.Now().UTC() q.IsActive = true options, _ := json.Marshal(q.Options) _, err := s.pool.Exec(ctx, ` INSERT INTO training_quiz_questions (id, module_id, checkpoint_id, question, options, correct_index, explanation, difficulty, is_active, sort_order, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) `, q.ID, q.ModuleID, checkpointID, q.Question, options, q.CorrectIndex, q.Explanation, string(q.Difficulty), q.IsActive, q.SortOrder, q.CreatedAt) return err } // AreAllCheckpointsPassed checks if all checkpoints for a module are passed by an assignment func (s *Store) AreAllCheckpointsPassed(ctx context.Context, assignmentID, moduleID uuid.UUID) (bool, error) { var totalCheckpoints, passedCheckpoints int err := s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM training_checkpoints WHERE module_id = $1 `, moduleID).Scan(&totalCheckpoints) if err != nil { return false, err } if totalCheckpoints == 0 { return true, nil } err = s.pool.QueryRow(ctx, ` SELECT COUNT(*) FROM training_checkpoint_progress cp JOIN training_checkpoints c ON cp.checkpoint_id = c.id WHERE cp.assignment_id = $1 AND c.module_id = $2 AND cp.passed = true `, assignmentID, moduleID).Scan(&passedCheckpoints) if err != nil { return false, err } return passedCheckpoints >= totalCheckpoints, nil } // ListCheckpointProgress returns all checkpoint progress for an assignment func (s *Store) ListCheckpointProgress(ctx context.Context, assignmentID uuid.UUID) ([]CheckpointProgress, error) { rows, err := s.pool.Query(ctx, ` SELECT id, assignment_id, checkpoint_id, passed, attempts, last_attempt_at, created_at FROM training_checkpoint_progress WHERE assignment_id = $1 ORDER BY created_at `, assignmentID) if err != nil { return nil, err } defer rows.Close() var progress []CheckpointProgress for rows.Next() { var cp CheckpointProgress if err := rows.Scan(&cp.ID, &cp.AssignmentID, &cp.CheckpointID, &cp.Passed, &cp.Attempts, &cp.LastAttemptAt, &cp.CreatedAt); err != nil { return nil, err } progress = append(progress, cp) } if progress == nil { progress = []CheckpointProgress{} } return progress, nil }