/*********************************************************************************************************************************************************************** * 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, };