package repository import ( dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "context" "errors" "time" "github.com/jackc/pgx/v5" ) func NewProgressionStore(s *Store) ports.ProgressionStore { return s } func (s *Store) AddSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error { _, err := s.queries.AddSubCoursePrerequisite(ctx, dbgen.AddSubCoursePrerequisiteParams{ SubCourseID: subCourseID, PrerequisiteSubCourseID: prerequisiteSubCourseID, }) return err } func (s *Store) RemoveSubCoursePrerequisite(ctx context.Context, subCourseID, prerequisiteSubCourseID int64) error { return s.queries.RemoveSubCoursePrerequisite(ctx, dbgen.RemoveSubCoursePrerequisiteParams{ SubCourseID: subCourseID, PrerequisiteSubCourseID: prerequisiteSubCourseID, }) } func (s *Store) GetSubCoursePrerequisites(ctx context.Context, subCourseID int64) ([]domain.SubCoursePrerequisite, error) { rows, err := s.queries.GetSubCoursePrerequisites(ctx, subCourseID) if err != nil { return nil, err } prereqs := make([]domain.SubCoursePrerequisite, len(rows)) for i, row := range rows { prereqs[i] = domain.SubCoursePrerequisite{ ID: row.ID, SubCourseID: row.SubCourseID, PrerequisiteSubCourseID: row.PrerequisiteSubCourseID, CreatedAt: row.CreatedAt.Time, PrerequisiteTitle: row.PrerequisiteTitle, PrerequisiteLevel: row.PrerequisiteLevel, PrerequisiteDisplayOrder: row.PrerequisiteDisplayOrder, } } return prereqs, nil } func (s *Store) GetSubCourseDependents(ctx context.Context, prerequisiteSubCourseID int64) ([]domain.SubCourseDependent, error) { rows, err := s.queries.GetSubCourseDependents(ctx, prerequisiteSubCourseID) if err != nil { return nil, err } deps := make([]domain.SubCourseDependent, len(rows)) for i, row := range rows { deps[i] = domain.SubCourseDependent{ ID: row.ID, SubCourseID: row.SubCourseID, PrerequisiteSubCourseID: row.PrerequisiteSubCourseID, CreatedAt: row.CreatedAt.Time, DependentTitle: row.DependentTitle, DependentLevel: row.DependentLevel, } } return deps, nil } func (s *Store) CountUnmetPrerequisites(ctx context.Context, subCourseID, userID int64) (int64, error) { return s.queries.CountUnmetPrerequisites(ctx, dbgen.CountUnmetPrerequisitesParams{ SubCourseID: subCourseID, UserID: userID, }) } func (s *Store) DeleteAllPrerequisitesForSubCourse(ctx context.Context, subCourseID int64) error { return s.queries.DeleteAllPrerequisitesForSubCourse(ctx, subCourseID) } func (s *Store) StartSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) { row, err := s.queries.StartSubCourseProgress(ctx, dbgen.StartSubCourseProgressParams{ UserID: userID, SubCourseID: subCourseID, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return s.GetUserSubCourseProgress(ctx, userID, subCourseID) } return domain.UserSubCourseProgress{}, err } return mapUserSubCourseProgress(row), nil } func (s *Store) UpdateSubCourseProgress(ctx context.Context, userID, subCourseID int64, percentage int16) error { return s.queries.UpdateSubCourseProgress(ctx, dbgen.UpdateSubCourseProgressParams{ ProgressPercentage: percentage, UserID: userID, SubCourseID: subCourseID, }) } func (s *Store) CompleteSubCourse(ctx context.Context, userID, subCourseID int64) error { return s.queries.CompleteSubCourse(ctx, dbgen.CompleteSubCourseParams{ UserID: userID, SubCourseID: subCourseID, }) } func (s *Store) RecalculateSubCourseProgress(ctx context.Context, userID, subCourseID int64) error { const query = ` WITH totals AS ( SELECT (SELECT COUNT(*)::INT FROM sub_course_videos v WHERE v.sub_course_id = $2 AND v.status = 'PUBLISHED') AS total_videos, (SELECT COUNT(*)::INT FROM question_sets qs WHERE qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $2 AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED') AS total_practices ), completed AS ( SELECT (SELECT COUNT(*)::INT FROM user_sub_course_video_progress uv JOIN sub_course_videos v ON v.id = uv.video_id WHERE uv.user_id = $1 AND uv.sub_course_id = $2 AND uv.completed_at IS NOT NULL AND v.status = 'PUBLISHED') AS completed_videos, (SELECT COUNT(*)::INT FROM user_practice_progress up JOIN question_sets qs ON qs.id = up.question_set_id WHERE up.user_id = $1 AND up.sub_course_id = $2 AND up.completed_at IS NOT NULL AND qs.owner_type = 'SUB_COURSE' AND qs.owner_id = $2 AND qs.set_type = 'PRACTICE' AND qs.status = 'PUBLISHED') AS completed_practices ), stats AS ( SELECT (total_videos + total_practices) AS total_items, (completed_videos + completed_practices) AS completed_items FROM totals, completed ) INSERT INTO user_sub_course_progress ( user_id, sub_course_id, status, progress_percentage, started_at, completed_at, updated_at ) SELECT $1, $2, CASE WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN 'COMPLETED' WHEN stats.completed_items > 0 THEN 'IN_PROGRESS' ELSE 'NOT_STARTED' END, CASE WHEN stats.total_items = 0 THEN 0 ELSE ROUND((stats.completed_items::NUMERIC * 100.0) / stats.total_items::NUMERIC)::SMALLINT END, CASE WHEN stats.completed_items > 0 THEN CURRENT_TIMESTAMP ELSE NULL END, CASE WHEN stats.total_items > 0 AND stats.completed_items >= stats.total_items THEN CURRENT_TIMESTAMP ELSE NULL END, CURRENT_TIMESTAMP FROM stats ON CONFLICT (user_id, sub_course_id) DO UPDATE SET status = EXCLUDED.status, progress_percentage = EXCLUDED.progress_percentage, started_at = COALESCE(user_sub_course_progress.started_at, EXCLUDED.started_at), completed_at = EXCLUDED.completed_at, updated_at = EXCLUDED.updated_at; ` _, err := s.conn.Exec(ctx, query, userID, subCourseID) return err } func (s *Store) GetUserSubCourseProgress(ctx context.Context, userID, subCourseID int64) (domain.UserSubCourseProgress, error) { row, err := s.queries.GetUserSubCourseProgress(ctx, dbgen.GetUserSubCourseProgressParams{ UserID: userID, SubCourseID: subCourseID, }) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.UserSubCourseProgress{}, domain.ErrProgressNotFound } return domain.UserSubCourseProgress{}, err } return mapUserSubCourseProgress(row), nil } func (s *Store) GetUserCourseProgress(ctx context.Context, userID, courseID int64) ([]domain.UserCourseProgressItem, error) { rows, err := s.queries.GetUserCourseProgress(ctx, dbgen.GetUserCourseProgressParams{ UserID: userID, CourseID: courseID, }) if err != nil { return nil, err } items := make([]domain.UserCourseProgressItem, len(rows)) for i, row := range rows { var startedAt, completedAt *time.Time if row.StartedAt.Valid { startedAt = &row.StartedAt.Time } if row.CompletedAt.Valid { completedAt = &row.CompletedAt.Time } var updatedAt *time.Time if row.UpdatedAt.Valid { updatedAt = &row.UpdatedAt.Time } items[i] = domain.UserCourseProgressItem{ ID: row.ID, UserID: row.UserID, SubCourseID: row.SubCourseID, Status: domain.ProgressStatus(row.Status), ProgressPercentage: row.ProgressPercentage, StartedAt: startedAt, CompletedAt: completedAt, CreatedAt: row.CreatedAt.Time, UpdatedAt: updatedAt, SubCourseTitle: row.SubCourseTitle, SubCourseLevel: row.SubCourseLevel, SubCourseDisplayOrder: row.SubCourseDisplayOrder, } } return items, nil } func (s *Store) GetSubCoursesWithProgressByCourse(ctx context.Context, userID, courseID int64) ([]domain.SubCourseWithProgress, error) { rows, err := s.queries.GetSubCoursesWithProgressByCourse(ctx, dbgen.GetSubCoursesWithProgressByCourseParams{ UserID: userID, CourseID: courseID, }) if err != nil { return nil, err } items := make([]domain.SubCourseWithProgress, len(rows)) for i, row := range rows { var startedAt, completedAt *time.Time if row.StartedAt.Valid { startedAt = &row.StartedAt.Time } if row.CompletedAt.Valid { completedAt = &row.CompletedAt.Time } items[i] = domain.SubCourseWithProgress{ SubCourseID: row.SubCourseID, Title: row.Title, Description: ptrText(row.Description), Thumbnail: ptrText(row.Thumbnail), DisplayOrder: row.DisplayOrder, Level: row.Level, IsActive: row.IsActive, ProgressStatus: domain.ProgressStatus(row.ProgressStatus), ProgressPercentage: row.ProgressPercentage, StartedAt: startedAt, CompletedAt: completedAt, UnmetPrerequisitesCount: row.UnmetPrerequisitesCount, IsLocked: row.UnmetPrerequisitesCount > 0, } } return items, nil } func mapUserSubCourseProgress(row dbgen.UserSubCourseProgress) domain.UserSubCourseProgress { var startedAt, completedAt *time.Time if row.StartedAt.Valid { startedAt = &row.StartedAt.Time } if row.CompletedAt.Valid { completedAt = &row.CompletedAt.Time } var updatedAt *time.Time if row.UpdatedAt.Valid { updatedAt = &row.UpdatedAt.Time } return domain.UserSubCourseProgress{ ID: row.ID, UserID: row.UserID, SubCourseID: row.SubCourseID, Status: domain.ProgressStatus(row.Status), ProgressPercentage: row.ProgressPercentage, StartedAt: startedAt, CompletedAt: completedAt, CreatedAt: row.CreatedAt.Time, UpdatedAt: updatedAt, } }