package repository import ( "context" "errors" "fmt" dbgen "Yimaru-Backend/gen/db" "github.com/jackc/pgx/v5" ) // 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 { if errors.Is(err, pgx.ErrNoRows) { // Exam-prep practices are not in lms_practices; completion is tracked in user_practice_progress only. if err := tx.Commit(ctx); err != nil { return fmt.Errorf("commit: %w", err) } return 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 }