Files

166 lines
6.3 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: reading_progress.service.js
* Type of Program: Service
* Description: UPSERT-based progress tracking for unit and lesson reading activity.
*
* upsertLessonRead — main entry point, called when a user reads a lesson.
* UPSERTs the lesson row in lesson_reading_progress, then derives and
* UPSERTs the parent unit row in unit_reading_progress.
* Both writes run in a single transaction.
*
* UPSERT keys:
* lesson_reading_progress → (user_id, lesson_id)
* unit_reading_progress → (user_id, unit_id)
*
* Derivation rule:
* unit → completed when ALL non-deleted lessons under it have a completed row for this user
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 26, 2026
***********************************************************************************************************************************************************************/
'use strict';
const sequelize = require('../config/db.config');
const LessonReadingProgress = require('../models/courses/lesson_reading_progress.mdl');
const UnitReadingProgress = require('../models/courses/unit_reading_progress.mdl');
const UnitLesson = require('../models/courses/unit_lessons.mdl');
const Lesson = require('../models/courses/lessons.mdl');
// ─── Core UPSERTs ────────────────────────────────────────────────────────────
async function upsertLessonProgress({ userId, courseId, unitId, lessonId, status }, t) {
const now = new Date();
const [record] = await LessonReadingProgress.upsert(
{
user_id: userId,
course_id: courseId ?? null, // NULL when read standalone
unit_id: unitId ?? null, // NULL when read standalone
lesson_id: lessonId,
status,
completed_at: status === 'completed' ? now : null,
last_accessed_at: now,
createdBy: userId,
updatedBy: userId,
},
{
conflictFields: ['user_id', 'lesson_id'],
returning: true,
transaction: t,
}
);
return record;
}
async function upsertUnitProgress({ userId, courseId, unitId, status }, t) {
const now = new Date();
const [record] = await UnitReadingProgress.upsert(
{
user_id: userId,
course_id: courseId ?? null, // NULL when read standalone
unit_id: unitId,
status,
completed_at: status === 'completed' ? now : null,
last_accessed_at: now,
createdBy: userId,
updatedBy: userId,
},
{
conflictFields: ['user_id', 'unit_id'],
returning: true,
transaction: t,
}
);
return record;
}
// ─── Derivation helper ────────────────────────────────────────────────────────
// Unit is completed when every non-deleted lesson attached to it (via unit_lessons)
// has a completed row for this user.
async function deriveUnitStatus(userId, unitId, t) {
const links = await UnitLesson.findAll({
where: { unit_id: unitId },
attributes: ['lesson_id'],
transaction: t,
});
if (!links.length) return 'in_progress';
const lessons = await Lesson.findAll({
where: { lesson_id: links.map(l => l.lesson_id) },
attributes: ['lesson_id'],
transaction: t,
});
if (!lessons.length) return 'in_progress';
const lessonIds = lessons.map(l => l.lesson_id);
const completedCount = await LessonReadingProgress.count({
where: {
user_id: userId,
lesson_id: lessonIds,
status: 'completed',
},
transaction: t,
});
return completedCount === lessons.length ? 'completed' : 'in_progress';
}
// ─── Main entry point ─────────────────────────────────────────────────────────
/**
* Called when a user reads (or finishes reading) a lesson.
* Writes two rows in one transaction: lesson → unit.
*
* @param {number} userId
* @param {Object} payload
* @param {number} payload.courseId — course BIGINT PK
* @param {number} payload.unitId — unit BIGINT PK
* @param {number} payload.lessonId — lesson BIGINT PK
* @param {string} payload.lessonStatus — 'in_progress' | 'completed'
* @returns {{ lesson, unit }} — status snapshot for each level
*/
async function upsertLessonRead(userId, { courseId = null, unitId = null, lessonId, lessonStatus = 'in_progress' }) {
const t = await sequelize.transaction();
try {
// 1. Lesson
await upsertLessonProgress({
userId,
courseId,
unitId,
lessonId,
status: lessonStatus,
}, t);
// 2. Unit — derived from all sibling lessons (skipped for standalone lesson reads)
let unitStatus = null;
if (unitId) {
unitStatus = await deriveUnitStatus(userId, unitId, t);
await upsertUnitProgress({
userId,
courseId,
unitId,
status: unitStatus,
}, t);
}
await t.commit();
return {
lesson: { lesson_id: lessonId, status: lessonStatus },
unit: unitId ? { unit_id: unitId, status: unitStatus } : null,
};
} catch (err) {
await t.rollback();
throw err;
}
}
// ─── Exports ──────────────────────────────────────────────────────────────────
module.exports = {
upsertLessonRead,
upsertLessonProgress,
upsertUnitProgress,
deriveUnitStatus,
};