/*********************************************************************************************************************************************************************** * File Name: completion_requirement_progress.mdl.js * Type of Program: Model * Description: Per-user tracking state for a CompletionRequirement, for the requirement types that need * genuinely new storage (watch_percent's running progress_percent, watch_video/listen_audio's * per-block progress_percent map, and manual_complete's completed flag — pass_quiz reads * QuizAttempt.passed directly, and read_all_content reads CourseReadingProgress directly; * neither needs a row here). * * CompletionRequirementProgress — UPSERT key: (requirement_id, user_id). * * Author: Kenneth Obsequio (@lash0000) * Date Created: Jul. 14, 2026 ***********************************************************************************************************************************************************************/ const { DataTypes } = require('sequelize'); const sequelize = require('../../config/db.config'); const CompletionRequirementProgress = sequelize.define('CompletionRequirementProgress', { progress_id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true, }, requirement_id: { type: DataTypes.UUID, allowNull: false, references: { model: 'completion_requirements', key: 'requirement_id' }, onDelete: 'CASCADE', }, user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: 'users', key: 'user_id' }, onDelete: 'CASCADE', }, entity_type: { type: DataTypes.ENUM('course', 'unit', 'lesson'), allowNull: false, comment: 'Denormalized from the requirement row to avoid a join on every read.', }, entity_id: { type: DataTypes.BIGINT, allowNull: false, comment: 'Denormalized from the requirement row.', }, progress_percent: { type: DataTypes.INTEGER, allowNull: true, comment: 'watch_percent only — running max % watched.', }, block_progress: { type: DataTypes.JSONB, allowNull: true, comment: 'watch_percent/watch_video/listen_audio — { [block_id]: { percent, updatedAt } } running max + last-sample timestamp per block. The timestamp lets recordWatchProgress validate a reported percent against real elapsed wall-clock time (anti-skip). watch_video/listen_audio: completed once every current block of that type is at 100. watch_percent: also keeps progress_percent as its aggregate max across whichever block reports.', }, completed: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, }, completed_at: { type: DataTypes.DATE, allowNull: true, }, // ── Audit trails ──────────────────────────────────────────────────────── createdBy: { type: DataTypes.BIGINT, allowNull: true }, updatedBy: { type: DataTypes.BIGINT, allowNull: true }, }, { tableName: 'completion_requirement_progress', timestamps: true, paranoid: false, // progress rows are never soft-deleted, matches CourseReadingProgress indexes: [ { unique: true, fields: ['requirement_id', 'user_id'], name: 'uq_crp2_requirement_user', }, { fields: ['user_id'], name: 'idx_crp2_user_id' }, { fields: ['entity_type', 'entity_id'], name: 'idx_crp2_entity_type_entity_id' }, ], }); module.exports = CompletionRequirementProgress;