Yimaru-BackEnd/internal/repository/lms_progress_tx.go

214 lines
5.7 KiB
Go

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
}