mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
chore: relocate backend into apps/api ahead of monorepo merge
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* 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,
|
||||
};
|
||||
Reference in New Issue
Block a user