added new requirements for lessons and units

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-15 16:26:09 +08:00
parent b7d62b3b18
commit 9c82b0de09
25 changed files with 1569 additions and 233 deletions
@@ -0,0 +1,60 @@
/***********************************************************************************************************************************************************************
* File Name: completion_requirement.mdl.js
* Type of Program: Model
* Description: Admin-configured rule describing what "complete" means for a course, unit, or lesson.
*
* CompletionRequirement — polymorphic via (entity_type, entity_id); entity_id is the BIGINT PK of the
* owning courses/units/lessons row (not a UUID reference like TaskRequirement —
* this row is OWNED by the entity, not pointing at an arbitrary target).
* type dispatches through utils/courses/completion_requirements.registry.js.
* Multiple rows on one entity are AND'd together by services/courses/completion.service.js.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 14, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const CompletionRequirement = sequelize.define('CompletionRequirement', {
requirement_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
entity_type: {
type: DataTypes.ENUM('course', 'unit', 'lesson'),
allowNull: false,
filterable: true,
},
entity_id: {
type: DataTypes.BIGINT,
allowNull: false,
comment: 'course_id | unit_id | lesson_id depending on entity_type. No DB-level FK (polymorphic across 3 tables).',
filterable: true,
},
type: {
type: DataTypes.ENUM('read_all_content', 'pass_quiz', 'watch_percent', 'manual_complete'),
allowNull: false,
filterable: true,
},
// ── watch_percent ───────────────────────────────────────────────────────
min_percent: { type: DataTypes.INTEGER, allowNull: true, comment: 'watch_percent only — minimum % of media watched.', filterable: false },
// ── manual_complete ─────────────────────────────────────────────────────
button_label: { type: DataTypes.STRING, allowNull: true, comment: 'manual_complete only — optional custom CTA text.', filterable: false },
is_required: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, filterable: true },
order: { type: DataTypes.INTEGER, defaultValue: 0, filterable: true },
// ── Audit trails ────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true },
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
deletedBy: { type: DataTypes.BIGINT, allowNull: true },
}, {
tableName: 'completion_requirements',
paranoid: true,
timestamps: true,
});
module.exports = CompletionRequirement;
@@ -0,0 +1,84 @@
/***********************************************************************************************************************************************************************
* 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_video/listen_audio only — { [block_id]: percent } running max per matching block; completed once every current block of that type is at 100.',
},
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;
+11
View File
@@ -24,6 +24,8 @@ const CourseReadingProgress = require("./course_reading_progress.mdl");
const UnitReadingProgress = require("./unit_reading_progress.mdl");
const LessonReadingProgress = require("./lesson_reading_progress.mdl");
const CourseAchievement = require("./course_achievement.mdl");
const CompletionRequirement = require("./completion_requirement.mdl");
const CompletionRequirementProgress = require("./completion_requirement_progress.mdl");
// ── CourseReadingProgress ─────────────────────────────────────────────────────
CourseReadingProgress.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
@@ -46,6 +48,14 @@ LessonReadingProgress.belongsTo(Lesson, { foreignKey: 'lesson_id', as: 'lesso
mdl_Users.hasMany(LessonReadingProgress, { foreignKey: 'user_id', as: 'lessonReadingProgress' });
Lesson.hasMany(LessonReadingProgress, { foreignKey: 'lesson_id', as: 'readingProgress' });
// ── CompletionRequirement ─────────────────────────────────────────────────────
// Polymorphic across course/unit/lesson via (entity_type, entity_id) — no belongsTo
// to Course/Unit/Lesson here (can't express a 3-way FK); entity resolution happens
// in services/courses/completion.service.js.
CompletionRequirement.hasMany(CompletionRequirementProgress, { foreignKey: 'requirement_id', as: 'progress' });
CompletionRequirementProgress.belongsTo(CompletionRequirement, { foreignKey: 'requirement_id', as: 'requirement' });
CompletionRequirementProgress.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
// ── Course ⇄ Unit / Unit ⇄ Lesson (junctions) ─────────────────────────────────
// Units and Lessons are standalone entities. Membership + ordering live on the
// course_units / unit_lessons junction rows (order_index).
@@ -138,4 +148,5 @@ module.exports = {
mdl_Category, Certificate, CourseInstructor,
CourseReadingProgress, UnitReadingProgress, LessonReadingProgress,
CourseAchievement,
CompletionRequirement, CompletionRequirementProgress,
};