/*********************************************************************************************************************************************************************** * File Name: task_progress.mdl.js * Type of Program: Model * Description: Sequelize models for tracking user progress on task requirements. * * TaskLinkVisit — tracks when a user visits a visit_link requirement. * UPSERT key: (requirement_id, user_id) * Revisiting updates visited_at. * * TaskProgress — tracks completion of read_course / read_unit / read_lesson. * UPSERT key: (requirement_id, user_id, reference_id) * Lessons: completed flipped directly by client. * Units/Courses: completed derived from child completion. * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jun. 13, 2026 ***********************************************************************************************************************************************************************/ const { DataTypes } = require('sequelize'); const sequelize = require('../../config/db.config'); const mdl_Users = require('../users/users.mdl'); const { Task, TaskRequirement } = require('./task.mdl'); // ─── TaskLinkVisit ───────────────────────────────────────────────────────────── const TaskLinkVisit = sequelize.define('TaskLinkVisit', { visit_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE', }, requirement_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_requirements', key: 'requirement_id' }, onDelete: 'CASCADE', }, user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE', }, visited_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW, allowNull: false, comment: 'Updated on every revisit via UPSERT.', }, // ── Audit trails ──────────────────────────────────────────────────────── createdBy: { type: DataTypes.BIGINT, allowNull: true }, updatedBy: { type: DataTypes.BIGINT, allowNull: true }, }, { tableName: 'task_link_visits', timestamps: true, paranoid: false, // visits are never soft-deleted indexes: [ { unique: true, fields: ['requirement_id', 'user_id'], name: 'uq_tlv_requirement_user', }, { fields: ['task_id'], name: 'idx_tlv_task_id' }, { fields: ['user_id'], name: 'idx_tlv_user_id' }, { fields: ['requirement_id'], name: 'idx_tlv_requirement_id' }, ], }); // ─── TaskProgress ────────────────────────────────────────────────────────────── const TaskProgress = sequelize.define('TaskProgress', { progress_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, task_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'tasks', key: 'task_id' }, onDelete: 'CASCADE', }, requirement_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'task_requirements', key: 'requirement_id' }, onDelete: 'CASCADE', }, user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE', }, reference_id: { type: DataTypes.UUID, allowNull: false, comment: 'course_id | unit_id | lesson_id | quiz_id depending on type.', }, type: { type: DataTypes.ENUM('read_course', 'read_unit', 'read_lesson', 'pass_quiz'), allowNull: false, }, completed: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, }, completed_at: { type: DataTypes.DATE, allowNull: true, comment: 'Set when completed flips true. Cleared when it reverts to false.', }, // ── Audit trails ──────────────────────────────────────────────────────── createdBy: { type: DataTypes.BIGINT, allowNull: true }, updatedBy: { type: DataTypes.BIGINT, allowNull: true }, }, { tableName: 'task_progress', timestamps: true, paranoid: false, // progress rows are never soft-deleted indexes: [ { unique: true, fields: ['requirement_id', 'user_id', 'reference_id'], name: 'uq_tp_requirement_user_reference', }, { fields: ['task_id'], name: 'idx_tp_task_id' }, { fields: ['user_id'], name: 'idx_tp_user_id' }, { fields: ['requirement_id'], name: 'idx_tp_requirement_id' }, { fields: ['type'], name: 'idx_tp_type' }, ], }); // ─── Associations ────────────────────────────────────────────────────────────── Task.hasMany(TaskLinkVisit, { foreignKey: 'task_id', as: 'linkVisits' }); TaskLinkVisit.belongsTo(Task, { foreignKey: 'task_id', as: 'task' }); TaskRequirement.hasMany(TaskLinkVisit, { foreignKey: 'requirement_id', as: 'visits' }); TaskLinkVisit.belongsTo(TaskRequirement, { foreignKey: 'requirement_id', as: 'requirement' }); mdl_Users.hasMany(TaskLinkVisit, { foreignKey: 'user_id', as: 'linkVisits' }); TaskLinkVisit.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' }); Task.hasMany(TaskProgress, { foreignKey: 'task_id', as: 'progress' }); TaskProgress.belongsTo(Task, { foreignKey: 'task_id', as: 'task' }); TaskRequirement.hasMany(TaskProgress, { foreignKey: 'requirement_id', as: 'progress' }); TaskProgress.belongsTo(TaskRequirement, { foreignKey: 'requirement_id', as: 'requirement' }); mdl_Users.hasMany(TaskProgress, { foreignKey: 'user_id', as: 'taskProgress' }); TaskProgress.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' }); module.exports = { TaskLinkVisit, TaskProgress };