/*********************************************************************************************************************************************************************** * File Name: course_reading_progress.service.js * Type of Program: Service * Description: UPSERT-based progress tracking for user reading activity across the course hierarchy. * * upsertLessonRead — main entry point, called when a user reads a lesson. * UPSERTs the lesson row, then derives and UPSERTs the parent unit and course rows. * All three writes run in a single transaction. * * UPSERT key: (user_id, type, reference_id) * * Derivation rules: * unit → completed when ALL its non-deleted lessons have a completed row for this user * course → completed when ALL its non-deleted units have a completed row for this user * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 21, 2026 ***********************************************************************************************************************************************************************/ 'use strict'; const sequelize = require('../config/db.config'); const CourseReadingProgress = require('../models/courses/course_reading_progress.mdl'); const Lesson = require('../models/courses/lessons.mdl'); const Unit = require('../models/courses/units.mdl'); // ─── Core UPSERT ───────────────────────────────────────────────────────────── async function upsertProgress({ userId, courseId, type, referenceId, status }, t) { const now = new Date(); const [record] = await CourseReadingProgress.upsert( { user_id: userId, course_id: courseId, reference_id: referenceId, type, status, completed_at: status === 'completed' ? now : null, last_accessed_at: now, createdBy: userId, updatedBy: userId, }, { conflictFields: ['user_id', 'type', 'reference_id'], returning: true, transaction: t, } ); return record; } // ─── Derivation helpers ─────────────────────────────────────────────────────── // Unit is completed when every non-deleted lesson under it has a completed row for this user. async function deriveUnitStatus(userId, courseId, unitId, t) { const lessons = await Lesson.findAll({ where: { unit_id: unitId }, attributes: ['uuid'], transaction: t, }); if (!lessons.length) return 'in_progress'; const lessonUuids = lessons.map(l => l.uuid); const completedCount = await CourseReadingProgress.count({ where: { user_id: userId, course_id: courseId, type: 'lesson', reference_id: lessonUuids, status: 'completed', }, transaction: t, }); return completedCount === lessons.length ? 'completed' : 'in_progress'; } // Course is completed when every non-deleted unit under it has a completed row for this user. async function deriveCourseStatus(userId, courseId, t) { const units = await Unit.findAll({ where: { course_id: courseId }, attributes: ['uuid'], transaction: t, }); if (!units.length) return 'in_progress'; const unitUuids = units.map(u => u.uuid); const completedCount = await CourseReadingProgress.count({ where: { user_id: userId, course_id: courseId, type: 'unit', reference_id: unitUuids, status: 'completed', }, transaction: t, }); return completedCount === units.length ? 'completed' : 'in_progress'; } // ─── Main entry point ───────────────────────────────────────────────────────── /** * Called when a user reads (or finishes reading) a lesson. * Writes three rows in one transaction: lesson → unit → course. * * @param {number} userId * @param {Object} payload * @param {number} payload.courseId — course BIGINT PK (FK on course_reading_progress) * @param {string} payload.courseUuid — course UUID (reference_id for the course row) * @param {number} payload.unitId — unit BIGINT PK (used to query sibling lessons) * @param {string} payload.unitUuid — unit UUID (reference_id for the unit row) * @param {string} payload.lessonUuid — lesson UUID (reference_id for the lesson row) * @param {string} payload.lessonStatus — 'in_progress' | 'completed' * @returns {{ lesson, unit, course }} — status snapshot for each level */ async function upsertLessonRead(userId, { courseId, courseUuid, unitId, unitUuid, lessonUuid, lessonStatus = 'in_progress' }) { const t = await sequelize.transaction(); try { // 1. Lesson await upsertProgress({ userId, courseId, type: 'lesson', referenceId: lessonUuid, status: lessonStatus, }, t); // 2. Unit — derived from all sibling lessons const unitStatus = await deriveUnitStatus(userId, courseId, unitId, t); await upsertProgress({ userId, courseId, type: 'unit', referenceId: unitUuid, status: unitStatus, }, t); // 3. Course — derived from all units const courseStatus = await deriveCourseStatus(userId, courseId, t); await upsertProgress({ userId, courseId, type: 'course', referenceId: courseUuid, status: courseStatus, }, t); await t.commit(); return { lesson: { reference_id: lessonUuid, status: lessonStatus }, unit: { reference_id: unitUuid, status: unitStatus }, course: { reference_id: courseUuid, status: courseStatus }, }; } catch (err) { await t.rollback(); throw err; } } // ─── Exports ────────────────────────────────────────────────────────────────── module.exports = { upsertLessonRead, upsertProgress, deriveUnitStatus, deriveCourseStatus, };