Yimaru-BackEnd/internal/repository/lms_progress_tx.go
2026-06-09 05:11:16 -07:00

260 lines
7.2 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
"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) (domain.LMSPracticeCompletionResult, error) {
var empty domain.LMSPracticeCompletionResult
q, tx, err := s.BeginTx(ctx)
if err != nil {
return empty, 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 empty, 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 empty, fmt.Errorf("commit: %w", err)
}
return empty, nil
}
return empty, 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 empty, err
}
courseID = mod.CourseID
case scope.LessonID.Valid:
lesson, err := q.GetLessonByID(ctx, scope.LessonID.Int64)
if err != nil {
return empty, err
}
mid := lesson.ModuleID
moduleID = &mid
mod, err := q.GetModuleByID(ctx, mid)
if err != nil {
return empty, err
}
courseID = mod.CourseID
case scope.CourseID.Valid:
courseID = scope.CourseID.Int64
default:
return empty, fmt.Errorf("practice %d is not linked to lesson/module/course", questionSetID)
}
crs, err := q.GetCourseByID(ctx, courseID)
if err != nil {
return empty, err
}
result, err := s.cascadeLMSCompletion(ctx, q, userID, moduleID, crs.ID, crs.ProgramID)
if err != nil {
return empty, err
}
if err := tx.Commit(ctx); err != nil {
return empty, fmt.Errorf("commit: %w", err)
}
return result, nil
}
func (s *Store) cascadeLMSCompletion(ctx context.Context, q *dbgen.Queries, userID int64, moduleID *int64, courseID, programID int64) (domain.LMSPracticeCompletionResult, error) {
var result domain.LMSPracticeCompletionResult
if moduleID != nil {
modulePracticesTotal, err := q.CountPublishedPracticesInModule(ctx, toPgInt8(moduleID))
if err != nil {
return result, err
}
modulePracticesDone, err := q.CountUserCompletedPublishedPracticesInModule(ctx, dbgen.CountUserCompletedPublishedPracticesInModuleParams{
ModuleID: toPgInt8(moduleID),
UserID: userID,
})
if err != nil {
return result, err
}
modulePracticesComplete := modulePracticesTotal > 0 && modulePracticesDone >= modulePracticesTotal
if !modulePracticesComplete {
return result, nil
}
alreadyDone, err := q.HasUserCompletedModule(ctx, dbgen.HasUserCompletedModuleParams{
UserID: userID,
ModuleID: *moduleID,
})
if err != nil {
return result, err
}
if !alreadyDone {
if err := q.InsertUserModuleProgress(ctx, dbgen.InsertUserModuleProgressParams{UserID: userID, ModuleID: *moduleID}); err != nil {
return result, err
}
mod, err := q.GetModuleByID(ctx, *moduleID)
if err != nil {
return result, err
}
result.ModuleCompleted = &domain.LMSCompletionMilestone{ID: mod.ID, Name: mod.Name}
}
}
nMods, err := q.CountModulesInCourse(ctx, courseID)
if err != nil {
return result, err
}
nDoneMods, err := q.CountUserCompletedModulesInCourse(ctx, dbgen.CountUserCompletedModulesInCourseParams{
CourseID: courseID,
UserID: userID,
})
if err != nil {
return result, err
}
coursePracticesTotal, err := q.CountPublishedPracticesInCourse(ctx, toPgInt8(&courseID))
if err != nil {
return result, err
}
coursePracticesDone, err := q.CountUserCompletedPublishedPracticesInCourse(ctx, dbgen.CountUserCompletedPublishedPracticesInCourseParams{
CourseID: toPgInt8(&courseID),
UserID: userID,
})
if err != nil {
return result, err
}
courseModulesComplete := nMods > 0 && nDoneMods >= nMods
coursePracticesComplete := coursePracticesTotal > 0 && coursePracticesDone >= coursePracticesTotal
if !courseModulesComplete || !coursePracticesComplete {
return result, nil
}
alreadyDone, err := q.HasUserCompletedCourse(ctx, dbgen.HasUserCompletedCourseParams{
UserID: userID,
CourseID: courseID,
})
if err != nil {
return result, err
}
if !alreadyDone {
if err := q.InsertUserCourseProgress(ctx, dbgen.InsertUserCourseProgressParams{UserID: userID, CourseID: courseID}); err != nil {
return result, err
}
crs, err := q.GetCourseByID(ctx, courseID)
if err != nil {
return result, err
}
result.CourseCompleted = &domain.LMSCompletionMilestone{ID: crs.ID, Name: crs.Name}
}
nCr, err := q.CountCoursesInProgram(ctx, programID)
if err != nil {
return result, err
}
nCrDone, err := q.CountUserCompletedCoursesInProgram(ctx, dbgen.CountUserCompletedCoursesInProgramParams{
ProgramID: programID,
UserID: userID,
})
if err != nil {
return result, err
}
programPracticesTotal, err := q.CountPublishedPracticesInProgram(ctx, programID)
if err != nil {
return result, err
}
programPracticesDone, err := q.CountUserCompletedPublishedPracticesInProgram(ctx, dbgen.CountUserCompletedPublishedPracticesInProgramParams{
ProgramID: programID,
UserID: userID,
})
if err != nil {
return result, err
}
programCoursesComplete := nCr > 0 && nCrDone >= nCr
programPracticesComplete := programPracticesTotal > 0 && programPracticesDone >= programPracticesTotal
if !programCoursesComplete || !programPracticesComplete {
return result, nil
}
alreadyDone, err = q.HasUserCompletedProgram(ctx, dbgen.HasUserCompletedProgramParams{
UserID: userID,
ProgramID: programID,
})
if err != nil {
return result, err
}
if !alreadyDone {
if err := q.InsertUserProgramProgress(ctx, dbgen.InsertUserProgramProgressParams{UserID: userID, ProgramID: programID}); err != nil {
return result, err
}
prog, err := q.GetProgramByID(ctx, programID)
if err != nil {
return result, err
}
result.ProgramCompleted = &domain.LMSCompletionMilestone{ID: prog.ID, Name: prog.Name}
}
return result, nil
}