Yimaru-BackEnd/internal/repository/lms_progress_tx.go
Yared Yemane afdd07d65d Update learner progress to use practice completions only.
Remove lesson completion from learner progress percentages, access completion snapshots, and LMS rollups while keeping generated SQLC and Swagger artifacts in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 03:27:54 -07:00

205 lines
5.4 KiB
Go

package repository
import (
"context"
"fmt"
dbgen "Yimaru-Backend/gen/db"
)
// CompleteLessonForUser records lesson completion for sequential lesson gating and
// re-evaluates higher-level practice-based rollups.
func (s *Store) CompleteLessonForUser(ctx context.Context, userID, lessonID 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.InsertUserLessonProgress(ctx, dbgen.InsertUserLessonProgressParams{UserID: userID, LessonID: lessonID}); err != nil {
return err
}
lesson, err := q.GetLessonByID(ctx, lessonID)
if err != nil {
return err
}
mod, err := q.GetModuleByID(ctx, lesson.ModuleID)
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
}
// CompletePracticeForUser records practice completion and cascades practice-based
// completion upward when all published practices in scope are complete.
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
}
var (
moduleID *int64
courseID int64
)
switch {
case scope.ModuleID.Valid:
mid := scope.ModuleID.Int64
moduleID = &mid
mod, err := q.GetModuleByID(ctx, mid)
if err != nil {
return err
}
courseID = mod.CourseID
case scope.LessonID.Valid:
lesson, err := q.GetLessonByID(ctx, scope.LessonID.Int64)
if err != nil {
return err
}
mid := lesson.ModuleID
moduleID = &mid
mod, err := q.GetModuleByID(ctx, mid)
if err != nil {
return err
}
courseID = mod.CourseID
case scope.CourseID.Valid:
courseID = scope.CourseID.Int64
default:
return fmt.Errorf("practice %d is not linked to lesson/module/course", questionSetID)
}
crs, err := q.GetCourseByID(ctx, courseID)
if err != nil {
return err
}
if err := s.cascadeLMSCompletion(ctx, q, userID, moduleID, 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 int64, moduleID *int64, courseID, programID int64) error {
if moduleID != nil {
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
}
modulePracticesComplete := modulePracticesTotal > 0 && modulePracticesDone >= modulePracticesTotal
if !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 := coursePracticesTotal > 0 && 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 := programPracticesTotal > 0 && programPracticesDone >= programPracticesTotal
if !programCoursesComplete || !programPracticesComplete {
return nil
}
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: programID}); err != nil {
return err
}
return nil
}