mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
ready to test
Testing Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -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;
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user