From 9027b65011b553123a1abaad765db0f6da395154 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 28 Apr 2026 09:56:53 -0700 Subject: [PATCH] Require lesson and practice completion for LMS rollups. Update lesson and practice completion flows to cascade module/course/program progress only when both lesson completion and related published practice completion criteria are met, and align progress counters with the new rule. Made-with: Cursor --- db/query/lms_progress.sql | 92 ++++++++++ gen/db/lms_progress.sql.go | 177 +++++++++++++++++++ internal/repository/lms_access.go | 73 ++++++-- internal/repository/lms_progress_tx.go | 202 +++++++++++++++++----- internal/services/lmsprogress/service.go | 5 + internal/web_server/handlers/questions.go | 2 +- 6 files changed, 485 insertions(+), 66 deletions(-) diff --git a/db/query/lms_progress.sql b/db/query/lms_progress.sql index 56315d1..5c6a606 100644 --- a/db/query/lms_progress.sql +++ b/db/query/lms_progress.sql @@ -246,3 +246,95 @@ FROM WHERE c.program_id = $1 AND ulp.user_id = $2; + +-- Published practices in a module (module-level and lesson-level practices should carry module_id). +-- name: CountPublishedPracticesInModule :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + lp.module_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedPracticesInModule :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + lp.module_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: CountPublishedPracticesInCourse :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + lp.course_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedPracticesInCourse :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + lp.course_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: CountPublishedPracticesInProgram :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN courses c ON c.id = lp.course_id + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + c.program_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: CountUserCompletedPublishedPracticesInProgram :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN courses c ON c.id = lp.course_id + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + c.program_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED'; + +-- name: GetPracticeScopeByQuestionSetID :one +SELECT + id, + course_id, + module_id, + lesson_id +FROM + lms_practices +WHERE + question_set_id = $1 +ORDER BY + id DESC +LIMIT 1; diff --git a/gen/db/lms_progress.sql.go b/gen/db/lms_progress.sql.go index f4f2b4d..8376ad9 100644 --- a/gen/db/lms_progress.sql.go +++ b/gen/db/lms_progress.sql.go @@ -7,6 +7,8 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const CountCoursesInProgram = `-- name: CountCoursesInProgram :one @@ -94,6 +96,65 @@ func (q *Queries) CountModulesInCourse(ctx context.Context, courseID int64) (int return n, err } +const CountPublishedPracticesInCourse = `-- name: CountPublishedPracticesInCourse :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + lp.course_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedPracticesInCourse(ctx context.Context, courseID pgtype.Int8) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedPracticesInCourse, courseID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountPublishedPracticesInModule = `-- name: CountPublishedPracticesInModule :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + lp.module_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +// Published practices in a module (module-level and lesson-level practices should carry module_id). +func (q *Queries) CountPublishedPracticesInModule(ctx context.Context, moduleID pgtype.Int8) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedPracticesInModule, moduleID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountPublishedPracticesInProgram = `-- name: CountPublishedPracticesInProgram :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN courses c ON c.id = lp.course_id + INNER JOIN question_sets qs ON qs.id = lp.question_set_id +WHERE + c.program_id = $1 + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +func (q *Queries) CountPublishedPracticesInProgram(ctx context.Context, programID int64) (int32, error) { + row := q.db.QueryRow(ctx, CountPublishedPracticesInProgram, programID) + var n int32 + err := row.Scan(&n) + return n, err +} + const CountUserCompletedCoursesInProgram = `-- name: CountUserCompletedCoursesInProgram :one SELECT count(*)::int AS n @@ -212,6 +273,122 @@ func (q *Queries) CountUserCompletedModulesInCourse(ctx context.Context, arg Cou return n, err } +const CountUserCompletedPublishedPracticesInCourse = `-- name: CountUserCompletedPublishedPracticesInCourse :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + lp.course_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +type CountUserCompletedPublishedPracticesInCourseParams struct { + CourseID pgtype.Int8 `json:"course_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedPracticesInCourse(ctx context.Context, arg CountUserCompletedPublishedPracticesInCourseParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInCourse, arg.CourseID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedPracticesInModule = `-- name: CountUserCompletedPublishedPracticesInModule :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + lp.module_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +type CountUserCompletedPublishedPracticesInModuleParams struct { + ModuleID pgtype.Int8 `json:"module_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedPracticesInModule(ctx context.Context, arg CountUserCompletedPublishedPracticesInModuleParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInModule, arg.ModuleID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const CountUserCompletedPublishedPracticesInProgram = `-- name: CountUserCompletedPublishedPracticesInProgram :one +SELECT + count(*)::int AS n +FROM + lms_practices lp + INNER JOIN courses c ON c.id = lp.course_id + INNER JOIN question_sets qs ON qs.id = lp.question_set_id + INNER JOIN user_practice_progress upp ON upp.question_set_id = lp.question_set_id +WHERE + c.program_id = $1 + AND upp.user_id = $2 + AND upp.completed_at IS NOT NULL + AND qs.set_type = 'PRACTICE' + AND qs.status = 'PUBLISHED' +` + +type CountUserCompletedPublishedPracticesInProgramParams struct { + ProgramID int64 `json:"program_id"` + UserID int64 `json:"user_id"` +} + +func (q *Queries) CountUserCompletedPublishedPracticesInProgram(ctx context.Context, arg CountUserCompletedPublishedPracticesInProgramParams) (int32, error) { + row := q.db.QueryRow(ctx, CountUserCompletedPublishedPracticesInProgram, arg.ProgramID, arg.UserID) + var n int32 + err := row.Scan(&n) + return n, err +} + +const GetPracticeScopeByQuestionSetID = `-- name: GetPracticeScopeByQuestionSetID :one +SELECT + id, + course_id, + module_id, + lesson_id +FROM + lms_practices +WHERE + question_set_id = $1 +ORDER BY + id DESC +LIMIT 1 +` + +type GetPracticeScopeByQuestionSetIDRow struct { + ID int64 `json:"id"` + CourseID pgtype.Int8 `json:"course_id"` + ModuleID pgtype.Int8 `json:"module_id"` + LessonID pgtype.Int8 `json:"lesson_id"` +} + +func (q *Queries) GetPracticeScopeByQuestionSetID(ctx context.Context, questionSetID int64) (GetPracticeScopeByQuestionSetIDRow, error) { + row := q.db.QueryRow(ctx, GetPracticeScopeByQuestionSetID, questionSetID) + var i GetPracticeScopeByQuestionSetIDRow + err := row.Scan( + &i.ID, + &i.CourseID, + &i.ModuleID, + &i.LessonID, + ) + return i, err +} + const GetPreviousCourseInProgram = `-- name: GetPreviousCourseInProgram :one SELECT c2.id, c2.program_id, c2.name, c2.description, c2.thumbnail, c2.created_at, c2.updated_at, c2.sort_order diff --git a/internal/repository/lms_access.go b/internal/repository/lms_access.go index 753338e..8db1f57 100644 --- a/internal/repository/lms_access.go +++ b/internal/repository/lms_access.go @@ -38,50 +38,89 @@ func (s *Store) LmsUserHasLessonProgress(ctx context.Context, userID, lessonID i return s.queries.UserHasLessonProgress(ctx, dbgen.UserHasLessonProgressParams{UserID: userID, LessonID: lessonID}) } -// LmsUserLessonProgressInModule returns completed and total lesson counts in a module (for progress UI). +// LmsUserLessonProgressInModule returns combined completed/total counts for lessons + published practices in a module. func (s *Store) LmsUserLessonProgressInModule(ctx context.Context, userID, moduleID int64) (completed, total int32, err error) { - total, err = s.queries.CountLessonsInModule(ctx, moduleID) + lessonTotal, err := s.queries.CountLessonsInModule(ctx, moduleID) if err != nil { return 0, 0, err } - completed, err = s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{ + lessonCompleted, err := s.queries.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{ ModuleID: moduleID, UserID: userID, }) if err != nil { return 0, 0, err } - return completed, total, nil -} - -// LmsUserLessonProgressInCourse returns completed and total lesson counts in a course (all modules). -func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) { - total, err = s.queries.CountLessonsInCourse(ctx, courseID) + practiceTotal, err := s.queries.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID)) if err != nil { return 0, 0, err } - completed, err = s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{ + practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{ + ModuleID: toPgInt8(&moduleID), + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + total = lessonTotal + practiceTotal + completed = lessonCompleted + practiceCompleted + return completed, total, nil +} + +// LmsUserLessonProgressInCourse returns combined completed/total counts for lessons + published practices in a course. +func (s *Store) LmsUserLessonProgressInCourse(ctx context.Context, userID, courseID int64) (completed, total int32, err error) { + lessonTotal, err := s.queries.CountLessonsInCourse(ctx, courseID) + if err != nil { + return 0, 0, err + } + lessonCompleted, err := s.queries.CountUserCompletedLessonsInCourse(ctx, dbgen.CountUserCompletedLessonsInCourseParams{ CourseID: courseID, UserID: userID, }) if err != nil { return 0, 0, err } - return completed, total, nil -} - -// LmsUserLessonProgressInProgram returns completed and total lesson counts in a program (all courses). -func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) { - total, err = s.queries.CountLessonsInProgram(ctx, programID) + practiceTotal, err := s.queries.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID)) if err != nil { return 0, 0, err } - completed, err = s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{ + practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{ + CourseID: toPgInt8(&courseID), + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + total = lessonTotal + practiceTotal + completed = lessonCompleted + practiceCompleted + return completed, total, nil +} + +// LmsUserLessonProgressInProgram returns combined completed/total counts for lessons + published practices in a program. +func (s *Store) LmsUserLessonProgressInProgram(ctx context.Context, userID, programID int64) (completed, total int32, err error) { + lessonTotal, err := s.queries.CountLessonsInProgram(ctx, programID) + if err != nil { + return 0, 0, err + } + lessonCompleted, err := s.queries.CountUserCompletedLessonsInProgram(ctx, dbgen.CountUserCompletedLessonsInProgramParams{ ProgramID: programID, UserID: userID, }) if err != nil { return 0, 0, err } + practiceTotal, err := s.queries.CountPublishedPracticesInProgram(ctx, programID) + if err != nil { + return 0, 0, err + } + practiceCompleted, err := s.queries.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{ + ProgramID: programID, + UserID: userID, + }) + if err != nil { + return 0, 0, err + } + total = lessonTotal + practiceTotal + completed = lessonCompleted + practiceCompleted return completed, total, nil } diff --git a/internal/repository/lms_progress_tx.go b/internal/repository/lms_progress_tx.go index 98ecf0a..a6da6ae 100644 --- a/internal/repository/lms_progress_tx.go +++ b/internal/repository/lms_progress_tx.go @@ -7,8 +7,8 @@ import ( dbgen "Yimaru-Backend/gen/db" ) -// CompleteLessonForUser records lesson completion and cascades to module, course, and program when the user -// has fully completed the preceding scope. Runs in a single transaction. +// CompleteLessonForUser records lesson completion and cascades completion upward when +// both lesson and related practice requirements are satisfied. func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int64) error { q, tx, err := s.BeginTx(ctx) if err != nil { @@ -31,56 +31,162 @@ func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID int6 if err != nil { return err } - nLess, err := q.CountLessonsInModule(ctx, lesson.ModuleID) - if err != nil { + + if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil { return err } - nDoneLess, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{ - ModuleID: lesson.ModuleID, - UserID: userID, - }) - if err != nil { - return err - } - if nLess > 0 && nDoneLess >= nLess { - if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: mod.ID}); err != nil { - return err - } - nMods, err := q.CountModulesInCourse(ctx, mod.CourseID) - if err != nil { - return err - } - nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{ - CourseID: mod.CourseID, - UserID: userID, - }) - if err != nil { - return err - } - if nMods > 0 && nDoneMods >= nMods { - if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: crs.ID}); err != nil { - return err - } - nCr, err := q.CountCoursesInProgram(ctx, crs.ProgramID) - if err != nil { - return err - } - nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{ - ProgramID: crs.ProgramID, - UserID: userID, - }) - if err != nil { - return err - } - if nCr > 0 && nCrDone >= nCr { - if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: crs.ProgramID}); err != nil { - return err - } - } - } - } + if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } return nil } + +// CompletePracticeForUser records practice completion and cascades completion upward when +// both lesson and related practice requirements are satisfied. +func (s *Store) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error { + q, tx, err := s.BeginTx(ctx) + if err != nil { + return fmt.Errorf("begin tx: %w", err) + } + defer func() { _ = tx.Rollback(ctx) }() + + if _, err := q.MarkPracticeCompleted(ctx, dbgen.MarkPracticeCompletedParams{ + UserID: userID, + QuestionSetID: questionSetID, + }); err != nil { + return err + } + + scope, err := q.GetPracticeScopeByQuestionSetID(ctx, questionSetID) + if err != nil { + return err + } + if !scope.ModuleID.Valid { + return fmt.Errorf("practice %d is not linked to a module", questionSetID) + } + + mod, err := q.GetModuleByID(ctx, scope.ModuleID.Int64) + if err != nil { + return err + } + + crs, err := q.GetCourseByID(ctx, mod.CourseID) + if err != nil { + return err + } + + if err := s.cascadeLMSCompletion(ctx, q, userID, mod.ID, crs.ID, crs.ProgramID); err != nil { + return err + } + + if err := tx.Commit(ctx); err != nil { + return fmt.Errorf("commit: %w", err) + } + return nil +} + +func (s *Store) cascadeLMSCompletion(ctx context.Context, q *dbgen.Queries, userID, moduleID, courseID, programID int64) error { + moduleLessonsTotal, err := q.CountLessonsInModule(ctx, moduleID) + if err != nil { + return err + } + moduleLessonsDone, err := q.CountUserCompletedLessonsInModule(ctx, dbgen.CountUserCompletedLessonsInModuleParams{ + ModuleID: moduleID, + UserID: userID, + }) + if err != nil { + return err + } + modulePracticesTotal, err := q.CountPublishedPracticesInModule(ctx, toPgInt8(&moduleID)) + if err != nil { + return err + } + modulePracticesDone, err := q.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{ + ModuleID: toPgInt8(&moduleID), + UserID: userID, + }) + if err != nil { + return err + } + + moduleLessonsComplete := moduleLessonsTotal > 0 && moduleLessonsDone >= moduleLessonsTotal + modulePracticesComplete := modulePracticesDone >= modulePracticesTotal + if !moduleLessonsComplete || !modulePracticesComplete { + return nil + } + + if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: moduleID}); err != nil { + return err + } + + nMods, err := q.CountModulesInCourse(ctx, courseID) + if err != nil { + return err + } + nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{ + CourseID: courseID, + UserID: userID, + }) + if err != nil { + return err + } + + coursePracticesTotal, err := q.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID)) + if err != nil { + return err + } + coursePracticesDone, err := q.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{ + CourseID: toPgInt8(&courseID), + UserID: userID, + }) + if err != nil { + return err + } + + courseModulesComplete := nMods > 0 && nDoneMods >= nMods + coursePracticesComplete := coursePracticesDone >= coursePracticesTotal + if !courseModulesComplete || !coursePracticesComplete { + return nil + } + + if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: courseID}); err != nil { + return err + } + + nCr, err := q.CountCoursesInProgram(ctx, programID) + if err != nil { + return err + } + nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{ + ProgramID: programID, + UserID: userID, + }) + if err != nil { + return err + } + + programPracticesTotal, err := q.CountPublishedPracticesInProgram(ctx, programID) + if err != nil { + return err + } + programPracticesDone, err := q.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{ + ProgramID: programID, + UserID: userID, + }) + if err != nil { + return err + } + + programCoursesComplete := nCr > 0 && nCrDone >= nCr + programPracticesComplete := programPracticesDone >= programPracticesTotal + if !programCoursesComplete || !programPracticesComplete { + return nil + } + + if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: programID}); err != nil { + return err + } + + return nil +} diff --git a/internal/services/lmsprogress/service.go b/internal/services/lmsprogress/service.go index 8950f76..7e1b4b9 100644 --- a/internal/services/lmsprogress/service.go +++ b/internal/services/lmsprogress/service.go @@ -31,6 +31,11 @@ func (s *Service) CompleteLessonForUser(ctx context.Context, userID, lessonID in return s.store.CompleteLessonForUser(ctx, userID, lessonID) } +// CompletePracticeForUser records practice completion and rolls up to module, course, and program when applicable. +func (s *Service) CompletePracticeForUser(ctx context.Context, userID, questionSetID int64) error { + return s.store.CompletePracticeForUser(ctx, userID, questionSetID) +} + // GetMyProgress returns completed lesson, module, course, and program IDs for the user. func (s *Service) GetMyProgress(ctx context.Context, userID int64) (domain.LMSUserProgress, error) { return s.store.GetLMSUserProgressSnapshot(ctx, userID) diff --git a/internal/web_server/handlers/questions.go b/internal/web_server/handlers/questions.go index dce2e57..95e6abf 100644 --- a/internal/web_server/handlers/questions.go +++ b/internal/web_server/handlers/questions.go @@ -1317,7 +1317,7 @@ func (h *Handler) CompletePractice(c *fiber.Ctx) error { }) } - if err := h.questionsSvc.MarkPracticeCompleted(c.Context(), userID, set.ID); err != nil { + if err := h.lmsProgressSvc.CompletePracticeForUser(c.Context(), userID, set.ID); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to complete practice", Error: err.Error(),