Files
starr-philproperties/services/task_reading_progress_sync.service.js
T
kennethobsequio 1d12f04967 new commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-03 16:20:58 +08:00

139 lines
4.7 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name: task_reading_progress_sync.service.js
* Type of Program: Service
* Description: Backfills task_progress for read_* task requirements from course_reading_progress.
*
* This covers the case where a user already completed reading a course/unit/lesson
* before a task requiring that item was created or assigned.
***********************************************************************************************************************************************************************/
'use strict';
const { Op } = require('sequelize');
const CourseReadingProgress = require('../models/courses/course_reading_progress.mdl');
const { TaskProgress } = require('../models/task/task_progress.mdl');
const READ_TYPE_TO_PROGRESS_TYPE = {
read_course: 'course',
read_unit: 'unit',
read_lesson: 'lesson',
};
const READ_REQUIREMENT_TYPES = Object.keys(READ_TYPE_TO_PROGRESS_TYPE);
function readAttr(row, attr) {
if (!row) return undefined;
if (typeof row.get === 'function') return row.get(attr);
return row[attr];
}
function normalizeRequirement(row) {
const type = readAttr(row, 'type');
if (!READ_REQUIREMENT_TYPES.includes(type)) return null;
const referenceId = readAttr(row, 'reference_id');
if (!referenceId) return null;
return {
task_id: readAttr(row, 'task_id'),
requirement_id: readAttr(row, 'requirement_id'),
reference_id: referenceId,
type,
};
}
async function hydrateReadTaskProgress(userId, requirements = [], options = {}) {
const readRequirements = requirements
.map(normalizeRequirement)
.filter((req) => req && req.task_id && req.requirement_id);
if (!readRequirements.length) return [];
const referencesByProgressType = readRequirements.reduce((acc, req) => {
const progressType = READ_TYPE_TO_PROGRESS_TYPE[req.type];
if (!acc[progressType]) acc[progressType] = new Set();
acc[progressType].add(req.reference_id);
return acc;
}, {});
const where = {
user_id: userId,
status: 'completed',
[Op.or]: Object.entries(referencesByProgressType).map(([type, references]) => ({
type,
reference_id: { [Op.in]: [...references] },
})),
};
const completedReadingRows = await CourseReadingProgress.findAll({
where,
attributes: ['type', 'reference_id', 'completed_at'],
transaction: options.transaction,
});
if (!completedReadingRows.length) return [];
const completedReading = new Map(
completedReadingRows.map((row) => [
`${readAttr(row, 'type')}:${readAttr(row, 'reference_id')}`,
readAttr(row, 'completed_at'),
])
);
const now = new Date();
const rowsToUpsert = readRequirements.filter((req) =>
completedReading.has(`${READ_TYPE_TO_PROGRESS_TYPE[req.type]}:${req.reference_id}`)
);
if (!rowsToUpsert.length) return [];
const existingProgressRows = await TaskProgress.findAll({
where: {
user_id: userId,
completed: true,
requirement_id: { [Op.in]: rowsToUpsert.map((req) => req.requirement_id) },
reference_id: { [Op.in]: rowsToUpsert.map((req) => req.reference_id) },
},
attributes: ['requirement_id', 'reference_id'],
transaction: options.transaction,
});
const existingProgress = new Set(
existingProgressRows.map((row) =>
`${readAttr(row, 'requirement_id')}:${readAttr(row, 'reference_id')}`
)
);
const missingRows = rowsToUpsert.filter((req) =>
!existingProgress.has(`${req.requirement_id}:${req.reference_id}`)
);
if (!missingRows.length) return [];
await Promise.all(missingRows.map((req) =>
TaskProgress.upsert(
{
task_id: req.task_id,
requirement_id: req.requirement_id,
user_id: userId,
reference_id: req.reference_id,
type: req.type,
completed: true,
completed_at: completedReading.get(`${READ_TYPE_TO_PROGRESS_TYPE[req.type]}:${req.reference_id}`) ?? now,
createdBy: userId,
updatedBy: userId,
},
{
conflictFields: ['requirement_id', 'user_id', 'reference_id'],
transaction: options.transaction,
}
)
));
return missingRows;
}
module.exports = {
hydrateReadTaskProgress,
READ_REQUIREMENT_TYPES,
};