mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -11,21 +11,31 @@ const Advertisement = sequelize.define("Advertisement", {
|
||||
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, label: "UUID", order: 0, hidden: true },
|
||||
|
||||
// ─── Identification / placement ──────────────────────────────────────────
|
||||
// placement is the source of truth for "where" (a registry key, see
|
||||
// advertisements.placements.js); type is denormalized from it on every
|
||||
// write (applyAdvertisementFields) and describes "what it looks like".
|
||||
placement: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: true,
|
||||
filterable: true,
|
||||
label: "Placement", order: 1
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM("hero", "banner", "popup", "sidebar"),
|
||||
allowNull: false,
|
||||
label: "Type", order: 1
|
||||
filterable: true,
|
||||
label: "Format", order: 2
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM("draft", "active", "scheduled", "expired", "archived"),
|
||||
allowNull: false,
|
||||
defaultValue: "draft", label: "Status", order: 2
|
||||
defaultValue: "draft", label: "Status", order: 3
|
||||
},
|
||||
|
||||
// ─── 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 },
|
||||
badge_label: { type: DataTypes.STRING(100), label: "Badge Label", order: 4 },
|
||||
headline: { type: DataTypes.STRING(255), label: "Headline", order: 5 },
|
||||
description: { type: DataTypes.TEXT, label: "Description", order: 6 },
|
||||
|
||||
// ─── Media ────────────────────────────────────────────────────────────────
|
||||
image_url: { type: DataTypes.STRING(512), label: "Image URL", order: 0, hidden: true },
|
||||
@@ -33,19 +43,19 @@ const Advertisement = sequelize.define("Advertisement", {
|
||||
|
||||
// ─── Calls-to-action ──────────────────────────────────────────────────────
|
||||
// [{ label, link }, ...] — 0-2 entries depending on type
|
||||
ctas: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "CTAs", order: 6 },
|
||||
ctas: { type: DataTypes.JSONB, allowNull: false, defaultValue: [], label: "CTAs", order: 7 },
|
||||
|
||||
// ─── Scheduling ───────────────────────────────────────────────────────────
|
||||
start_date: { type: DataTypes.DATE, label: "Start Date", order: 7 },
|
||||
end_date: { type: DataTypes.DATE, label: "End Date", order: 8 },
|
||||
start_date: { type: DataTypes.DATE, label: "Start Date", order: 8 },
|
||||
end_date: { type: DataTypes.DATE, label: "End Date", order: 9 },
|
||||
|
||||
// ─── 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
|
||||
order: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: "order", label: "Order", order: 10 },
|
||||
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: "Active", order: 11 },
|
||||
size: { type: DataTypes.ENUM("sm", "md", "lg"), allowNull: true, label: "Size", order: 12 }, // banner-only, ignored by other types
|
||||
|
||||
// ─── Metrics ──────────────────────────────────────────────────────────────
|
||||
click_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Clicks", order: 12, hidden: true },
|
||||
click_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Clicks", order: 13, hidden: true },
|
||||
|
||||
// ─── Audit trails ─────────────────────────────────────────────────────────
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
||||
@@ -58,6 +68,7 @@ const Advertisement = sequelize.define("Advertisement", {
|
||||
indexes: [
|
||||
{ fields: ["uuid"] },
|
||||
{ fields: ["type"] },
|
||||
{ fields: ["placement"] },
|
||||
{ fields: ["status"] },
|
||||
{ fields: ["is_active"] },
|
||||
{ fields: ["deletedAt"] },
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// models/advertisements/advertisements.placements.js
|
||||
//
|
||||
// Declarative registry of every ad placement in the client app. Each entry
|
||||
// is a self-contained "slot" — a page + position pair — that determines the
|
||||
// visual format (hero/banner/popup/sidebar) automatically. Adding a new
|
||||
// placement should only ever require adding one entry here (and wiring the
|
||||
// corresponding client page to fetch/render it) — nothing else in this file
|
||||
// should need to change.
|
||||
//
|
||||
// `key` is what's stored on advertisements.placement. `format` is what gets
|
||||
// denormalized onto advertisements.type on write (see applyAdvertisementFields
|
||||
// in controllers/admin/advertisements.controller.js) — type is never accepted
|
||||
// from the client once a placement is set.
|
||||
|
||||
const PLACEMENTS = [
|
||||
{ key: "dashboard.hero", format: "hero", page: "dashboard", pageLabel: "Dashboard", slotLabel: "Hero (top of page)" },
|
||||
{ key: "dashboard.popup", format: "popup", page: "dashboard", pageLabel: "Dashboard", slotLabel: "Popup (on load)" },
|
||||
{ key: "course_list.banner", format: "banner", page: "course_list", pageLabel: "Courses", slotLabel: "Banner (above course grid)" },
|
||||
{ key: "course_details.banner", format: "banner", page: "course_details", pageLabel: "Course Details", slotLabel: "Banner (below hero)" },
|
||||
{ key: "course_details.sidebar", format: "sidebar", page: "course_details", pageLabel: "Course Details", slotLabel: "Sidebar (beside course content)" },
|
||||
{ key: "plans.banner", format: "banner", page: "plans", pageLabel: "Plans", slotLabel: "Banner (above plan cards)" },
|
||||
];
|
||||
|
||||
const PLACEMENT_MAP = Object.fromEntries(PLACEMENTS.map((p) => [p.key, p]));
|
||||
const PLACEMENT_KEYS = PLACEMENTS.map((p) => p.key);
|
||||
|
||||
function getFormatForPlacement(key) {
|
||||
return PLACEMENT_MAP[key]?.format ?? null;
|
||||
}
|
||||
|
||||
module.exports = { PLACEMENTS, PLACEMENT_MAP, PLACEMENT_KEYS, getFormatForPlacement };
|
||||
@@ -1,10 +1,10 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: pending_certificate.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Holds certificates queued for issuance after a 45-minute delay
|
||||
* Description: Holds certificates queued for issuance after a 5-minute delay
|
||||
* following a passed course assessment. The cron job
|
||||
* (cron/jobs/issue_certificates.cron.js) polls this table every
|
||||
* 5 minutes and processes rows where issue_at <= NOW().
|
||||
* (cron/jobs/issue_certificates.cron.js) runs hourly on the hour
|
||||
* and processes rows where issue_at <= NOW().
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 24, 2026
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: email_broadcast.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: One "send this email template to this audience" job. The API
|
||||
* only ever creates this row + its email_broadcast_recipients
|
||||
* rows (see controllers/admin/email_broadcasts.controller.js) —
|
||||
* actual SMTP sending happens later, paced, in
|
||||
* cron/jobs/dispatch_email_broadcasts.cron.js.
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_EmailTemplate = require('./email_templates.mdl');
|
||||
const mdl_Users = require('../users/users.mdl');
|
||||
|
||||
const mdl_EmailBroadcast = sequelize.define('EmailBroadcast', {
|
||||
email_broadcast_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
email_template_id: { type: DataTypes.BIGINT, allowNull: false, label: 'Template', order: 1 },
|
||||
target_type: { type: DataTypes.ENUM('admin', 'user', 'both', 'task_list', 'course', 'tier_plan'), allowNull: false, label: 'Target', order: 2, filterable: true },
|
||||
target_id: { type: DataTypes.STRING(64), allowNull: true, label: 'Target ID', order: 3 },
|
||||
status: { type: DataTypes.ENUM('queued', 'sending', 'completed', 'canceled'), allowNull: false, defaultValue: 'queued', label: 'Status', order: 4, filterable: true },
|
||||
total_recipients: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: 'Total', order: 5 },
|
||||
sent_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: 'Sent', order: 6 },
|
||||
failed_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: 'Failed', order: 7 },
|
||||
started_at: { type: DataTypes.DATE, allowNull: true, label: 'Started', order: 8 },
|
||||
completed_at: { type: DataTypes.DATE, allowNull: true, label: 'Completed', order: 9 },
|
||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: 'Created By' },
|
||||
}, {
|
||||
tableName: 'email_broadcasts',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
mdl_EmailBroadcast.belongsTo(mdl_EmailTemplate, { as: 'template', foreignKey: 'email_template_id' });
|
||||
mdl_EmailBroadcast.belongsTo(mdl_Users, { as: 'creator', foreignKey: 'createdBy' });
|
||||
|
||||
module.exports = mdl_EmailBroadcast;
|
||||
@@ -0,0 +1,33 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: email_broadcast_recipient.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: The outbox — one row per recipient of an email_broadcasts job.
|
||||
* `email` is snapshotted at enqueue time so a later change to the
|
||||
* user's account email doesn't affect an in-flight broadcast.
|
||||
* cron/jobs/dispatch_email_broadcasts.cron.js is the only writer
|
||||
* of `status`/`error`/`sent_at` after creation.
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
const mdl_EmailBroadcast = require('./email_broadcast.mdl');
|
||||
|
||||
const mdl_EmailBroadcastRecipient = sequelize.define('EmailBroadcastRecipient', {
|
||||
email_broadcast_recipient_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
email_broadcast_id: { type: DataTypes.BIGINT, allowNull: false, label: 'Broadcast', order: 1 },
|
||||
user_id: { type: DataTypes.BIGINT, allowNull: true, label: 'User', order: 2 },
|
||||
email: { type: DataTypes.STRING(255), allowNull: false, label: 'Email', order: 3 },
|
||||
name: { type: DataTypes.STRING(255), allowNull: true, label: 'Name', order: 4 },
|
||||
status: { type: DataTypes.ENUM('pending', 'sent', 'failed'), allowNull: false, defaultValue: 'pending', label: 'Status', order: 5, filterable: true },
|
||||
error: { type: DataTypes.TEXT, allowNull: true, label: 'Error', order: 6 },
|
||||
sent_at: { type: DataTypes.DATE, allowNull: true, label: 'Sent At', order: 7 },
|
||||
}, {
|
||||
tableName: 'email_broadcast_recipients',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
mdl_EmailBroadcastRecipient.belongsTo(mdl_EmailBroadcast, { as: 'broadcast', foreignKey: 'email_broadcast_id' });
|
||||
|
||||
module.exports = mdl_EmailBroadcastRecipient;
|
||||
@@ -0,0 +1,49 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: email_templates.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Admin-managed catalog of email templates (subject + HTML body).
|
||||
* Replaces the old static emailTemplates map in data/email_body.data.js.
|
||||
* `is_system` rows are the built-in types referenced by name in
|
||||
* services/email.service.js's sendEmail() callers — protected
|
||||
* from deletion/type-rename by the admin controller. The outer
|
||||
* layout (header/footer/signature) is NOT stored here — it stays
|
||||
* fixed in services/email.service.js and is never admin-editable.
|
||||
*
|
||||
* Publish workflow: `subject`/`html_body` are the LIVE content —
|
||||
* the only columns services/email.service.js's sendEmail() ever
|
||||
* reads. Editing a 'sent' template writes to `draft_subject`/
|
||||
* `draft_html_body` instead, leaving live content (and therefore
|
||||
* real outgoing mail) untouched until an admin explicitly
|
||||
* publishes again (see controllers/admin/email_templates.controller.js).
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_EmailTemplate = sequelize.define('EmailTemplate', {
|
||||
email_template_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
type: { type: DataTypes.STRING(100), allowNull: false, unique: true, label: 'Type', hidden: false, order: 1, filterable: true },
|
||||
label: { type: DataTypes.STRING(150), allowNull: false, label: 'Label', hidden: false, order: 2, filterable: true },
|
||||
category: { type: DataTypes.ENUM('announcement', 'advertisement', 'system', 'other'), allowNull: false, defaultValue: 'other', label: 'Category', hidden: false, order: 3, filterable: true },
|
||||
status: { type: DataTypes.ENUM('draft', 'sent'), allowNull: false, defaultValue: 'draft', label: 'Status', hidden: false, order: 4, filterable: true },
|
||||
subject: { type: DataTypes.STRING(255), allowNull: true, label: 'Subject', hidden: false, order: 5, filterable: false },
|
||||
html_body: { type: DataTypes.TEXT, allowNull: true, label: 'HTML Body', hidden: false, order: 6, filterable: false },
|
||||
// Markdown source for the live/draft HTML above — editor convenience only,
|
||||
// never read by services/email.service.js. Null for templates authored
|
||||
// before Markdown support (including all 8 system templates), which keep
|
||||
// editing html_body/draft_html_body directly.
|
||||
body_markdown: { type: DataTypes.TEXT, allowNull: true, label: 'Body (Markdown)', hidden: false, order: 6.5, filterable: false },
|
||||
draft_subject: { type: DataTypes.STRING(255), allowNull: true, label: 'Draft Subject', hidden: false, order: 7, filterable: false },
|
||||
draft_html_body: { type: DataTypes.TEXT, allowNull: true, label: 'Draft Body', hidden: false, order: 8, filterable: false },
|
||||
draft_body_markdown: { type: DataTypes.TEXT, allowNull: true, label: 'Draft Body (Markdown)', hidden: false, order: 8.5, filterable: false },
|
||||
last_sent_at: { type: DataTypes.DATE, allowNull: true, label: 'Last Sent', hidden: false, order: 9, filterable: false },
|
||||
is_system: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, label: 'System', hidden: false, order: 10, filterable: true },
|
||||
}, {
|
||||
tableName: 'email_templates',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_EmailTemplate;
|
||||
@@ -0,0 +1,15 @@
|
||||
// models/notifications/cron_notification_setting.mdl.js
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("../../config/db.config");
|
||||
|
||||
const CronNotificationSetting = sequelize.define("CronNotificationSetting", {
|
||||
job_name: { type: DataTypes.STRING(64), primaryKey: true, label: "Job" },
|
||||
enabled: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: "Enabled" },
|
||||
schedule: { type: DataTypes.STRING(20), allowNull: false, label: "Schedule" },
|
||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Modified By" },
|
||||
}, {
|
||||
tableName: "cron_notification_settings",
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
module.exports = CronNotificationSetting;
|
||||
@@ -0,0 +1,24 @@
|
||||
// models/notifications/notification_broadcast.attributes.js
|
||||
|
||||
// ─── Exclude sets ─────────────────────────────────────────────────────────────
|
||||
|
||||
const excludeAttributes = [];
|
||||
|
||||
// Admins see everything
|
||||
const adminExclude = [
|
||||
...excludeAttributes,
|
||||
];
|
||||
|
||||
// ─── JSONB schemas ──────────────────────────────────────────────────────────
|
||||
// No JSONB columns on this model.
|
||||
const jsonbSchemas = {};
|
||||
|
||||
// ─── Computed attributes ──────────────────────────────────────────────────────
|
||||
const computedAttributes = [];
|
||||
|
||||
module.exports = {
|
||||
excludeAttributes,
|
||||
adminExclude,
|
||||
jsonbSchemas,
|
||||
computedAttributes,
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
// models/notifications/notification_broadcast.mdl.js
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("../../config/db.config");
|
||||
const mdl_Users = require("../users/users.mdl");
|
||||
|
||||
const NotificationBroadcast = sequelize.define("NotificationBroadcast", {
|
||||
|
||||
// ─── Identity ─────────────────────────────────────────────────────────────
|
||||
broadcast_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: "Broadcast ID", order: 0, hidden: true },
|
||||
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, label: "UUID", order: 0, hidden: true },
|
||||
|
||||
// ─── Content ──────────────────────────────────────────────────────────────
|
||||
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", order: 1 },
|
||||
message: { type: DataTypes.TEXT, allowNull: false, label: "Message", order: 2 },
|
||||
|
||||
// ─── Targeting ────────────────────────────────────────────────────────────
|
||||
target_type: {
|
||||
type: DataTypes.ENUM("admin", "user", "both", "task_list", "course", "tier_plan"),
|
||||
allowNull: false,
|
||||
label: "Target", order: 3
|
||||
},
|
||||
// Holds a task list UUID, course UUID, or tier plan ID (stringified) — only
|
||||
// set when target_type is 'task_list' / 'course' / 'tier_plan'.
|
||||
target_id: { type: DataTypes.STRING(64), allowNull: true, label: "Target ID", order: 3.5 },
|
||||
|
||||
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
||||
status: {
|
||||
type: DataTypes.ENUM("draft", "sent", "archived"),
|
||||
allowNull: false,
|
||||
defaultValue: "draft", label: "Status", order: 4
|
||||
},
|
||||
sent_at: { type: DataTypes.DATE, allowNull: true, label: "Sent At", order: 5 },
|
||||
recipient_count: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Recipients", order: 6 },
|
||||
|
||||
// ─── 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: "notification_broadcasts",
|
||||
timestamps: true, // createdAt, updatedAt
|
||||
paranoid: true,
|
||||
indexes: [
|
||||
{ fields: ["uuid"] },
|
||||
{ fields: ["status"] },
|
||||
{ fields: ["target_type"] },
|
||||
{ fields: ["deletedAt"] },
|
||||
],
|
||||
});
|
||||
|
||||
NotificationBroadcast.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
|
||||
NotificationBroadcast.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
|
||||
|
||||
module.exports = NotificationBroadcast;
|
||||
@@ -8,6 +8,9 @@
|
||||
|
||||
const excludeAttributes = ['provider_payload'];
|
||||
const jsonbSchemas = {};
|
||||
const computedAttributes = [];
|
||||
const computedAttributes = [
|
||||
{ key: 'user_full_name', label: 'Full Name', type: 'text', order: 1, filterable: false, literal: `("user"."personal_info"->'name'->>'full_name')` },
|
||||
{ key: 'user.email', label: 'Email Address', type: 'text', order: 2, filterable: false },
|
||||
];
|
||||
|
||||
module.exports = { excludeAttributes, jsonbSchemas, computedAttributes };
|
||||
@@ -25,13 +25,13 @@ const mdl_Payments = sequelize.define('Payment', {
|
||||
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 },
|
||||
amount: { type: DataTypes.DECIMAL(10, 2), allowNull: false, label: 'Amount', hidden: false, order: 3, 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 },
|
||||
paid_at: { type: DataTypes.DATE, allowNull: true, label: 'Paid At', hidden: false, order: 5, 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' },
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: plan_prices.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Admin-managed localized price overrides for tier plans.
|
||||
* One row per (plan_id, currency) pair. When a user's preferred_currency
|
||||
* matches a row here, the override price is shown instead of the base price.
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 29, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
'use strict';
|
||||
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_PlanPrices = sequelize.define('PlanPrice', {
|
||||
price_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
|
||||
plan_id: { type: DataTypes.BIGINT, allowNull: false },
|
||||
currency: { type: DataTypes.CHAR(3), allowNull: false, label: 'Currency' },
|
||||
price: { type: DataTypes.DECIMAL(10, 2), allowNull: false, label: 'Price' },
|
||||
}, {
|
||||
tableName: 'plan_prices',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_PlanPrices;
|
||||
@@ -5,7 +5,6 @@ const mdl_UserTiers = require('./user_tiers.mdl');
|
||||
const mdl_Payments = require('./payments.mdl');
|
||||
const mdl_PlanCourses = require('./plan_courses.mdl');
|
||||
const mdl_PlanPolicies = require('./plan_policies.mdl');
|
||||
const mdl_PlanPrices = require('./plan_prices.mdl');
|
||||
const mdl_SystemBadges = require('../system_badges/system_badges.mdl');
|
||||
const Asset = require('../assets/assets.mdl');
|
||||
const { Course } = require('../courses/courses.mdl');
|
||||
@@ -54,10 +53,6 @@ mdl_TierPlans.hasMany(mdl_UserTiers, { foreignKey: 'plan_id', as: 'userTiers'
|
||||
mdl_TierPlans.hasOne(mdl_PlanPolicies, { foreignKey: 'plan_id', as: 'policy' });
|
||||
mdl_PlanPolicies.belongsTo(mdl_TierPlans, { foreignKey: 'plan_id', as: 'plan' });
|
||||
|
||||
// ─── Plan ↔ Localized Prices ──────────────────────────────────────────────────
|
||||
mdl_TierPlans.hasMany(mdl_PlanPrices, { foreignKey: 'plan_id', as: 'prices' });
|
||||
mdl_PlanPrices.belongsTo(mdl_TierPlans, { foreignKey: 'plan_id', as: 'plan' });
|
||||
|
||||
// ─── SystemBadge → Asset ─────────────────────────────────────────────────────
|
||||
mdl_SystemBadges.belongsTo(Asset, { foreignKey: 'asset_id', as: 'asset' });
|
||||
|
||||
@@ -68,6 +63,5 @@ module.exports = {
|
||||
mdl_Payments,
|
||||
mdl_PlanCourses,
|
||||
mdl_PlanPolicies,
|
||||
mdl_PlanPrices,
|
||||
mdl_SystemBadges,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: achievement_definitions.mdl.js
|
||||
* Type of Program: Model
|
||||
* Description: Admin-managed catalog of achievements (badges and milestones).
|
||||
* Replaces the old static ACHIEVEMENT_REGISTRY in data/achievements.data.js.
|
||||
* `is_system` rows are the built-in keys referenced by name in
|
||||
* services/achievements.service.js's trigger functions — protected
|
||||
* from deletion/key-rename by the admin controller.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 3, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../../config/db.config');
|
||||
|
||||
const mdl_AchievementDefinitions = sequelize.define('AchievementDefinition', {
|
||||
achievement_definition_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, label: 'ID', hidden: true, order: 0 },
|
||||
key: { type: DataTypes.STRING(100), allowNull: false, unique: true, label: 'Key', hidden: false, order: 1, filterable: true },
|
||||
type: { type: DataTypes.ENUM('badge', 'milestone'), allowNull: false, defaultValue: 'badge', label: 'Type', 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 },
|
||||
icon: { type: DataTypes.STRING(50), allowNull: true, label: 'Icon', hidden: false, order: 5, filterable: false },
|
||||
trigger: { type: DataTypes.STRING(30), allowNull: true, label: 'Trigger', hidden: false, order: 6, filterable: true },
|
||||
is_active: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, label: 'Active', hidden: false, order: 7, filterable: true },
|
||||
is_system: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, label: 'System', hidden: false, order: 8, filterable: true },
|
||||
}, {
|
||||
tableName: 'achievement_definitions',
|
||||
timestamps: true,
|
||||
paranoid: false,
|
||||
});
|
||||
|
||||
module.exports = mdl_AchievementDefinitions;
|
||||
@@ -26,6 +26,7 @@ const mdl_Achievements = sequelize.define('Achievement', {
|
||||
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 },
|
||||
icon: { type: DataTypes.STRING(50), allowNull: true, label: 'Icon', hidden: false, order: 5, 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 },
|
||||
|
||||
@@ -43,9 +43,6 @@ const mdl_Users = sequelize.define('User', {
|
||||
*/
|
||||
personal_info: { type: DataTypes.JSONB, defaultValue: null, label: "Personal Info", order: 2 },
|
||||
|
||||
// ── Currency preference ──────────────────────────────────────────────────────
|
||||
preferred_currency: { type: DataTypes.CHAR(3), allowNull: false, defaultValue: 'USD', label: 'Preferred Currency' },
|
||||
|
||||
// ── Ban state ────────────────────────────────────────────────────────────────
|
||||
is_banned: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false, label: "Banned" },
|
||||
ban_expires_at: { type: DataTypes.DATE, defaultValue: null, allowNull: true, label: "Ban Expires At" },
|
||||
|
||||
Reference in New Issue
Block a user