chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+19
View File
@@ -0,0 +1,19 @@
/***********************************************************************************************************************************************************************
* File Name: achievements.data.js
* Type of Program: Data
* Description: Static config for the achievement system that isn't part of the
* admin-managed catalog. The achievement catalog itself (keys, type,
* label, description, icon) now lives in the achievement_definitions
* table — see models/users/achievement_definitions.mdl.js and
* controllers/admin/achievements.controller.js for CRUD.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 2026
***********************************************************************************************************************************************************************/
'use strict';
const EARLY_ACCESS_CUTOFF = new Date('2026-12-31T23:59:59Z');
module.exports = {
EARLY_ACCESS_CUTOFF,
};
+28
View File
@@ -0,0 +1,28 @@
/***********************************************************************************************************************************************************************
* File Name: cronPresets.data.js
* Type of Program: Data
* Description: Friendly schedule presets for admin-configurable notification
* crons. The UI only ever offers these six options — no raw cron
* expressions are accepted from the client, so `updateSetting`
* in notificationSettings.controller.js validates against this map.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 2, 2026
***********************************************************************************************************************************************************************/
'use strict';
const CRON_PRESETS = {
every_minute: '* * * * *',
every_5_min: '*/5 * * * *',
every_15_min: '*/15 * * * *',
hourly: '0 * * * *',
every_6_hours: '0 */6 * * *',
daily: '0 0 * * *',
};
// Reverse lookup — cron string -> preset key (used to label a job's current schedule)
const CRON_PRESET_BY_EXPRESSION = Object.fromEntries(
Object.entries(CRON_PRESETS).map(([key, expr]) => [expr, key])
);
module.exports = { CRON_PRESETS, CRON_PRESET_BY_EXPRESSION };
+198
View File
@@ -0,0 +1,198 @@
/***********************************************************************************************************************************************************************
* File Name: email_body.data.js
* Type of Program: Data
* Description: Hardcoded subject/body per email type, keyed by the `type`
* string passed to services/email.service.js's sendEmail(). To
* add or edit an email, edit the template function here directly
* and redeploy — there is no admin UI or DB table for this.
* Author: rgrgogu, Kenneth Obsequio (@lash0000)
* Date Created: Oct. 6, 2025
***********************************************************************************************************************************************************************/
const FONT = 'font-family: Arial, sans-serif; font-size: 14px; color: #000;';
const wrap = (body) => `
<html>
<body style="${FONT} line-height: 1.6;">
${body.trim()}
<br><br>
<p style="margin: 0;">Regards,<br>Philproperties IT Team</p>
<p style="margin: 0; font-size: 12px; color: #555;">This is an automated message from STARR System. Please do not reply.</p>
</body>
</html>`.trim();
const emailTemplates = {
OTP: ({ otp, expiryMinutes = 10 }) => ({
subject: "Email OTP Verification - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>Please use the One-Time Password (OTP) below to verify your email address. This code is valid for <strong>${expiryMinutes} minutes</strong>.</p>
<p style="font-size: 28px; font-weight: bold; letter-spacing: 6px;">${otp}</p>
<p>For security reasons, please do not share this code with anyone. If you did not request this, please contact the administrator.</p>
`),
}),
LOGIN_OTP: ({ otp, expiryMinutes = 10 }) => ({
subject: "Sign-In Verification Code - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>Use the One-Time Password (OTP) below to confirm this sign-in. This code is valid for <strong>${expiryMinutes} minutes</strong>.</p>
<p style="font-size: 28px; font-weight: bold; letter-spacing: 6px;">${otp}</p>
<p>For security reasons, please do not share this code with anyone. If you did not attempt to log in, please contact the administrator.</p>
`),
}),
RESET_PASSWORD_OTP: ({ otp, expiryMinutes = 10 }) => ({
subject: "Password Reset Code - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>Use the One-Time Password (OTP) below to reset your account password. This code is valid for <strong>${expiryMinutes} minutes</strong>.</p>
<p style="font-size: 28px; font-weight: bold; letter-spacing: 6px;">${otp}</p>
<p>For security reasons, please do not share this code with anyone. If you did not request a password reset, please ignore this email or contact the administrator.</p>
`),
}),
WELCOME: ({ name }) => ({
subject: "Welcome to STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>We are pleased to welcome you to the STARR System. Your account has been successfully created and is now ready for use.</p>
<p>You may now access your dashboard and begin using the available services.</p>
<p>We look forward to supporting you.</p>
`),
}),
PASSWORD_CHANGED: () => ({
subject: "Password Update Confirmation - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>This is to confirm that your account password has been successfully changed.</p>
<p>If you did not perform this action, please reset your password immediately or contact support.</p>
<p>For your security, we recommend using a strong and unique password.</p>
`),
}),
ADDED_TO_GROUP: ({ groupName }) => ({
subject: "Group Assignment Notification - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>You have been assigned to the group <strong>${groupName}</strong> in the STARR System.</p>
<p>This assignment grants you access to shared resources and collaboration tools within the group.</p>
<p>Please log in to your account to view group details.</p>
`),
}),
TASK_ASSIGNED: ({ taskTitle, dueDate }) => ({
subject: "New Task Assignment - STARR System",
html: wrap(`
<p>Dear User,</p>
<p>You have been assigned a new task in the STARR System.</p>
<table style="${FONT}">
<tr><td><strong>Task</strong></td><td>${taskTitle}</td></tr>
<tr><td><strong>Due Date</strong></td><td>${dueDate}</td></tr>
</table>
<p>Kindly ensure completion within the specified timeframe.</p>
`),
}),
BAN_LIFTED: ({ name, email, date }) => ({
subject: "Account Suspension Lifted - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>We are writing to inform you that the suspension on your account (<strong>${email}</strong>) has been lifted effective <strong>${date}</strong>.</p>
<p>You may now log in and resume access to all services within the STARR System.</p>
<p>If you have any concerns, please do not hesitate to contact your administrator.</p>
`),
}),
BANNED: ({ name, email, date, reason, ban_type }) => ({
subject: "Account Suspension Notice - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>Your account (<strong>${email}</strong>) has been ${ban_type === 'permanent' ? 'permanently' : 'temporarily'} suspended from the STARR System effective <strong>${date}</strong>.</p>
<table style="${FONT}">
<tr><td><strong>Reason</strong></td><td>${reason}</td></tr>
<tr><td><strong>Duration</strong></td><td>${ban_type === 'permanent' ? 'Permanent' : 'Temporary'}</td></tr>
</table>
<p>During this period, access to all system services has been revoked.${ban_type === 'permanent' ? '' : ' This suspension may be lifted upon review by the administrator.'}</p>
<p>If you believe this was made in error, please contact your administrator.</p>
`),
}),
MADE_ADMIN: ({ name, email }) => ({
subject: "You've Been Granted Administrator Access - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p><strong>Congratulations!</strong></p>
<p>We're pleased to inform you that your account has been granted Administrator access to the platform.</p>
<p>As an Administrator, you now have access to management features that allow you to oversee users, courses, content, and other administrative functions.</p>
<table style="${FONT}">
<tr><td><strong>Email</strong></td><td>${email}</td></tr>
<tr><td><strong>Role</strong></td><td>Administrator</td></tr>
</table>
<p>Please sign in using your existing credentials. If you have any questions or require assistance, don't hesitate to contact our support team.</p>
`),
}),
DEMOTED_ADMIN: ({ name, email }) => ({
subject: "Your Account Role Has Been Updated - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>This email is to inform you that your account has been updated.</p>
<p>Your Administrator privileges have been removed, and your account has been returned to a User role.</p>
<table style="${FONT}">
<tr><td><strong>Email</strong></td><td>${email}</td></tr>
<tr><td><strong>Current Role</strong></td><td>User</td></tr>
</table>
<p>You can continue using the platform with the permissions available to standard users. If you believe this change was made in error or have any questions, please contact your system administrator.</p>
`),
}),
ADD_STAFF: ({ name, email, password, expiryHours = 24 }) => ({
subject: "Your Staff Account Has Been Created - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>Your staff account has been successfully created in the STARR System. Below are your login credentials:</p>
<table style="${FONT}">
<tr><td><strong>Email</strong></td><td>${email}</td></tr>
<tr><td><strong>Password</strong></td><td>${password}</td></tr>
</table>
<p>This temporary password is valid for <strong>${expiryHours} hours</strong>. You will be required to change it upon first login.</p>
<p>If you did not request this account or believe this was created in error, please contact your administrator immediately to have it deactivated.</p>
<p>For security, please do not share your credentials with anyone. If you need a new password, contact your administrator.</p>
`),
}),
TIER_ACCESS_REVOKED: ({ name, label, date }) => ({
subject: "Your Subscription Access Has Been Revoked - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>We're writing to inform you that your access under the following subscription plan has been revoked by our team, effective <strong>${date}</strong>:</p>
<table style="${FONT}">
<tr><td><strong>Plan</strong></td><td>${label}</td></tr>
<tr><td><strong>Status</strong></td><td>Access Revoked</td></tr>
<tr><td><strong>Refund</strong></td><td>Not applicable — this subscription is non-refundable</td></tr>
</table>
<p>Your account no longer has access to the courses, units, and lessons included in this plan. If you were not concurrently subscribed to another active plan, your account has reverted to the Free subscription.</p>
<p>If you believe this was done in error or have questions about this change, please contact our support team.</p>
`),
}),
REFUND_PROCESSED: ({ name, label, amount, currency, date, refundId }) => ({
subject: "Your Refund Has Been Processed - STARR System",
html: wrap(`
<p>Dear ${name},</p>
<p>We're writing to confirm that your refund request has been processed as of <strong>${date}</strong>:</p>
<table style="${FONT}">
<tr><td><strong>Plan</strong></td><td>${label}</td></tr>
<tr><td><strong>Amount Refunded</strong></td><td>${currency} ${amount}</td></tr>
<tr><td><strong>Refund ID</strong></td><td>${refundId}</td></tr>
<tr><td><strong>Status</strong></td><td>Refunded</td></tr>
</table>
<p>Your access to the courses, units, and lessons included in this plan has been revoked effective immediately. If you were not concurrently subscribed to another active plan, your account has reverted to the Free subscription.</p>
<p>Please allow a few business days for the refunded amount to reflect on your original payment method, depending on your provider.</p>
<p>If you have any questions about this refund, please contact our support team.</p>
`),
}),
};
module.exports = { emailTemplates };
+432
View File
@@ -0,0 +1,432 @@
/***********************************************************************************************************************************************************************
* File Name: notifications.data.js
* Type of Program: Data
* Description: Central registry of all notification types for both admin and
* client (user) notifications.
*
* Each entry describes one notification type:
* type {string} — stored in the DB 'type' column
* scope {string} — 'admin' | 'user' | 'both'
* trigger {string} — what fires it (cron | event | manual)
* build {function} — takes a data payload, returns the object
* ready to pass to AdminNotification.create()
* or UserNotification.create() / bulkCreate()
*
* To add a new notification type:
* 1. Add an entry in the relevant section below.
* 2. Call NOTIFICATION_REGISTRY.<key>.build(data) at the trigger
* site (controller, cron, service).
* No other changes needed.
*
* Current types:
* Admin : task_overdue, user_registration, nogrp_user_registered
* User : task_requirements_updated, task_submissions_closed, task_submissions_reopened, user_task_overdue, user_task_auto_completed, task_reminder, achievement,
* course_unlocked, course_completed, certificate_issued, welcome,
* nogrp_welcome, assessment_updated, announcement, tier_expired,
* tier_plan_archived, tier_plan_access_revoked, payment_refunded,
* task_submission_reviewed, task_assigned, task_completed
* Both : broadcast (admin-composed, sent via notification_broadcasts CRUD)
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 19, 2026
* Date Modified: Jul. 11, 2026 — reverted from notification_templates (DB-editable) back to hardcoded
***********************************************************************************************************************************************************************/
'use strict';
const { fmtDate } = require('../utils/datetime.util');
const NOTIFICATION_REGISTRY = {
// ─────────────────────────────────────────────────────────────────────────
// ADMIN notifications (scope: 'admin')
// ─────────────────────────────────────────────────────────────────────────
// ── Task ──────────────────────────────────────────────────────────────────
task_overdue: {
type: 'task_overdue',
scope: 'admin',
trigger: 'cron',
build({ count, task_list_ids = [], targetStatus = 'overdue' }) {
const label = targetStatus === 'completed' ? 'completed' : 'overdue';
return {
type: 'task_overdue',
title: targetStatus === 'completed' ? 'Tasks Auto-Completed' : 'Tasks Overdue',
message: `${count} task${count === 1 ? ' was' : 's were'} automatically marked as ${label}.`,
data: { count, task_list_ids, target_status: targetStatus },
};
},
},
// ── User Registration ─────────────────────────────────────────────────────
user_registration: {
type: 'user_registration',
scope: 'admin',
trigger: 'event',
build({ groupName, groupCode, userEmail }) {
return {
type: 'user_registration',
title: 'New User Registered',
message: `A new user registered in ${groupName}.`,
data: { groupName, groupCode, userEmail },
};
},
},
// ── Unaffiliated User Registration ───────────────────────────────────────
nogrp_user_registered: {
type: 'nogrp_user_registered',
scope: 'admin',
trigger: 'event',
build({ userEmail, regType }) {
return {
type: 'nogrp_user_registered',
title: 'New Unaffiliated User',
message: `A new user (${userEmail}) registered via ${regType} without a group code and was placed in the default group.`,
data: { userEmail, regType },
};
},
},
// ─────────────────────────────────────────────────────────────────────────
// USER notifications (scope: 'user')
// ─────────────────────────────────────────────────────────────────────────
// ── Task ──────────────────────────────────────────────────────────────────
task_requirements_updated: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskName, taskListId = null, groupId = null, taskId = null }) {
return {
type: 'task',
title: 'Task Updated',
message: `The requirements for "${taskName}" have been updated by your administrator.`,
data: { taskName, taskListId, groupId, taskId },
};
},
},
task_submissions_closed: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskName, taskListId = null, groupId = null, taskId = null }) {
return {
type: 'task',
title: 'Submissions Closed',
message: `"${taskName}" is no longer accepting submissions.`,
data: { taskName, taskListId, groupId, taskId },
};
},
},
task_submissions_reopened: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskName, taskListId = null, groupId = null, taskId = null }) {
return {
type: 'task',
title: 'Submissions Reopened',
message: `"${taskName}" is accepting submissions again.`,
data: { taskName, taskListId, groupId, taskId },
};
},
},
user_task_overdue: {
type: 'task',
scope: 'user',
trigger: 'cron',
build({ count, task_list_ids = [], taskListId = null, taskId = null }) {
const label = count === 1 ? '1 task has' : `${count} tasks have`;
return {
type: 'task',
title: 'Tasks Overdue',
message: `${label} passed their deadline and been marked as overdue.`,
data: { count, task_list_ids, taskListId, taskId },
};
},
},
user_task_auto_completed: {
type: 'task',
scope: 'user',
trigger: 'cron',
build({ count, task_list_ids = [], taskListId = null, taskId = null }) {
const label = count === 1 ? '1 task has' : `${count} tasks have`;
return {
type: 'task',
title: 'Tasks Auto-Completed',
message: `${label} passed their deadline and been automatically marked as completed.`,
data: { count, task_list_ids, taskListId, taskId },
};
},
},
task_reminder: {
type: 'task',
scope: 'user',
trigger: 'cron',
build({ taskName, deadline, taskListId = null, groupId = null }) {
return {
type: 'task',
title: 'Task Deadline Approaching',
message: `"${taskName}" is due on ${fmtDate(deadline)}.`,
data: { taskName, deadline, taskListId, groupId },
};
},
},
// ── Task submission review (approve/reject) ─────────────────────────────
task_submission_reviewed: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskName, status, review_note = null }) {
const statusLabel = status === 'approved' ? 'approved' : 'rejected';
const reviewNoteSuffix = review_note ? ` Note: ${review_note}` : '';
return {
type: 'task',
title: 'Submission Reviewed',
message: `Your submission for "${taskName}" was ${statusLabel}.${reviewNoteSuffix}`,
data: { taskName, status, review_note },
};
},
},
// ── Task list assignment (group added to a task list) ───────────────────
task_assigned: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskListName, taskCount }) {
return {
type: 'task',
title: 'New Task Assigned',
message: `You have been assigned "${taskListName}" — ${taskCount} task(s) to complete.`,
data: { taskListName, taskCount },
};
},
},
// ── Per-task completion (0→1 transition) ─────────────────────────────────
task_completed: {
type: 'task',
scope: 'user',
trigger: 'event',
build({ taskName }) {
return {
type: 'task',
title: 'Task Completed',
message: `You completed "${taskName}".`,
data: { taskName },
};
},
},
// ── Achievement — title/message come from the achievement definition itself ─
achievement: {
type: 'achievement',
scope: 'user',
trigger: 'event',
build({ label, description, key }) {
return {
type: 'achievement',
title: label,
message: description,
data: { key },
};
},
},
// ── Course ────────────────────────────────────────────────────────────────
course_unlocked: {
type: 'course',
scope: 'user',
trigger: 'event',
build({ courseTitle, courseUuid = null }) {
return {
type: 'course',
title: 'New Course Available',
message: `"${courseTitle}" has been added to your learning library.`,
data: { courseTitle, courseUuid },
};
},
},
course_completed: {
type: 'course',
scope: 'user',
trigger: 'event',
build({ courseTitle, courseUuid = null }) {
return {
type: 'course',
title: 'Course Completed',
message: `Great job! You've completed "${courseTitle}". Your certificate will be issued within the next hour.`,
data: { courseTitle, courseUuid },
};
},
},
certificate_issued: {
type: 'course',
scope: 'user',
trigger: 'cron',
build({ courseTitle, courseUuid }) {
return {
type: 'course',
title: 'Certificate Issued',
message: `Congratulations! Your certificate for "${courseTitle}" is ready.`,
data: { courseTitle, courseUuid },
};
},
},
// ── Welcome ───────────────────────────────────────────────────────────────
welcome: {
type: 'announcement',
scope: 'user',
trigger: 'event',
build({ groupName, groupCode, accType, groupId = null }) {
const greeting = accType === 'admin'
? 'Welcome, Administrator!'
: accType === 'staff'
? 'Welcome to the Philproperties team!'
: 'Welcome to Philproperties!';
return {
type: 'announcement',
title: 'Welcome to Philproperties',
message: groupName ? `${greeting} You have been added to ${groupName}.` : greeting,
data: { groupName, groupCode, accType, groupId },
};
},
},
// ── No-Group Welcome ──────────────────────────────────────────────────────
nogrp_welcome: {
type: 'announcement',
scope: 'user',
trigger: 'event',
build() {
return {
type: 'announcement',
title: "You're Not in a Group Yet",
message: 'You are currently in the default group. Contact an administrator to be assigned to your team.',
data: { groupCode: 'NOGRP' },
};
},
},
// ── Assessment ────────────────────────────────────────────────────────────
assessment_updated: {
type: 'assessment',
scope: 'user',
trigger: 'event',
build({ assessmentTitle, courseTitle, courseUuid = null }) {
return {
type: 'assessment',
title: 'Assessment Updated',
message: `The administrator has updated the "${assessmentTitle || 'Course Assessment'}" in "${courseTitle || 'your course'}". Your current session is still valid — continue where you left off.`,
data: { assessmentTitle, courseTitle, courseUuid },
};
},
},
// ── Platform ──────────────────────────────────────────────────────────────
announcement: {
type: 'announcement',
scope: 'user',
trigger: 'manual',
build({ title, body }) {
return {
type: 'announcement',
title,
message: body,
data: {},
};
},
},
// ── Broadcast (admin-composed, manual) — title/message typed per-send via
// the notification_broadcasts CRUD, not a fixed template ─────────────────
broadcast: {
type: 'announcement',
scope: 'both',
trigger: 'manual',
build({ title, message, targetType = null, targetId = null, groupId = null, linkUrl = null, linkLabel = null }) {
return {
type: 'announcement',
title,
message,
data: { targetType, targetId, groupId, linkUrl, linkLabel },
};
},
},
// ── Tier ──────────────────────────────────────────────────────────────────
tier_expired: {
type: 'tier_expired',
scope: 'user',
trigger: 'cron',
build({ tier, label, planId = null }) {
return {
type: 'tier_expired',
title: 'Subscription Expired',
message: `Your ${label ?? tier} plan has expired. Renew to keep access.`,
data: { tier, label, planId },
};
},
},
tier_plan_archived: {
type: 'tier_plan_archived',
scope: 'user',
trigger: 'event',
build({ label, planId = null }) {
return {
type: 'tier_plan_archived',
title: 'Plan Archived',
message: `Your "${label}" plan has been archived and is no longer available for new subscriptions. Your current access is unaffected until it expires.`,
data: { label, planId },
};
},
},
// Distinct from tier_plan_archived above — that one explicitly says access
// is unaffected, which would be false here, so an admin force-revoke
// always fires THIS instead of (never alongside) tier_plan_archived.
tier_plan_access_revoked: {
type: 'tier_plan_access_revoked',
scope: 'user',
trigger: 'event',
build({ label, planId = null }) {
return {
type: 'tier_plan_access_revoked',
title: 'Subscription Access Revoked',
message: `Your access under the "${label}" plan has been revoked by an administrator, effective immediately. This subscription is non-refundable.`,
data: { label, planId },
};
},
},
// Fired when a user self-serves a refund within their plan's refund window
// (controllers/client/tiers.controller.js refundOrder) — distinct from
// tier_plan_access_revoked above, which is an admin-initiated, non-refundable cutoff.
payment_refunded: {
type: 'payment_refunded',
scope: 'user',
trigger: 'event',
build({ label, amount, currency, planId = null }) {
return {
type: 'payment_refunded',
title: 'Refund Processed',
message: `Your refund of ${currency} ${amount} for the "${label}" plan has been processed. Your access to this plan has been revoked.`,
data: { label, amount, currency, planId },
};
},
},
};
module.exports = { NOTIFICATION_REGISTRY };