ready to test

Testing

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-22 10:06:58 +08:00
parent bf48c95467
commit 439bb33f77
189 changed files with 17559 additions and 686 deletions
@@ -0,0 +1,58 @@
// models/advertisements/advertisements.attributes.js
// ─── Exclude sets ─────────────────────────────────────────────────────────────
// Fields hidden from all roles (internal/denormalized details)
const excludeAttributes = [
"image_asset_id", // internal FK, "image" association exposes the resolved asset instead
];
// Admins see everything except the base excludes
const adminExclude = [
...excludeAttributes,
];
// Regular users also cannot see audit trails, soft-delete info, or click metrics
const userExclude = [
...excludeAttributes,
"click_count",
"createdBy",
"updatedBy",
"deletedBy",
"deletedAt",
];
// ─── JSONB schemas ──────────────────────────────────────────────────────────
// Describes shape of the `ctas` JSONB column for paginate's coercion/validation step.
const jsonbSchemas = {
ctas: {
type: "array",
itemShape: {
label: "string",
link: "string",
variant: "string", // "default" | "outline"
},
},
};
// ─── Computed attributes ──────────────────────────────────────────────────────
// Add any SQL-computed fields here (e.g. a "is_currently_live" derived flag).
// Format: { key, label, type, order, literal }
const computedAttributes = [
// Example:
// {
// key: "is_live",
// label: "Currently Live",
// type: "boolean",
// order: 99,
// literal: `("Advertisement"."is_active" = true AND "Advertisement"."start_date" <= NOW() AND ("Advertisement"."end_date" IS NULL OR "Advertisement"."end_date" >= NOW()))`,
// },
];
module.exports = {
excludeAttributes,
adminExclude,
userExclude,
jsonbSchemas,
computedAttributes,
};
@@ -0,0 +1,71 @@
// models/advertisements/advertisements.mdl.js
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const mdl_Users = require("../users/users.mdl");
const mdl_Assets = require("../assets/assets.mdl");
const Advertisement = sequelize.define("Advertisement", {
// ─── Identity ─────────────────────────────────────────────────────────────
advertisement_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Advertisement ID", order: 0, hidden: true },
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, label: "UUID", order: 0, hidden: true },
// ─── Identification / placement ──────────────────────────────────────────
type: {
type: DataTypes.ENUM("hero", "banner", "popup", "sidebar"),
allowNull: false,
label: "Type", order: 1
},
status: {
type: DataTypes.ENUM("draft", "active", "scheduled", "expired", "archived"),
allowNull: false,
defaultValue: "draft", label: "Status", order: 2
},
// ─── Content ──────────────────────────────────────────────────────────────
badge_label: { type: DataTypes.STRING(100), label: "Badge Label", order: 3 },
headline: { type: DataTypes.STRING(255), label: "Headline", order: 4 },
description: { type: DataTypes.TEXT, label: "Description", order: 5 },
// ─── Media ────────────────────────────────────────────────────────────────
image_url: { type: DataTypes.STRING(512), label: "Image URL", order: 0, hidden: true },
image_asset_id: { type: DataTypes.BIGINT, allowNull: true, label: "Image Asset", order: 0, hidden: true },
// ─── Calls-to-action ──────────────────────────────────────────────────────
// [{ label, link }, ...] — 0-2 entries depending on type
ctas: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "CTAs", order: 6 },
// ─── Scheduling ───────────────────────────────────────────────────────────
start_date: { type: DataTypes.DATE, label: "Start Date", order: 7 },
end_date: { type: DataTypes.DATE, label: "End Date", order: 8 },
// ─── Display behavior ─────────────────────────────────────────────────────
order: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: "order", label: "Order", order: 9 },
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: "Active", order: 10 },
size: { type: DataTypes.ENUM("sm", "md", "lg"), allowNull: true, label: "Size", order: 11 }, // banner-only, ignored by other types
// ─── Metrics ──────────────────────────────────────────────────────────────
click_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Clicks", order: 12, hidden: true },
// ─── Audit trails ─────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
}, {
tableName: "advertisements",
timestamps: true, // createdAt, updatedAt
paranoid: true,
indexes: [
{ fields: ["uuid"] },
{ fields: ["type"] },
{ fields: ["status"] },
{ fields: ["is_active"] },
{ fields: ["deletedAt"] },
],
});
Advertisement.belongsTo(mdl_Assets, { as: "image", foreignKey: "image_asset_id" });
Advertisement.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
Advertisement.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
module.exports = Advertisement;
+2 -1
View File
@@ -36,7 +36,8 @@ const Asset = sequelize.define("Asset", {
bitrate: { type: DataTypes.BIGINT, label: "", order: 0, hidden: true }, // bps
video_codec: { type: DataTypes.STRING(50), label: "", order: 0, hidden: true }, // "H.264", "H.265"
audio_codec: { type: DataTypes.STRING(50), label: "", order: 0, hidden: true }, // "AAC", "MP3"
thumbnail_url: { type: DataTypes.STRING(512), label: "", order: 0, hidden: true }, // face of the video / doc preview
thumbnail_url: { type: DataTypes.STRING(512), label: "", order: 0, hidden: true },
thumbnail_storage_key: { type: DataTypes.STRING(512), label: "", order: 0, hidden: true },
// ─── Description ──────────────────────────────────────────────────────────
description: { type: DataTypes.TEXT, label: "Description", hidden: true },
+17
View File
@@ -0,0 +1,17 @@
'use strict';
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_Category = sequelize.define('Category', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING(100), allowNull: false, unique: true },
slug: { type: DataTypes.STRING(120), allowNull: false, unique: true },
description: { type: DataTypes.TEXT, allowNull: true },
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
}, {
tableName: 'categories',
timestamps: true,
paranoid: true,
});
module.exports = mdl_Category;
+27
View File
@@ -0,0 +1,27 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const Certificate = sequelize.define("Certificate", {
certificate_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: DataTypes.UUID, allowNull: false, defaultValue: DataTypes.UUIDV4, unique: true },
cert_no: { type: DataTypes.STRING(25), allowNull: false, unique: true },
ref_no: { type: DataTypes.STRING(50), allowNull: false },
user_id: { type: DataTypes.BIGINT, allowNull: false },
course_id: { type: DataTypes.BIGINT, allowNull: false },
instructors: { type: DataTypes.TEXT, allowNull: true },
score: { type: DataTypes.INTEGER, allowNull: true },
length_str: { type: DataTypes.STRING(50), allowNull: true },
issued_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW },
}, {
tableName: "certificates",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{ unique: true, fields: ["user_id", "course_id"] },
{ fields: ["uuid"] },
{ fields: ["cert_no"] },
],
});
module.exports = Certificate;
+21
View File
@@ -0,0 +1,21 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const CourseInstructor = sequelize.define("CourseInstructor", {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
user_id: { type: DataTypes.BIGINT, allowNull: true },
display_name: { type: DataTypes.STRING(255), allowNull: false },
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
created_by: { type: DataTypes.BIGINT, allowNull: true },
}, {
tableName: "course_instructors",
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
indexes: [
{ fields: ["course_id"] },
],
});
module.exports = CourseInstructor;
+26
View File
@@ -0,0 +1,26 @@
'use strict';
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_Product = require('./products.mdl');
const mdl_CoursePurchase = sequelize.define('CoursePurchase', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.BIGINT, allowNull: false },
product_id: { type: DataTypes.BIGINT, allowNull: false },
amount: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
currency: { type: DataTypes.STRING(10), allowNull: false, defaultValue: 'USD' },
status: { type: DataTypes.ENUM('pending', 'completed', 'failed', 'cancelled', 'refunded'), allowNull: false, defaultValue: 'pending' },
provider: { type: DataTypes.STRING(50), allowNull: false, defaultValue: 'paypal' },
provider_payload: { type: DataTypes.JSONB, allowNull: true, defaultValue: {} },
expires_at: { type: DataTypes.DATE, allowNull: true },
paid_at: { type: DataTypes.DATE, allowNull: true },
}, {
tableName: 'course_purchases',
timestamps: true,
paranoid: true,
});
mdl_CoursePurchase.belongsTo(mdl_Product, { foreignKey: 'product_id', as: 'product' });
mdl_Product.hasMany(mdl_CoursePurchase, { foreignKey: 'product_id', as: 'purchases' });
module.exports = mdl_CoursePurchase;
@@ -0,0 +1,82 @@
/***********************************************************************************************************************************************************************
* File Name: course_reading_progress.mdl.js
* Type of Program: Model
* Description: Tracks a user's reading progress through a course hierarchy (course → unit → lesson).
*
* CourseReadingProgress — one row per (user, type, reference).
* UPSERT key: (user_id, type, reference_id)
* reference_id points to course.uuid | unit.uuid | lesson.uuid based on type.
* status flips in_progress → completed when the user finishes the item.
* last_accessed_at updated on every UPSERT.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 21, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const CourseReadingProgress = sequelize.define('CourseReadingProgress', {
progress_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
user_id: {
type: DataTypes.BIGINT,
allowNull: false,
references: { model: 'users', key: 'user_id' },
onDelete: 'CASCADE',
},
course_id: {
type: DataTypes.BIGINT,
allowNull: false,
references: { model: 'courses', key: 'course_id' },
onDelete: 'CASCADE',
},
reference_id: {
type: DataTypes.UUID,
allowNull: false,
comment: 'uuid of the course | unit | lesson depending on type.',
},
type: {
type: DataTypes.ENUM('course', 'unit', 'lesson'),
allowNull: false,
},
status: {
type: DataTypes.ENUM('in_progress', 'completed'),
allowNull: false,
defaultValue: 'in_progress',
},
completed_at: {
type: DataTypes.DATE,
allowNull: true,
comment: 'Set when status flips to completed. Null while in_progress.',
},
last_accessed_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: 'Updated on every UPSERT.',
},
// ── Audit trails ────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true },
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
}, {
tableName: 'course_reading_progress',
timestamps: true,
paranoid: false, // progress rows are never soft-deleted
indexes: [
{
unique: true,
fields: ['user_id', 'type', 'reference_id'],
name: 'uq_crp_user_type_reference',
},
{ fields: ['user_id'], name: 'idx_crp_user_id' },
{ fields: ['course_id'], name: 'idx_crp_course_id' },
{ fields: ['type'], name: 'idx_crp_type' },
{ fields: ['status'], name: 'idx_crp_status' },
],
});
module.exports = CourseReadingProgress;
+30 -7
View File
@@ -1,6 +1,6 @@
// models/courses/associations.js
const { Course, CourseProduct, CourseRole, CourseProductCategory } = require("./courses.mdl");
const { Course, CourseProductCategory } = require("./courses.mdl");
const Unit = require("./units.mdl");
const Lesson = require("./lessons.mdl");
const LessonPage = require("./lesson_page.mdl");
@@ -12,6 +12,17 @@ const UnitQuiz = require("./unit_quiz.mdl");
const QuizQuestion = require("./quiz_question.mdl");
const QuizOption = require("./quiz_option.mdl");
const mdl_Users = require("../users/users.mdl");
const QuizAttempt = require("./quiz_attempt.mdl");
const mdl_Category = require("./categories.mdl");
const Certificate = require("./certificate.mdl");
const CourseInstructor = require("./course_instructor.mdl");
const CourseReadingProgress = require("./course_reading_progress.mdl");
// ── CourseReadingProgress ─────────────────────────────────────────────────────
CourseReadingProgress.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
CourseReadingProgress.belongsTo(Course, { foreignKey: 'course_id', as: 'course' });
mdl_Users.hasMany(CourseReadingProgress, { foreignKey: 'user_id', as: 'courseReadingProgress' });
Course.hasMany(CourseReadingProgress, { foreignKey: 'course_id', as: 'readingProgress' });
// ── Course ────────────────────────────────────────────────────────────────────
Course.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
@@ -19,10 +30,13 @@ Course.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
Course.hasMany(Unit, { as: "units", foreignKey: "course_id" });
Course.hasMany(CourseObjective, { as: "objectives", foreignKey: "course_id" });
Course.hasMany(CoursePrerequisite, { as: "prerequisites", foreignKey: "course_id" });
Course.hasOne(CourseAssessment, { as: "assessment", foreignKey: "course_id" });
Course.hasMany(CourseProduct, { as: "products", foreignKey: "course_id" });
Course.hasMany(CourseRole, { as: "roles", foreignKey: "course_id" });
Course.hasMany(CourseProductCategory, { as: "categories", foreignKey: "course_id" });
Course.hasOne(CourseAssessment, { as: "assessment", foreignKey: "course_id" });
Course.hasMany(Certificate, { as: "certificates", foreignKey: "course_id" });
Course.hasMany(CourseInstructor, { as: "instructors", foreignKey: "course_id" });
CourseInstructor.belongsTo(Course, { as: "course", foreignKey: "course_id" });
CourseInstructor.belongsTo(mdl_Users, { as: "user", foreignKey: "user_id" });
Course.belongsToMany(mdl_Category, { through: CourseProductCategory, foreignKey: 'course_id', otherKey: 'category_id', as: 'categories' });
mdl_Category.belongsToMany(Course, { through: CourseProductCategory, foreignKey: 'category_id', otherKey: 'course_id', as: 'courses' });
// ── Unit ──────────────────────────────────────────────────────────────────────
Unit.belongsTo(Course, { as: "course", foreignKey: "course_id" });
@@ -58,10 +72,19 @@ QuizQuestion.hasMany(QuizOption, { as: "options", foreignKey: "question_id" });
// ── QuizOption ────────────────────────────────────────────────────────────────
QuizOption.belongsTo(QuizQuestion, { as: "question", foreignKey: "question_id" });
// ── QuizAttempt ───────────────────────────────────────────────────────────────
QuizAttempt.belongsTo(UnitQuiz, { as: "quiz", foreignKey: "quiz_id" });
QuizAttempt.belongsTo(CourseAssessment, { as: "assessment", foreignKey: "assessment_id" });
QuizAttempt.belongsTo(mdl_Users, { as: "user", foreignKey: "user_id" });
UnitQuiz.hasMany(QuizAttempt, { as: "attempts", foreignKey: "quiz_id" });
CourseAssessment.hasMany(QuizAttempt, { as: "attempts", foreignKey: "assessment_id" });
module.exports = {
Course, CourseProduct, CourseRole, CourseProductCategory,
Course, CourseProductCategory,
Unit, Lesson, LessonPage,
CourseObjective, LessonObjective,
CoursePrerequisite, CourseAssessment,
UnitQuiz, QuizQuestion, QuizOption,
UnitQuiz, QuizQuestion, QuizOption, QuizAttempt,
mdl_Category, Certificate, CourseInstructor,
CourseReadingProgress,
};
-14
View File
@@ -22,18 +22,6 @@ const Course = sequelize.define("Course", {
// ── Junction Tables ───────────────────────────────────────────────────────────
const CourseProduct = sequelize.define("CourseProduct", {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
product_id: { type: DataTypes.BIGINT, allowNull: false },
}, { tableName: "course_products", timestamps: true });
const CourseRole = sequelize.define("CourseRole", {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
role_id: { type: DataTypes.BIGINT, allowNull: false },
}, { tableName: "course_roles", timestamps: true });
const CourseProductCategory = sequelize.define("CourseProductCategory", {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
@@ -42,7 +30,5 @@ const CourseProductCategory = sequelize.define("CourseProductCategory", {
module.exports = {
Course,
CourseProduct,
CourseRole,
CourseProductCategory,
};
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const { Course } = require('./courses.mdl');
const mdl_Product = sequelize.define('Product', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
name: { type: DataTypes.STRING(200), allowNull: false },
description: { type: DataTypes.TEXT, allowNull: true },
price: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
currency: { type: DataTypes.STRING(10), allowNull: false, defaultValue: 'USD' },
access_days: { type: DataTypes.INTEGER, allowNull: true },
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true },
}, {
tableName: 'products',
timestamps: true,
paranoid: true,
});
// A course has one product listing; a product belongs to one course
mdl_Product.belongsTo(Course, { foreignKey: 'course_id', as: 'course' });
Course.hasOne(mdl_Product, { foreignKey: 'course_id', as: 'product' });
module.exports = mdl_Product;
+24
View File
@@ -0,0 +1,24 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const QuizAttempt = sequelize.define("QuizAttempt", {
attempt_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, unique: true },
user_id: { type: DataTypes.BIGINT, allowNull: false },
// polymorphic: belongs to either unit_quiz OR course_assessment
quiz_id: { type: DataTypes.BIGINT, allowNull: true }, // → unit_quizzes
assessment_id: { type: DataTypes.BIGINT, allowNull: true }, // → course_assessments
course_id: { type: DataTypes.BIGINT, allowNull: true }, // denormalized — for fast per-course queries
answers: { type: DataTypes.JSONB, allowNull: false, defaultValue: {} },
attempt_number: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 }, // 1-based per user+quiz
total_points: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
earned_points: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
score: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, // percentage 0-100
passing_score: { type: DataTypes.INTEGER, allowNull: true }, // snapshot at submit time
passed: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
}, {
tableName: "quiz_attempts",
timestamps: true,
});
module.exports = QuizAttempt;
@@ -0,0 +1,57 @@
/***********************************************************************************************************************************************************************
* File Name : notification.mdl.js
* Type : Sequelize Model
* Description : Admin-facing system notifications. Each row is a single event
* surfaced in the admin bell dropdown (e.g. "5 tasks overdue").
* Not user-scoped — visible to all admins.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const AdminNotification = sequelize.define('AdminNotification', {
notification_id: {
type: DataTypes.BIGINT,
autoIncrement: true,
primaryKey: true,
},
type: {
type: DataTypes.STRING(64),
allowNull: false,
},
title: {
type: DataTypes.STRING(255),
allowNull: false,
},
message: {
type: DataTypes.TEXT,
allowNull: false,
},
data: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: null,
},
seen: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
seen_at: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
}, {
tableName: 'admin_notifications',
timestamps: true,
indexes: [
{ fields: ['seen'], name: 'idx_an_seen' },
{ fields: ['type'], name: 'idx_an_type' },
{ fields: ['createdAt'], name: 'idx_an_created_at' },
],
});
module.exports = AdminNotification;
@@ -0,0 +1,62 @@
/***********************************************************************************************************************************************************************
* File Name : user_notification.mdl.js
* Type : Sequelize Model
* Description : Client-facing per-user notifications (achievement unlocked,
* task events, etc.). Scoped to a single user_id — each row
* is private to the user it belongs to.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const UserNotification = sequelize.define('UserNotification', {
notification_id: {
type: DataTypes.BIGINT,
autoIncrement: true,
primaryKey: true,
},
user_id: {
type: DataTypes.BIGINT,
allowNull: false,
},
type: {
type: DataTypes.STRING(64),
allowNull: false,
},
title: {
type: DataTypes.STRING(255),
allowNull: false,
},
message: {
type: DataTypes.TEXT,
allowNull: false,
},
data: {
type: DataTypes.JSONB,
allowNull: true,
defaultValue: null,
},
seen: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
seen_at: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
}, {
tableName: 'user_notifications',
timestamps: true,
indexes: [
{ fields: ['user_id'], name: 'idx_un_user_id' },
{ fields: ['user_id', 'seen'], name: 'idx_un_seen' },
{ fields: ['type'], name: 'idx_un_type' },
{ fields: ['createdAt'], name: 'idx_un_created_at' },
],
});
module.exports = UserNotification;
+40
View File
@@ -0,0 +1,40 @@
/***********************************************************************************************************************************************************************
* File Name: task_completion.attributes.js
* Type of Program: Attributes / Field Config
* Description: Attribute exclusion lists for TaskCompletion and TaskCompletionFile.
* Admins see audit fields; clients see only their own completion data.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 13, 2026
***********************************************************************************************************************************************************************/
// ─── Shared base exclusions (neither role needs these) ────────────────────────
const excludeAttributes = [];
// ─── Admin: can see audit trails ──────────────────────────────────────────────
const adminExclude = [
...excludeAttributes,
];
// ─── Client: no audit trails ──────────────────────────────────────────────────
const clientExclude = [
...excludeAttributes,
'createdBy',
'updatedBy',
'deletedBy',
'deletedAt',
'storage_key',
];
const jsonbSchemas = {};
const computedAttributes = [];
const filterableFields = {};
module.exports = {
adminExclude,
clientExclude,
excludeAttributes,
jsonbSchemas,
computedAttributes,
filterableFields,
};
+129
View File
@@ -0,0 +1,129 @@
/***********************************************************************************************************************************************************************
* File Name: task_completion.mdl.js
* Type of Program: Model
* Description: Sequelize models for `task_completions` and `task_completion_files`.
* A completion is created when a user turns in work for a task.
* Multiple completions are allowed (resubmit anytime).
* Files are stored separately — one completion can have many files.
*
* 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 } = require('./task.mdl');
// ─── TaskCompletion ────────────────────────────────────────────────────────────
const TaskCompletion = sequelize.define('TaskCompletion', {
completion_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
order: 3,
},
task_id: {
type: DataTypes.UUID,
allowNull: false,
references: { model: 'tasks', key: 'task_id' },
onDelete: 'CASCADE',
order: 4,
},
user_id: {
type: DataTypes.BIGINT,
allowNull: false,
references: { model: 'users', key: 'user_id' },
onDelete: 'CASCADE',
order: 5,
},
note: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Optional note the user attaches when submitting.',
order: 1,
},
submitted_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
allowNull: false,
order: 2,
},
// ── Audit trails ────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true, order: 6 },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, order: 7 },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, order: 8 },
}, {
tableName: 'task_completions',
paranoid: true,
timestamps: true,
indexes: [
{ fields: ['task_id'], name: 'idx_tc_task_id' },
{ fields: ['user_id'], name: 'idx_tc_user_id' },
{ fields: ['task_id', 'user_id'], name: 'idx_tc_task_user' },
],
});
// ─── TaskCompletionFile ────────────────────────────────────────────────────────
const TaskCompletionFile = sequelize.define('TaskCompletionFile', {
file_id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
completion_id: {
type: DataTypes.UUID,
allowNull: false,
references: { model: 'task_completions', key: 'completion_id' },
onDelete: 'CASCADE',
},
file_url: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Storage path or CDN URL of the uploaded file.',
},
file_name: {
type: DataTypes.STRING,
allowNull: false,
comment: 'Original filename as uploaded by the user.',
},
file_size: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'File size in bytes.',
},
mime_type: {
type: DataTypes.STRING,
allowNull: true,
comment: 'MIME type e.g. application/pdf, image/png.',
},
storage_key: {
type: DataTypes.STRING(500),
allowNull: true,
comment: 'S3 object key (e.g. "images/uuid.jpg"), used by the download proxy endpoint.',
},
// ── Audit trails ────────────────────────────────────────────────────────
createdBy: { type: DataTypes.BIGINT, allowNull: true },
updatedBy: { type: DataTypes.BIGINT, allowNull: true },
deletedBy: { type: DataTypes.BIGINT, allowNull: true },
}, {
tableName: 'task_completion_files',
paranoid: true,
timestamps: true,
indexes: [
{ fields: ['completion_id'], name: 'idx_tcf_completion_id' },
],
});
// ─── Associations ──────────────────────────────────────────────────────────────
Task.hasMany(TaskCompletion, { foreignKey: 'task_id', as: 'completions' });
TaskCompletion.belongsTo(Task, { foreignKey: 'task_id', as: 'task' });
mdl_Users.hasMany(TaskCompletion, { foreignKey: 'user_id', as: 'taskCompletions' });
TaskCompletion.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
TaskCompletion.hasMany(TaskCompletionFile, { foreignKey: 'completion_id', as: 'files' });
TaskCompletionFile.belongsTo(TaskCompletion, { foreignKey: 'completion_id', as: 'completion' });
module.exports = { TaskCompletion, TaskCompletionFile };
+158
View File
@@ -0,0 +1,158 @@
/***********************************************************************************************************************************************************************
* 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 depending on type.',
},
type: {
type: DataTypes.ENUM('read_course', 'read_unit', 'read_lesson'),
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 };
+13
View File
@@ -0,0 +1,13 @@
/***********************************************************************************************************************************************************************
* File Name: payments.attributes.js
* Type of Program: Model Attributes Config
* Description: DataTable column config for payments — used by paginate.util.js
* Author: rgrgogu
* Date Created: Jun. 9, 2026
***********************************************************************************************************************************************************************/
const excludeAttributes = ['provider_payload'];
const jsonbSchemas = {};
const computedAttributes = [];
module.exports = { excludeAttributes, jsonbSchemas, computedAttributes };
+44
View File
@@ -0,0 +1,44 @@
/***********************************************************************************************************************************************************************
* File Name: payments.mdl.js
* Type of Program: Model
* Description: Payment records for tier plan purchases.
* provider_payload JSONB stores all provider-specific data:
* {
* order_id, ← PayPal order ID
* capture_id, ← PayPal capture ID
* payer_id, ← PayPal payer ID
* approval_url, ← PayPal approval URL (stored at order creation)
* checkout: { promo_code, subtotal, discount },
* capture: { ...full PayPal capture response },
* error: { ...PayPal error response if failed },
* cancelled_at, cancelled_by
* }
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 9, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_Payments = sequelize.define('Payment', {
payment_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'Payment ID', hidden: true, order: 0, filterable: true },
user_id: { type: DataTypes.BIGINT, allowNull: false, label: 'User ID', hidden: true, order: 1, filterable: true },
plan_id: { type: DataTypes.BIGINT, allowNull: false, label: 'Plan ID', hidden: true, order: 2, filterable: true },
tier_id: { type: DataTypes.BIGINT, allowNull: true, label: 'Tier ID', hidden: true, order: 3, filterable: true },
status: { type: DataTypes.ENUM('pending', 'completed', 'failed', 'cancelled', 'expired', 'refunded'), allowNull: false, defaultValue: 'pending', label: 'Status', hidden: false, order: 4, filterable: true },
amount: { type: DataTypes.DECIMAL(10, 2), allowNull: false, label: 'Amount', hidden: false, order: 5, filterable: false },
currency: { type: DataTypes.STRING(10), allowNull: false, defaultValue: 'USD', label: 'Currency', hidden: false, order: 6, filterable: true },
promo_code: { type: DataTypes.STRING(50), allowNull: true, label: 'Promo Code', hidden: false, order: 7, filterable: true },
discount: { type: DataTypes.DECIMAL(10, 2), allowNull: false, defaultValue: 0.00, label: 'Discount', hidden: false, order: 8, filterable: false },
provider: { type: DataTypes.STRING(50), allowNull: false, defaultValue: 'paypal', label: 'Provider', hidden: false, order: 9, filterable: true },
provider_payload: { type: DataTypes.JSONB, allowNull: true, defaultValue: {}, label: 'Provider Payload', hidden: true, order: 10, filterable: false },
paid_at: { type: DataTypes.DATE, allowNull: true, label: 'Paid At', hidden: false, order: 11, filterable: false },
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: 'Created By' },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: 'Updated By' },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: 'Deleted By' },
}, {
tableName: 'payments',
timestamps: true,
paranoid: true,
});
module.exports = mdl_Payments;
+22
View File
@@ -0,0 +1,22 @@
/***********************************************************************************************************************************************************************
* File Name: plan_courses.mdl.js
* Type of Program: Model
* Description: Junction table — links courses to a specific tier plan.
* UNIQUE on course_id enforces one course belongs to one plan only.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 6, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_PlanCourses = sequelize.define('PlanCourse', {
id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
plan_id: { type: DataTypes.BIGINT, allowNull: false },
course_id: { type: DataTypes.BIGINT, allowNull: false },
}, {
tableName: 'plan_courses',
timestamps: true,
paranoid: false,
});
module.exports = mdl_PlanCourses;
+39
View File
@@ -0,0 +1,39 @@
const mdl_Users = require('../users/users.mdl');
const mdl_TierPlans = require('./tier_plans.mdl');
const mdl_UserTiers = require('./user_tiers.mdl');
const mdl_Payments = require('./payments.mdl');
const mdl_PlanCourses = require('./plan_courses.mdl');
const { Course } = require('../courses/courses.mdl'); // ← named export
// ─── UserTiers ────────────────────────────────────────────────────────────────
mdl_UserTiers.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
mdl_UserTiers.belongsTo(mdl_Users, { foreignKey: 'granted_by', as: 'grantedByUser' });
mdl_UserTiers.belongsTo(mdl_Users, { foreignKey: 'revoked_by', as: 'revokedByUser' });
mdl_Users.hasMany(mdl_UserTiers, { foreignKey: 'user_id', as: 'tiers' });
// ─── Payments ─────────────────────────────────────────────────────────────────
mdl_Payments.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
mdl_Payments.belongsTo(mdl_TierPlans, { foreignKey: 'plan_id', as: 'plan' });
mdl_Payments.belongsTo(mdl_UserTiers, { foreignKey: 'tier_id', as: 'tier' });
// ─── Plan ↔ Courses ───────────────────────────────────────────────────────────
mdl_TierPlans.belongsToMany(Course, {
through: mdl_PlanCourses,
foreignKey: 'plan_id',
otherKey: 'course_id',
as: 'courses',
});
Course.belongsToMany(mdl_TierPlans, {
through: mdl_PlanCourses,
foreignKey: 'course_id',
otherKey: 'plan_id',
as: 'plans',
});
mdl_PlanCourses.belongsTo(mdl_TierPlans, { foreignKey: 'plan_id', as: 'plan' });
mdl_PlanCourses.belongsTo(Course, { foreignKey: 'course_id', as: 'course' });
Course.hasOne(mdl_PlanCourses, { as: 'planCourse', foreignKey: 'course_id' });
mdl_TierPlans.hasMany(mdl_PlanCourses, { as: 'planCourses', foreignKey: 'plan_id' });
module.exports = { mdl_TierPlans, mdl_UserTiers, mdl_Payments, mdl_PlanCourses };
+17
View File
@@ -0,0 +1,17 @@
/***********************************************************************************************************************************************************************
* File Name: tier_plans.attributes.js
* Type of Program: Attributes
* Description: Exclude list, display attributes, and computed fields for tier_plans.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 6, 2026
***********************************************************************************************************************************************************************/
const excludeAttributes = [
// nothing hidden by default — all columns are safe to expose to admin
];
const jsonbSchemas = {}; // no JSONB columns on this model
const computedAttributes = []; // no computed fields needed
module.exports = { excludeAttributes, jsonbSchemas, computedAttributes };
+26
View File
@@ -0,0 +1,26 @@
/***********************************************************************************************************************************************************************
* File Name: tier_plans.mdl.js
* Type of Program: Model
* Description: Sequelize model for the `tier_plans` table.
* Catalog of fixed plans (premium/exclusive) with duration and price.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 6, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_TierPlans = sequelize.define('TierPlan', {
plan_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'Plan ID' },
tier: { type: DataTypes.ENUM('premium', 'exclusive'), allowNull: false, label: 'Tier' },
label: { type: DataTypes.STRING(100), allowNull: false, label: 'Plan Label' },
duration_days: { type: DataTypes.INTEGER, allowNull: false, label: 'Duration (Days)' },
price: { type: DataTypes.DECIMAL(10, 2), allowNull: false, label: 'Price' },
currency: { type: DataTypes.CHAR(3), allowNull: false, defaultValue: 'USD', label: 'Currency' },
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: 'Active' },
}, {
tableName: 'tier_plans',
timestamps: true,
paranoid: true,
});
module.exports = mdl_TierPlans;
+30
View File
@@ -0,0 +1,30 @@
/***********************************************************************************************************************************************************************
* File Name: user_tiers.mdl.js
* Type of Program: Model
* Description: Sequelize model for the `user_tiers` table.
* Full history of tier grants per user.
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 6, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_UserTiers = sequelize.define('UserTier', {
tier_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'Tier ID' },
user_id: { type: DataTypes.BIGINT, allowNull: false, label: 'User ID' },
tier: { type: DataTypes.ENUM('free', 'premium', 'exclusive'), allowNull: false, defaultValue: 'free', label: 'Tier' },
status: { type: DataTypes.ENUM('active', 'expired', 'revoked'), allowNull: false, defaultValue: 'active', label: 'Status' },
starts_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, label: 'Starts At' },
expires_at: { type: DataTypes.DATE, allowNull: true, label: 'Expires At' },
granted_by: { type: DataTypes.BIGINT, allowNull: true, label: 'Granted By' },
revoked_by: { type: DataTypes.BIGINT, allowNull: true, label: 'Revoked By' },
revoked_at: { type: DataTypes.DATE, allowNull: true, label: 'Revoked At' },
notes: { type: DataTypes.TEXT, allowNull: true, label: 'Notes' },
}, {
tableName: 'user_tiers',
timestamps: true,
paranoid: false,
});
module.exports = mdl_UserTiers;
+38
View File
@@ -0,0 +1,38 @@
/***********************************************************************************************************************************************************************
* File Name: achievements.mdl.js
* Type of Program: Model
* Description: User achievements — badges and milestones.
* Unique per user + key (one achievement of each type per user).
*
* Built-in keys:
* Badge:
* early_access — registered before Dec 31, 2026
* premium_first_time — first premium tier purchase
* exclusive_first_time — first exclusive tier purchase
* Milestone:
* course_completed — completed first course
* (extend as needed)
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 11, 2026
***********************************************************************************************************************************************************************/
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_Achievements = sequelize.define('Achievement', {
achievement_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'Achievement ID', hidden: true, order: 0 },
user_id: { type: DataTypes.BIGINT, allowNull: false, label: 'User ID', hidden: true, order: 0 },
type: { type: DataTypes.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge', label: 'Type', hidden: false, order: 1, filterable: true },
key: { type: DataTypes.STRING(100), allowNull: false, label: 'Key', hidden: false, order: 2, filterable: true },
label: { type: DataTypes.STRING(255), allowNull: false, label: 'Label', hidden: false, order: 3, filterable: true },
description: { type: DataTypes.TEXT, allowNull: true, label: 'Description', hidden: false, order: 4, filterable: false },
granted_by: { type: DataTypes.BIGINT, allowNull: true, label: 'Granted By', hidden: false, order: 5, filterable: false },
granted_at: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, label: 'Granted At', hidden: false, order: 6, filterable: false },
metadata: { type: DataTypes.JSONB, allowNull: true, defaultValue: {}, label: 'Metadata', hidden: true, order: 0 },
}, {
tableName: 'achievements',
timestamps: true,
paranoid: false,
});
module.exports = mdl_Achievements;
+30
View File
@@ -0,0 +1,30 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../../config/db.config');
const mdl_Users = require('./users.mdl');
const mdl_UserActivity = sequelize.define('UserActivity', {
activity_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
user_id: { type: DataTypes.BIGINT, allowNull: false, references: { model: mdl_Users, key: 'user_id' } },
session_id: { type: DataTypes.BIGINT, allowNull: true },
action: { type: DataTypes.STRING(100), allowNull: false },
entity_type: { type: DataTypes.STRING(50), allowNull: true },
entity_id: { type: DataTypes.BIGINT, allowNull: true },
/**
* details JSONB — free-form context per action, e.g.:
* login : { session_id, reg_type }
* deactivate_user : { target_email }
* lesson_read : { lesson_uuid, status }
* submit_task : { task_id }
* set_user_status : { is_active }
*/
details: { type: DataTypes.JSONB, allowNull: true },
created_at: { type: DataTypes.DATE, allowNull: true, defaultValue: DataTypes.NOW },
}, {
tableName: 'user_activity',
timestamps: false,
});
mdl_UserActivity.belongsTo(mdl_Users, { foreignKey: 'user_id', as: 'user' });
mdl_Users.hasMany(mdl_UserActivity, { foreignKey: 'user_id', as: 'activities' });
module.exports = mdl_UserActivity;