refactor: extract lesson completion logic into shared markLessonComplete method and add course progress tracking to video and quiz responses
This commit is contained in:
parent
ab560809c7
commit
18816c4fb2
2 changed files with 129 additions and 83 deletions
|
|
@ -28,6 +28,104 @@ import { getPresignedUrl, listObjects, getVideoFolder, getAttachmentsFolder } fr
|
|||
|
||||
|
||||
export class CoursesStudentService {
|
||||
/**
|
||||
* Mark lesson as complete and update enrollment progress
|
||||
* Shared function for submitQuiz, saveVideoProgress, completeLesson
|
||||
*/
|
||||
private async markLessonComplete(userId: number, lessonId: number, courseId: number): Promise<{
|
||||
lessonProgress: { lesson_id: number; is_completed: boolean; completed_at: Date | null };
|
||||
enrollmentProgress: { progress_percentage: number; is_course_completed: boolean };
|
||||
}> {
|
||||
const now = new Date();
|
||||
|
||||
// Upsert lesson progress
|
||||
const lessonProgress = await prisma.lessonProgress.upsert({
|
||||
where: {
|
||||
user_id_lesson_id: {
|
||||
user_id: userId,
|
||||
lesson_id: lessonId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
user_id: userId,
|
||||
lesson_id: lessonId,
|
||||
is_completed: true,
|
||||
completed_at: now,
|
||||
},
|
||||
update: {
|
||||
is_completed: true,
|
||||
completed_at: now,
|
||||
},
|
||||
});
|
||||
|
||||
// Get all lessons in the course
|
||||
const course = await prisma.course.findUnique({
|
||||
where: { id: courseId },
|
||||
include: {
|
||||
chapters: {
|
||||
include: {
|
||||
lessons: {
|
||||
where: { is_published: true },
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!course) {
|
||||
throw new NotFoundError('Course not found');
|
||||
}
|
||||
|
||||
const allLessonIds = course.chapters.flatMap(ch => ch.lessons.map(l => l.id));
|
||||
const totalLessons = allLessonIds.length;
|
||||
|
||||
// Count completed lessons
|
||||
const completedLessons = await prisma.lessonProgress.count({
|
||||
where: {
|
||||
user_id: userId,
|
||||
lesson_id: { in: allLessonIds },
|
||||
is_completed: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate progress percentage
|
||||
const progressPercentage = totalLessons > 0
|
||||
? Math.round((completedLessons / totalLessons) * 100)
|
||||
: 0;
|
||||
|
||||
const isCourseCompleted = completedLessons >= totalLessons;
|
||||
|
||||
// Update enrollment
|
||||
await prisma.enrollment.update({
|
||||
where: {
|
||||
unique_enrollment: {
|
||||
user_id: userId,
|
||||
course_id: courseId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
progress_percentage: progressPercentage,
|
||||
...(isCourseCompleted ? {
|
||||
status: 'COMPLETED',
|
||||
completed_at: now,
|
||||
} : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
lessonProgress: {
|
||||
lesson_id: lessonProgress.lesson_id,
|
||||
is_completed: lessonProgress.is_completed,
|
||||
completed_at: lessonProgress.completed_at,
|
||||
},
|
||||
enrollmentProgress: {
|
||||
progress_percentage: progressPercentage,
|
||||
is_course_completed: isCourseCompleted,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async enrollCourse(input: EnrollCourseInput): Promise<EnrollCourseResponse> {
|
||||
try {
|
||||
const { course_id } = input;
|
||||
|
|
@ -810,10 +908,11 @@ export class CoursesStudentService {
|
|||
? (video_progress_seconds / video_duration_seconds) * 100
|
||||
: null;
|
||||
|
||||
// Auto-complete at >= 90%
|
||||
const isCompleted = progressPercentage !== null && progressPercentage >= 90;
|
||||
// Auto-complete at >= 90% OR when video_progress_seconds >= video_duration_seconds
|
||||
const isCompleted = (progressPercentage !== null && progressPercentage >= 90) ||
|
||||
!!(video_duration_seconds && video_progress_seconds >= video_duration_seconds);
|
||||
|
||||
// Save progress
|
||||
// Save video progress (without marking complete yet)
|
||||
const progress = await prisma.lessonProgress.upsert({
|
||||
where: {
|
||||
user_id_lesson_id: {
|
||||
|
|
@ -827,21 +926,24 @@ export class CoursesStudentService {
|
|||
video_progress_seconds,
|
||||
video_duration_seconds: video_duration_seconds ?? null,
|
||||
video_progress_percentage: progressPercentage,
|
||||
is_completed: isCompleted,
|
||||
completed_at: isCompleted ? new Date() : null,
|
||||
is_completed: false,
|
||||
last_watched_at: new Date(),
|
||||
},
|
||||
update: {
|
||||
video_progress_seconds,
|
||||
video_duration_seconds: video_duration_seconds ?? null,
|
||||
video_progress_percentage: progressPercentage,
|
||||
// Only set completed if not already completed
|
||||
is_completed: isCompleted ? true : undefined,
|
||||
completed_at: isCompleted ? new Date() : undefined,
|
||||
last_watched_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// If video completed, mark lesson as complete and update enrollment progress
|
||||
let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined;
|
||||
if (isCompleted) {
|
||||
const result = await this.markLessonComplete(decoded.id, lesson_id, course_id);
|
||||
enrollmentProgress = result.enrollmentProgress;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
message: 'Video progress saved successfully',
|
||||
|
|
@ -850,8 +952,10 @@ export class CoursesStudentService {
|
|||
video_progress_seconds: progress.video_progress_seconds,
|
||||
video_duration_seconds: progress.video_duration_seconds,
|
||||
video_progress_percentage: progress.video_progress_percentage ? Number(progress.video_progress_percentage) : null,
|
||||
is_completed: progress.is_completed,
|
||||
is_completed: isCompleted || progress.is_completed,
|
||||
last_watched_at: progress.last_watched_at!,
|
||||
course_progress_percentage: enrollmentProgress?.progress_percentage,
|
||||
is_course_completed: enrollmentProgress?.is_course_completed,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
@ -922,46 +1026,9 @@ export class CoursesStudentService {
|
|||
throw new ForbiddenError('You are not enrolled in this course');
|
||||
}
|
||||
|
||||
// Complete lesson
|
||||
const progress = await prisma.lessonProgress.upsert({
|
||||
where: {
|
||||
user_id_lesson_id: {
|
||||
user_id: decoded.id,
|
||||
lesson_id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
user_id: decoded.id,
|
||||
lesson_id,
|
||||
is_completed: true,
|
||||
completed_at: new Date(),
|
||||
},
|
||||
update: {
|
||||
is_completed: true,
|
||||
completed_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
// Get all lesson IDs in the course
|
||||
const allLessons = lesson.chapter.course.chapters.flatMap(ch => ch.lessons.map(l => l.id));
|
||||
const totalLessons = allLessons.length;
|
||||
|
||||
// Get completed lessons count
|
||||
const completedLessons = await prisma.lessonProgress.count({
|
||||
where: {
|
||||
user_id: decoded.id,
|
||||
lesson_id: { in: allLessons },
|
||||
is_completed: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Calculate course progress percentage
|
||||
const course_progress_percentage = totalLessons > 0
|
||||
? Math.round((completedLessons / totalLessons) * 100)
|
||||
: 0;
|
||||
|
||||
// Check if course is completed
|
||||
const is_course_completed = completedLessons >= totalLessons;
|
||||
// Mark lesson as complete and update enrollment progress
|
||||
const { lessonProgress, enrollmentProgress } = await this.markLessonComplete(decoded.id, lesson_id, course_id);
|
||||
const { progress_percentage: course_progress_percentage, is_course_completed } = enrollmentProgress;
|
||||
|
||||
// Find next lesson
|
||||
const currentChapterLessons = lesson.chapter.lessons;
|
||||
|
|
@ -983,18 +1050,6 @@ export class CoursesStudentService {
|
|||
}
|
||||
}
|
||||
|
||||
// Update enrollment progress
|
||||
await prisma.enrollment.update({
|
||||
where: { id: enrollment.id },
|
||||
data: {
|
||||
progress_percentage: course_progress_percentage,
|
||||
...(is_course_completed ? {
|
||||
status: 'COMPLETED',
|
||||
completed_at: new Date(),
|
||||
} : {}),
|
||||
},
|
||||
});
|
||||
|
||||
// Issue certificate if course completed and has certificate
|
||||
let certificate_issued: boolean | undefined;
|
||||
if (is_course_completed && lesson.chapter.course.have_certificate) {
|
||||
|
|
@ -1026,9 +1081,9 @@ export class CoursesStudentService {
|
|||
code: 200,
|
||||
message: 'Lesson completed successfully',
|
||||
data: {
|
||||
lesson_id: progress.lesson_id,
|
||||
is_completed: progress.is_completed,
|
||||
completed_at: progress.completed_at!,
|
||||
lesson_id: lessonProgress.lesson_id,
|
||||
is_completed: lessonProgress.is_completed,
|
||||
completed_at: lessonProgress.completed_at!,
|
||||
course_progress_percentage,
|
||||
is_course_completed,
|
||||
next_lesson_id,
|
||||
|
|
@ -1169,26 +1224,11 @@ export class CoursesStudentService {
|
|||
},
|
||||
});
|
||||
|
||||
// If passed, upsert lesson_progress to mark lesson as completed
|
||||
// If passed, mark lesson as complete and update enrollment progress
|
||||
let enrollmentProgress: { progress_percentage: number; is_course_completed: boolean } | undefined;
|
||||
if (isPassed) {
|
||||
await prisma.lessonProgress.upsert({
|
||||
where: {
|
||||
user_id_lesson_id: {
|
||||
user_id: decoded.id,
|
||||
lesson_id,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
user_id: decoded.id,
|
||||
lesson_id,
|
||||
is_completed: true,
|
||||
completed_at: now,
|
||||
},
|
||||
update: {
|
||||
is_completed: true,
|
||||
completed_at: now,
|
||||
},
|
||||
});
|
||||
const result = await this.markLessonComplete(decoded.id, lesson_id, course_id);
|
||||
enrollmentProgress = result.enrollmentProgress;
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -1207,6 +1247,8 @@ export class CoursesStudentService {
|
|||
started_at: quizAttempt.started_at,
|
||||
completed_at: quizAttempt.completed_at!,
|
||||
answers_review: quiz.show_answers_after_completion ? answersReview : undefined,
|
||||
course_progress_percentage: enrollmentProgress?.progress_percentage,
|
||||
is_course_completed: enrollmentProgress?.is_course_completed,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -242,6 +242,8 @@ export interface SaveVideoProgressResponse {
|
|||
video_progress_percentage: number | null;
|
||||
is_completed: boolean;
|
||||
last_watched_at: Date;
|
||||
course_progress_percentage?: number;
|
||||
is_course_completed?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -362,5 +364,7 @@ export interface SubmitQuizResponse {
|
|||
is_correct: boolean;
|
||||
score: number;
|
||||
}[];
|
||||
course_progress_percentage?: number;
|
||||
is_course_completed?: boolean;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue